More efficient UnityGUID. Resolves #720

This commit is contained in:
Jeremy Pritts 2023-03-20 03:11:42 -04:00
parent dbeba151d5
commit 2cad1dec77

View File

@ -1,6 +1,8 @@
using AssetRipper.IO.Endian;
using AssetRipper.IO.Files.Extensions;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@ -8,14 +10,24 @@ namespace AssetRipper.IO.Files
{
public record struct UnityGUID : IEndianReadable, IEndianWritable
{
public UnityGUID(Guid guid) : this(ConvertSystemOrUnityBytes(guid.ToByteArray())) { }
public UnityGUID(Guid guid)
{
Span<byte> guidData = stackalloc byte[16];
bool success = guid.TryWriteBytes(guidData);
Debug.Assert(success);
ConvertSystemOrUnityBytes(guidData, guidData);
Data0 = ReadUInt32LittleEndian(guidData, 0);
Data1 = ReadUInt32LittleEndian(guidData, 1);
Data2 = ReadUInt32LittleEndian(guidData, 2);
Data3 = ReadUInt32LittleEndian(guidData, 3);
}
public UnityGUID(ReadOnlySpan<byte> guidData)
{
Data0 = BinaryPrimitives.ReadUInt32LittleEndian(guidData.Slice(0, sizeof(uint)));
Data1 = BinaryPrimitives.ReadUInt32LittleEndian(guidData.Slice(4, sizeof(uint)));
Data2 = BinaryPrimitives.ReadUInt32LittleEndian(guidData.Slice(8, sizeof(uint)));
Data3 = BinaryPrimitives.ReadUInt32LittleEndian(guidData.Slice(12, sizeof(uint)));
Data0 = ReadUInt32LittleEndian(guidData, 0);
Data1 = ReadUInt32LittleEndian(guidData, 1);
Data2 = ReadUInt32LittleEndian(guidData, 2);
Data3 = ReadUInt32LittleEndian(guidData, 3);
}
public UnityGUID(uint data0, uint data1, uint data2, uint data3)
@ -26,11 +38,26 @@ namespace AssetRipper.IO.Files
Data3 = data3;
}
public static UnityGUID NewGuid() => new UnityGUID(Guid.NewGuid().ToByteArray());
public static UnityGUID NewGuid()
{
//This is not an acceptable way to convert between Unity and System Guids.
//We only do it this way to efficiently get 16 random bytes.
//We don't care about official Guid validity because Unity does not care either.
Guid guid = Guid.NewGuid();
ReadOnlySpan<Guid> guidSpan = MemoryMarshal.CreateReadOnlySpan(ref guid, 1);
ReadOnlySpan<byte> byteSpan = MemoryMarshal.Cast<Guid, byte>(guidSpan);
return new UnityGUID(byteSpan);
}
public static explicit operator UnityGUID(Guid systemGuid) => new UnityGUID(systemGuid);
public static explicit operator Guid(UnityGUID unityGuid) => new Guid(ConvertSystemOrUnityBytes(unityGuid.ToByteArray()));
public static explicit operator Guid(UnityGUID unityGuid)
{
Span<byte> span = stackalloc byte[16];
unityGuid.Write(span);
ConvertSystemOrUnityBytes(span, span);
return new Guid(span);
}
public void Read(EndianReader reader)
{
@ -48,13 +75,18 @@ namespace AssetRipper.IO.Files
writer.Write(Data3);
}
private void Write(Span<byte> span)
{
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(0 * sizeof(uint), sizeof(uint)), Data0);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(1 * sizeof(uint), sizeof(uint)), Data1);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(2 * sizeof(uint), sizeof(uint)), Data2);
BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(3 * sizeof(uint), sizeof(uint)), Data3);
}
public byte[] ToByteArray()
{
byte[] result = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(0, 4), Data0);
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(4, 4), Data1);
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(8, 4), Data2);
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(12, 4), Data3);
Write(result);
return result;
}
@ -83,46 +115,45 @@ namespace AssetRipper.IO.Files
sb.Append(StringBuilderExtensions.ByteHexRepresentations[unchecked((int)(value >> 20) & 0xF0) | unchecked((int)(value >> 28) & 0xF)]);
}
/// <summary>
/// Read little-endian <see cref="uint"/> from <see cref="Span{byte}"/>
/// </summary>
/// <param name="byteSpan">A span of bytes.</param>
/// <param name="index">The ith <see cref="uint"/> in <paramref name="byteSpan"/>.</param>
/// <returns></returns>
private static uint ReadUInt32LittleEndian(ReadOnlySpan<byte> byteSpan, int index)
{
return BinaryPrimitives.ReadUInt32LittleEndian(byteSpan.Slice(index * sizeof(uint), sizeof(uint)));
}
/// <summary>
/// Converts system bytes to unity bytes, or the reverse
/// </summary>
/// <param name="originalBytes">A 16 byte input array</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Array is null</exception>
/// <exception cref="ArgumentException">Array doesn't have 16 elements</exception>
private static byte[] ConvertSystemOrUnityBytes(byte[] originalBytes)
/// <param name="input">A 16 byte input span</param>
/// <returns>The same span: <paramref name="input"/></returns>
/// <exception cref="ArgumentException">Span doesn't have 16 elements</exception>
private static void ConvertSystemOrUnityBytes(scoped ReadOnlySpan<byte> input, scoped Span<byte> output)
{
if (originalBytes is null)
if (input.Length != 16)
{
throw new ArgumentNullException(nameof(originalBytes));
throw new ArgumentException($"Invalid length: {input.Length}", nameof(input));
}
if (output.Length != 16)
{
throw new ArgumentException($"Invalid length: {output.Length}", nameof(output));
}
if (originalBytes.Length != 16)
{
throw new ArgumentException($"Invalid length: {originalBytes.Length}", nameof(originalBytes));
}
byte[] newBytes = new byte[16];
for (int i = 0; i < 4; i++)
{
newBytes[i] = originalBytes[3 - i];
}
newBytes[4] = originalBytes[5];
newBytes[5] = originalBytes[4];
newBytes[6] = originalBytes[7];
newBytes[7] = originalBytes[6];
for (int i = 8; i < 16; i++)
{
newBytes[i] = originalBytes[i];
}
//Unity Guid's are in big endian, so the bytes have to be flipped for multibyte fields
(output[0], output[1], output[2], output[3]) = (input[3], input[2], input[1], input[0]);
(output[4], output[5]) = (input[5], input[4]);
(output[6], output[7]) = (input[7], input[6]);
input.Slice(8).CopyTo(output.Slice(8));
for (int i = 0; i < 16; i++)
{
//AB becomes BA
byte value = newBytes[i];
newBytes[i] = (byte)(unchecked((int)(value << 4) & 0xF0) | unchecked((int)(value >> 4) & 0xF));
uint value = output[i];
output[i] = (byte)(unchecked((value << 4) & 0xF0) | unchecked((value >> 4) & 0xF));
}
return newBytes;
}
public static UnityGUID Parse(string guidString) => new UnityGUID(Guid.Parse(guidString));
@ -145,13 +176,14 @@ namespace AssetRipper.IO.Files
/// </remarks>
/// <param name="input">Input data. Can be any length</param>
/// <returns>A stable guid corresponding to the <paramref name="input"/>.</returns>
public static UnityGUID Md5Hash(ReadOnlySpan<byte> input)
public static UnityGUID Md5Hash(scoped ReadOnlySpan<byte> input)
{
byte[] hashBytes = MD5.HashData(input);
return new UnityGUID(ConvertSystemOrUnityBytes(hashBytes));
ConvertSystemOrUnityBytes(hashBytes, hashBytes);
return new UnityGUID(hashBytes);
}
public static UnityGUID Md5Hash(ReadOnlySpan<byte> assemblyName, ReadOnlySpan<byte> @namespace, ReadOnlySpan<byte> className)
public static UnityGUID Md5Hash(scoped ReadOnlySpan<byte> assemblyName, scoped ReadOnlySpan<byte> @namespace, scoped ReadOnlySpan<byte> className)
{
int length = assemblyName.Length + @namespace.Length + className.Length;
Span<byte> input = length < 1024 ? stackalloc byte[length] : GC.AllocateUninitializedArray<byte>(length);