mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 20:05:45 -10:00
553 lines
14 KiB
C#
553 lines
14 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using LibCpp2IL.Metadata;
|
|
|
|
namespace LibCpp2IL;
|
|
|
|
public abstract class ClassReadingBinaryReader(Stream input) : EndianAwareBinaryReader(input)
|
|
{
|
|
/// <summary>
|
|
/// Set this to true to enable storing of amount of bytes read of each readable structure.
|
|
/// </summary>
|
|
public static bool EnableReadableSizeInformation = false;
|
|
|
|
private SpinLock PositionShiftLock;
|
|
|
|
public bool is32Bit;
|
|
private readonly MemoryStream? _memoryStream = input as MemoryStream;
|
|
|
|
public ulong PointerSize => is32Bit ? 4ul : 8ul;
|
|
|
|
protected bool _hasFinishedInitialRead;
|
|
private bool _inReadableRead;
|
|
public ConcurrentDictionary<Type, int> BytesReadPerClass = new();
|
|
|
|
public abstract float MetadataVersion { get; }
|
|
|
|
public long Position
|
|
{
|
|
get => BaseStream.Position;
|
|
set => BaseStream.Position = value;
|
|
}
|
|
|
|
public long Length => BaseStream.Length;
|
|
|
|
internal virtual object? ReadPrimitive(Type type, bool overrideArchCheck = false)
|
|
{
|
|
if (type == typeof(bool))
|
|
return ReadBoolean();
|
|
|
|
if (type == typeof(char))
|
|
return ReadChar();
|
|
|
|
if (type == typeof(int))
|
|
return ReadInt32();
|
|
|
|
if (type == typeof(uint))
|
|
return ReadUInt32();
|
|
|
|
if (type == typeof(short))
|
|
return ReadInt16();
|
|
|
|
if (type == typeof(ushort))
|
|
return ReadUInt16();
|
|
|
|
if (type == typeof(sbyte))
|
|
return ReadSByte();
|
|
|
|
if (type == typeof(byte))
|
|
return ReadByte();
|
|
|
|
if (type == typeof(long))
|
|
return is32Bit && !overrideArchCheck ? ReadInt32() : ReadInt64();
|
|
|
|
if (type == typeof(ulong))
|
|
return is32Bit && !overrideArchCheck ? ReadUInt32() : ReadUInt64();
|
|
|
|
if (type == typeof(float))
|
|
return ReadSingle();
|
|
|
|
if (type == typeof(double))
|
|
return ReadDouble();
|
|
|
|
return null;
|
|
}
|
|
|
|
public uint ReadUnityCompressedUIntAtRawAddr(long offset, out int bytesRead)
|
|
{
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
return ReadUnityCompressedUIntAtRawAddrNoLock(offset, out bytesRead);
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
protected internal uint ReadUnityCompressedUIntAtRawAddrNoLock(long offset, out int bytesRead)
|
|
{
|
|
if (offset >= 0)
|
|
Position = offset;
|
|
|
|
//Ref Unity.IL2CPP.dll, Unity.IL2CPP.Metadata.MetadataUtils::WriteCompressedUInt32
|
|
//Read first byte
|
|
var b = ReadByte();
|
|
bytesRead = 1;
|
|
if (b < 128)
|
|
return b;
|
|
if (b == 240)
|
|
{
|
|
//Full Uint
|
|
bytesRead = 5;
|
|
return ReadUInt32();
|
|
}
|
|
|
|
//Special constant values
|
|
if (b == byte.MaxValue)
|
|
return uint.MaxValue;
|
|
if (b == 254)
|
|
return uint.MaxValue - 1;
|
|
|
|
if ((b & 192) == 192)
|
|
{
|
|
//3 more to read
|
|
bytesRead = 4;
|
|
return (b & ~192U) << 24 | (uint)(ReadByte() << 16) | (uint)(ReadByte() << 8) | ReadByte();
|
|
}
|
|
|
|
if ((b & 128) == 128)
|
|
{
|
|
//1 more to read
|
|
bytesRead = 2;
|
|
return (b & ~128U) << 8 | ReadByte();
|
|
}
|
|
|
|
|
|
throw new Exception($"How did we even get here? Invalid compressed int first byte {b}");
|
|
}
|
|
|
|
public int ReadUnityCompressedIntAtRawAddr(long position, out int bytesRead)
|
|
=> ReadUnityCompressedIntAtRawAddr(position, true, out bytesRead);
|
|
|
|
protected internal int ReadUnityCompressedIntAtRawAddr(long position, bool doLock, out int bytesRead)
|
|
{
|
|
//Ref libil2cpp, il2cpp\utils\ReadCompressedInt32
|
|
uint unsigned;
|
|
if (doLock)
|
|
unsigned = ReadUnityCompressedUIntAtRawAddr(position, out bytesRead);
|
|
else
|
|
unsigned = ReadUnityCompressedUIntAtRawAddrNoLock(position, out bytesRead);
|
|
|
|
if (unsigned == uint.MaxValue)
|
|
return int.MinValue;
|
|
|
|
var isNegative = (unsigned & 1) == 1;
|
|
unsigned >>= 1;
|
|
if (isNegative)
|
|
return -(int)(unsigned + 1);
|
|
|
|
return (int)unsigned;
|
|
}
|
|
|
|
private T InternalReadClass<T>(bool overrideArchCheck = false) where T : new()
|
|
{
|
|
return (T)InternalReadClass(typeof(T), overrideArchCheck);
|
|
}
|
|
|
|
private T InternalReadReadableClass<T>() where T : ReadableClass, new()
|
|
{
|
|
var t = new T { MetadataVersion = MetadataVersion };
|
|
OnReadableCreated(t);
|
|
|
|
if (!_inReadableRead)
|
|
{
|
|
_inReadableRead = true;
|
|
t.Read(this);
|
|
_inReadableRead = false;
|
|
}
|
|
else
|
|
{
|
|
t.Read(this);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after a ReadableClass instance is created but before Read is called.
|
|
/// Override in subclasses to set OwningContext on newly created instances.
|
|
/// </summary>
|
|
protected virtual void OnReadableCreated(ReadableClass instance) { }
|
|
|
|
private object InternalReadClass(Type type, bool overrideArchCheck = false)
|
|
{
|
|
if (type.IsPrimitive)
|
|
{
|
|
return ReadAndConvertPrimitive(overrideArchCheck, type);
|
|
}
|
|
|
|
if (type.IsEnum)
|
|
{
|
|
var value = ReadPrimitive(type.GetEnumUnderlyingType());
|
|
|
|
return value!;
|
|
}
|
|
|
|
throw new("Support for reading classes has been removed. Please inherit from ReadableClass and call ReadReadable on your local binary reader.");
|
|
}
|
|
|
|
private object ReadAndConvertPrimitive(bool overrideArchCheck, Type type)
|
|
{
|
|
var value = ReadPrimitive(type, overrideArchCheck);
|
|
|
|
//32-bit fixes...
|
|
if (value is uint && type == typeof(ulong))
|
|
value = Convert.ToUInt64(value);
|
|
if (value is int && type == typeof(long))
|
|
value = Convert.ToInt64(value);
|
|
|
|
return value!;
|
|
}
|
|
|
|
public T[] ReadClassArrayAtRawAddr<T>(long offset, long count) where T : new()
|
|
{
|
|
var t = new T[count];
|
|
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
if (offset != -1) Position = offset;
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
t[i] = InternalReadClass<T>();
|
|
}
|
|
|
|
return t;
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
public Il2CppVariableWidthIndex<T>[] ReadIndexArrayAtRawAddress<T>(long offset, long count) where T : ReadableClass
|
|
{
|
|
var t = new Il2CppVariableWidthIndex<T>[count];
|
|
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
if (offset != -1) Position = offset;
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
t[i] = Il2CppVariableWidthIndex<T>.Read(this);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
public string ReadStringToNull(ulong offset) => ReadStringToNull((long)offset);
|
|
|
|
public virtual string ReadStringToNull(long offset)
|
|
{
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
return ReadStringToNullNoLock(offset);
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
internal string ReadStringToNullNoLock(long offset)
|
|
{
|
|
var builder = new List<byte>();
|
|
|
|
if (offset != -1)
|
|
Position = offset;
|
|
|
|
try
|
|
{
|
|
byte b;
|
|
while ((b = ReadByte()) != 0)
|
|
builder.Add(b);
|
|
|
|
return Encoding.UTF8.GetString(builder.ToArray());
|
|
}
|
|
finally
|
|
{
|
|
var bytesRead = (int)(Position - offset);
|
|
TrackRead<string>(bytesRead);
|
|
}
|
|
}
|
|
|
|
public string ReadStringToNullAtCurrentPos()
|
|
=> ReadStringToNullNoLock(-1);
|
|
|
|
public byte[] ReadByteArrayAtRawAddress(long offset, int count)
|
|
{
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
return ReadByteArrayAtRawAddressNoLock(offset, count);
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
protected internal byte[] ReadByteArrayAtRawAddressNoLock(long offset, int count)
|
|
{
|
|
if (offset != -1)
|
|
Position = offset;
|
|
|
|
if (count == 0)
|
|
return [];
|
|
|
|
try
|
|
{
|
|
var ret = new byte[count];
|
|
Read(ret, 0, count);
|
|
|
|
return ret;
|
|
}
|
|
finally
|
|
{
|
|
TrackRead<byte>(count, false);
|
|
}
|
|
}
|
|
|
|
protected internal void GetLockOrThrow()
|
|
{
|
|
var obtained = false;
|
|
PositionShiftLock.Enter(ref obtained);
|
|
|
|
if (!obtained)
|
|
throw new Exception("Failed to obtain lock");
|
|
}
|
|
|
|
protected internal void ReleaseLock()
|
|
{
|
|
PositionShiftLock.Exit();
|
|
}
|
|
|
|
public ulong ReadNUintAtRawAddress(long offset)
|
|
{
|
|
if (offset > Length)
|
|
throw new EndOfStreamException($"ReadNUintAtRawAddress: Offset 0x{offset:X} is beyond the end of the stream (length 0x{Length:X})");
|
|
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
Position = offset;
|
|
return ReadNUint();
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
|
|
TrackRead<ulong>((int)PointerSize, false);
|
|
}
|
|
}
|
|
|
|
public ulong[] ReadNUintArrayAtRawAddress(long offset, int count)
|
|
{
|
|
if (offset > Length)
|
|
throw new EndOfStreamException($"ReadNUintArrayAtRawAddress: Offset 0x{offset:X} is beyond the end of the stream (length 0x{Length:X})");
|
|
|
|
var inBounds = offset + count * (int)PointerSize <= Length;
|
|
if (!inBounds)
|
|
throw new EndOfStreamException($"ReadNUintArrayAtRawAddress: Attempted to read {count} pointers (pointer length {PointerSize}) at offset 0x{offset:X}, but this goes beyond the end of the stream (length 0x{Length:X})");
|
|
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
Position = offset;
|
|
|
|
var ret = new ulong[count];
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
ret[i] = ReadNUint();
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
|
|
var bytesRead = count * (int)PointerSize;
|
|
TrackRead<ulong>(bytesRead, false);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Read a native-sized integer (i.e. 32 or 64 bit, depending on platform) at the current position
|
|
/// </summary>
|
|
public virtual long ReadNInt() => is32Bit ? ReadInt32() : ReadInt64();
|
|
|
|
/// <summary>
|
|
/// Read a native-sized unsigned integer (i.e. 32 or 64 bit, depending on platform) at the current position
|
|
/// </summary>
|
|
public virtual ulong ReadNUint() => is32Bit ? ReadUInt32() : ReadUInt64();
|
|
|
|
protected void WriteWord(int position, ulong word) => WriteWord(position, (long)word);
|
|
|
|
/// <summary>
|
|
/// Used for ELF Relocations.
|
|
/// </summary>
|
|
protected void WriteWord(int position, long word)
|
|
{
|
|
if (_memoryStream == null)
|
|
throw new("WriteWord is not supported in non-memory-backed readers");
|
|
|
|
GetLockOrThrow();
|
|
|
|
try
|
|
{
|
|
byte[] rawBytes;
|
|
if (is32Bit)
|
|
{
|
|
var value = (int)word;
|
|
rawBytes = BitConverter.GetBytes(value);
|
|
}
|
|
else
|
|
{
|
|
rawBytes = BitConverter.GetBytes(word);
|
|
}
|
|
|
|
if (BitConverter.IsLittleEndian != IsLittleEndian)
|
|
Array.Reverse(rawBytes);
|
|
|
|
if (position > _memoryStream.Length)
|
|
throw new Exception($"WriteWord: Position {position} beyond length {_memoryStream.Length}");
|
|
|
|
var count = is32Bit ? 4 : 8;
|
|
if (position + count > _memoryStream.Length)
|
|
throw new Exception($"WriteWord: Writing {count} bytes at {position} would go beyond length {_memoryStream.Length}");
|
|
|
|
if (rawBytes.Length != count)
|
|
throw new Exception($"WriteWord: Expected {count} bytes from BitConverter, got {position}");
|
|
|
|
try
|
|
{
|
|
_memoryStream.Seek(position, SeekOrigin.Begin);
|
|
_memoryStream.Write(rawBytes, 0, count);
|
|
}
|
|
catch
|
|
{
|
|
Logging.LibLogger.ErrorNewline("WriteWord: Unexpected exception!");
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
public T ReadReadableHereNoLock<T>() where T : ReadableClass, new() => InternalReadReadableClass<T>();
|
|
|
|
public T ReadReadable<T>(long offset = -1) where T : ReadableClass, new()
|
|
{
|
|
GetLockOrThrow();
|
|
|
|
if (offset >= 0)
|
|
Position = offset;
|
|
|
|
var initialPos = Position;
|
|
|
|
try
|
|
{
|
|
return InternalReadReadableClass<T>();
|
|
}
|
|
finally
|
|
{
|
|
var bytesRead = (int)(Position - initialPos);
|
|
TrackRead<T>(bytesRead, trackIfFinishedReading: true);
|
|
|
|
ReleaseLock();
|
|
}
|
|
}
|
|
|
|
public T[] ReadReadableArrayAtRawAddr<T>(long offset, long count) where T : ReadableClass, new()
|
|
{
|
|
var t = new T[count];
|
|
|
|
GetLockOrThrow();
|
|
|
|
if (offset != -1)
|
|
Position = offset;
|
|
|
|
try
|
|
{
|
|
//This handles the actual reading into the array, and tracking read counts, for us.
|
|
FillReadableArrayHereNoLock(t);
|
|
}
|
|
finally
|
|
{
|
|
ReleaseLock();
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
public void FillReadableArrayHereNoLock<T>(T[] array, int startOffset = 0) where T : ReadableClass, new()
|
|
{
|
|
var initialPos = Position;
|
|
|
|
try
|
|
{
|
|
var i = startOffset;
|
|
for (; i < array.Length; i++)
|
|
{
|
|
array[i] = InternalReadReadableClass<T>();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
var bytesRead = (int)(Position - initialPos);
|
|
TrackRead<T>(bytesRead, trackIfFinishedReading: true);
|
|
}
|
|
}
|
|
|
|
public void TrackRead<T>(int bytesRead, bool trackIfInReadableRead = true, bool trackIfFinishedReading = false)
|
|
{
|
|
if (!EnableReadableSizeInformation)
|
|
return;
|
|
|
|
if (!trackIfInReadableRead && _inReadableRead)
|
|
return;
|
|
|
|
if (_hasFinishedInitialRead && !trackIfFinishedReading)
|
|
return;
|
|
|
|
BytesReadPerClass[typeof(T)] = BytesReadPerClass.GetOrDefault(typeof(T)) + bytesRead;
|
|
}
|
|
}
|