// dnlib: See LICENSE.txt for more info using System; using System.Diagnostics; using System.IO; using System.Runtime.Serialization; using System.Text; using dnlib.DotNet.Writer; namespace dnlib.IO { /// /// Thrown by a when it can't read data or if the caller tries to set an invalid offset /// [Serializable] public sealed class DataReaderException : IOException { internal DataReaderException(string message) : base(message) { } internal DataReaderException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Reads data /// [DebuggerDisplay("{StartOffset,h}-{EndOffset,h} Length={Length} BytesLeft={BytesLeft}")] public struct DataReader { /// /// Gets the start offset of the data /// public readonly uint StartOffset => startOffset; /// /// Gets the end offset of the data, not inclusive /// public readonly uint EndOffset => endOffset; /// /// Gets the total length of the data /// public readonly uint Length => endOffset - startOffset; /// /// Gets the current offset. This is between and (inclusive) /// public uint CurrentOffset { readonly get => currentOffset; set { VerifyState(); if (value < startOffset || value > endOffset) { // Invalid offsets should be an IOException and not an ArgumentException ThrowDataReaderException($"Invalid new {nameof(CurrentOffset)}"); } currentOffset = value; VerifyState(); } } /// /// Gets/sets the position relative to /// public uint Position { readonly get => currentOffset - startOffset; set { VerifyState(); if (value > Length) { // Invalid positions should be an IOException and not an ArgumentException ThrowDataReaderException($"Invalid new {nameof(Position)}"); } currentOffset = startOffset + value; VerifyState(); } } /// /// Gets the number of bytes that can be read without throwing an exception /// public readonly uint BytesLeft => endOffset - currentOffset; readonly DataStream stream; readonly uint startOffset; readonly uint endOffset; uint currentOffset; /// /// Constructor /// /// Stream /// Start offset of data /// Length of data public DataReader(DataStream stream, uint offset, uint length) { Debug.Assert(stream is not null || (offset == 0 && length == 0)); Debug.Assert(offset + length >= offset); this.stream = stream; startOffset = offset; endOffset = offset + length; currentOffset = offset; VerifyState(); } [Conditional("DEBUG")] readonly void VerifyState() { Debug.Assert(startOffset <= currentOffset); Debug.Assert(currentOffset <= endOffset); } static void ThrowNoMoreBytesLeft() => throw new DataReaderException("There's not enough bytes left to read"); static void ThrowDataReaderException(string message) => throw new DataReaderException(message); static void ThrowInvalidOperationException() => throw new InvalidOperationException(); static void ThrowArgumentNullException(string paramName) => throw new ArgumentNullException(paramName); static void ThrowInvalidArgument(string paramName) => throw new DataReaderException("Invalid argument value"); /// /// Resets the reader so it points to the start of the data /// public void Reset() => currentOffset = startOffset; /// /// Creates a new reader that can access a smaller part of this reader /// /// Start position relative to /// Length of data /// public readonly DataReader Slice(uint start, uint length) { if ((ulong)start + length > Length) ThrowInvalidArgument(nameof(length)); return new DataReader(stream, startOffset + start, length); } /// /// Creates a new reader that can access everything from to the end of the data /// /// Start position relative to /// public readonly DataReader Slice(uint start) { if (start > Length) ThrowInvalidArgument(nameof(start)); return Slice(start, Length - start); } /// /// Creates a new reader that can access a smaller part of this reader /// /// Start position relative to /// Length of data /// public readonly DataReader Slice(int start, int length) { if (start < 0) ThrowInvalidArgument(nameof(start)); if (length < 0) ThrowInvalidArgument(nameof(length)); return Slice((uint)start, (uint)length); } /// /// Creates a new reader that can access everything from to the end of the data /// /// Start position relative to /// public readonly DataReader Slice(int start) { if (start < 0) ThrowInvalidArgument(nameof(start)); if ((uint)start > Length) ThrowInvalidArgument(nameof(start)); return Slice((uint)start, Length - (uint)start); } /// /// Checks if it's possible to read bytes /// /// Length of data /// public readonly bool CanRead(int length) => length >= 0 && (uint)length <= BytesLeft; /// /// Checks if it's possible to read bytes /// /// Length of data /// public readonly bool CanRead(uint length) => length <= BytesLeft; /// /// Reads a /// /// public bool ReadBoolean() { VerifyState(); const uint SIZE = 1; var currentOffset = this.currentOffset; if (currentOffset == endOffset) ThrowNoMoreBytesLeft(); var value = stream.ReadBoolean(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public char ReadChar() { VerifyState(); const uint SIZE = 2; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadChar(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public sbyte ReadSByte() { VerifyState(); const uint SIZE = 1; var currentOffset = this.currentOffset; if (currentOffset == endOffset) ThrowNoMoreBytesLeft(); var value = stream.ReadSByte(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public byte ReadByte() { VerifyState(); const uint SIZE = 1; var currentOffset = this.currentOffset; if (currentOffset == endOffset) ThrowNoMoreBytesLeft(); var value = stream.ReadByte(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public short ReadInt16() { VerifyState(); const uint SIZE = 2; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadInt16(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public ushort ReadUInt16() { VerifyState(); const uint SIZE = 2; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadUInt16(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public int ReadInt32() { VerifyState(); const uint SIZE = 4; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadInt32(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public uint ReadUInt32() { VerifyState(); const uint SIZE = 4; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadUInt32(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } internal byte Unsafe_ReadByte() { VerifyState(); const uint SIZE = 1; var currentOffset = this.currentOffset; Debug.Assert(currentOffset != endOffset); var value = stream.ReadByte(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } internal ushort Unsafe_ReadUInt16() { VerifyState(); const uint SIZE = 2; var currentOffset = this.currentOffset; Debug.Assert(endOffset - currentOffset >= SIZE); var value = stream.ReadUInt16(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } internal uint Unsafe_ReadUInt32() { VerifyState(); const uint SIZE = 4; var currentOffset = this.currentOffset; Debug.Assert(endOffset - currentOffset >= SIZE); var value = stream.ReadUInt32(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public long ReadInt64() { VerifyState(); const uint SIZE = 8; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadInt64(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public ulong ReadUInt64() { VerifyState(); const uint SIZE = 8; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadUInt64(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public float ReadSingle() { VerifyState(); const uint SIZE = 4; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadSingle(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public double ReadDouble() { VerifyState(); const uint SIZE = 8; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadDouble(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public Guid ReadGuid() { VerifyState(); const uint SIZE = 16; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadGuid(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a /// /// public decimal ReadDecimal() { VerifyState(); const uint SIZE = 16; var currentOffset = this.currentOffset; if (endOffset - currentOffset < SIZE) ThrowNoMoreBytesLeft(); var value = stream.ReadDecimal(currentOffset); this.currentOffset = currentOffset + SIZE; VerifyState(); return value; } /// /// Reads a UTF-16 encoded /// /// Number of characters to read /// public string ReadUtf16String(int chars) { if (chars < 0) ThrowInvalidArgument(nameof(chars)); if (chars == 0) return string.Empty; VerifyState(); uint length = (uint)chars * 2; var currentOffset = this.currentOffset; if (endOffset - currentOffset < length) ThrowNoMoreBytesLeft(); var s = length == 0 ? string.Empty : stream.ReadUtf16String(currentOffset, chars); this.currentOffset = currentOffset + length; VerifyState(); return s; } /// /// Reads bytes /// /// Destination pointer /// Number of bytes to read public unsafe void ReadBytes(void* destination, int length) { if (destination is null && length != 0) ThrowArgumentNullException(nameof(destination)); if (length < 0) ThrowInvalidArgument(nameof(length)); // This is also true if 'this' is the 'default' instance ('stream' is null) if (length == 0) return; VerifyState(); var currentOffset = this.currentOffset; if (endOffset - currentOffset < (uint)length) ThrowNoMoreBytesLeft(); stream.ReadBytes(currentOffset, destination, length); this.currentOffset = currentOffset + (uint)length; VerifyState(); } /// /// Reads bytes /// /// Destination array /// Destination index /// Number of bytes to read public void ReadBytes(byte[] destination, int destinationIndex, int length) { if (destination is null) ThrowArgumentNullException(nameof(destination)); if (destinationIndex < 0) ThrowInvalidArgument(nameof(destinationIndex)); if (length < 0) ThrowInvalidArgument(nameof(length)); // This is also true if 'this' is the 'default' instance ('stream' is null) if (length == 0) return; VerifyState(); var currentOffset = this.currentOffset; if (endOffset - currentOffset < (uint)length) ThrowNoMoreBytesLeft(); stream.ReadBytes(currentOffset, destination, destinationIndex, length); this.currentOffset = currentOffset + (uint)length; VerifyState(); } /// /// Reads bytes /// /// Number of bytes to read /// public byte[] ReadBytes(int length) { if (length < 0) ThrowInvalidArgument(nameof(length)); if (length == 0) return Array2.Empty(); var data = new byte[length]; ReadBytes(data, 0, length); return data; } /// /// Reads a compressed /// /// Uncompressed /// public bool TryReadCompressedUInt32(out uint value) { VerifyState(); var currentOffset = this.currentOffset; var bytesLeft = endOffset - currentOffset; if (bytesLeft == 0) { value = 0; VerifyState(); return false; } var stream = this.stream; byte b = stream.ReadByte(currentOffset++); if ((b & 0x80) == 0) { value = b; this.currentOffset = currentOffset; VerifyState(); return true; } if ((b & 0xC0) == 0x80) { if (bytesLeft < 2) { value = 0; VerifyState(); return false; } value = (uint)(((b & 0x3F) << 8) | stream.ReadByte(currentOffset++)); this.currentOffset = currentOffset; VerifyState(); return true; } // The encoding 111x isn't allowed but the CLR sometimes doesn't verify this // and just assumes it's 110x. Don't fail if it's 111x, just assume it's 110x. if (bytesLeft < 4) { value = 0; VerifyState(); return false; } value = (uint)(((b & 0x1F) << 24) | (stream.ReadByte(currentOffset++) << 16) | (stream.ReadByte(currentOffset++) << 8) | stream.ReadByte(currentOffset++)); this.currentOffset = currentOffset; VerifyState(); return true; } /// /// Reads a compressed /// /// public uint ReadCompressedUInt32() { if (!TryReadCompressedUInt32(out uint value)) ThrowNoMoreBytesLeft(); return value; } /// /// Reads a compressed /// /// Uncompressed /// public bool TryReadCompressedInt32(out int value) { VerifyState(); var currentOffset = this.currentOffset; var bytesLeft = endOffset - currentOffset; if (bytesLeft == 0) { value = 0; VerifyState(); return false; } var stream = this.stream; byte b = stream.ReadByte(currentOffset++); if ((b & 0x80) == 0) { if ((b & 1) != 0) value = -0x40 | (b >> 1); else value = (b >> 1); this.currentOffset = currentOffset; VerifyState(); return true; } if ((b & 0xC0) == 0x80) { if (bytesLeft < 2) { value = 0; VerifyState(); return false; } uint tmp = (uint)(((b & 0x3F) << 8) | stream.ReadByte(currentOffset++)); if ((tmp & 1) != 0) value = -0x2000 | (int)(tmp >> 1); else value = (int)(tmp >> 1); this.currentOffset = currentOffset; VerifyState(); return true; } if ((b & 0xE0) == 0xC0) { if (bytesLeft < 4) { value = 0; VerifyState(); return false; } uint tmp = (uint)(((b & 0x1F) << 24) | (stream.ReadByte(currentOffset++) << 16) | (stream.ReadByte(currentOffset++) << 8) | stream.ReadByte(currentOffset++)); if ((tmp & 1) != 0) value = -0x10000000 | (int)(tmp >> 1); else value = (int)(tmp >> 1); this.currentOffset = currentOffset; VerifyState(); return true; } value = 0; VerifyState(); return false; } /// /// Reads a compressed /// /// public int ReadCompressedInt32() { if (!TryReadCompressedInt32(out int value)) ThrowNoMoreBytesLeft(); return value; } /// /// Reads a 7-bit encoded integer /// /// public uint Read7BitEncodedUInt32() { uint val = 0; int bits = 0; for (int i = 0; i < 5; i++) { byte b = ReadByte(); val |= (uint)(b & 0x7F) << bits; if ((b & 0x80) == 0) return val; bits += 7; } ThrowDataReaderException("Invalid encoded UInt32"); return 0; } /// /// Reads a 7-bit encoded integer /// /// public int Read7BitEncodedInt32() => (int)Read7BitEncodedUInt32(); /// /// Reads a serialized UTF-8 string /// /// public string ReadSerializedString() => ReadSerializedString(Encoding.UTF8); /// /// Reads a serialized string /// /// Encoding /// public string ReadSerializedString(Encoding encoding) { if (encoding is null) ThrowArgumentNullException(nameof(encoding)); int length = Read7BitEncodedInt32(); if (length < 0) ThrowNoMoreBytesLeft(); if (length == 0) return string.Empty; return ReadString(length, encoding); } /// /// Returns all data without updating the current position /// /// public readonly byte[] ToArray() { int length = (int)Length; if (length < 0) ThrowInvalidOperationException(); // This is also true if 'this' is the 'default' instance ('stream' is null) if (length == 0) return Array2.Empty(); var data = new byte[length]; stream.ReadBytes(startOffset, data, 0, data.Length); return data; } /// /// Returns the remaining data /// /// public byte[] ReadRemainingBytes() { int length = (int)BytesLeft; if (length < 0) ThrowInvalidOperationException(); return ReadBytes(length); } /// /// Reads all bytes until a terminating byte or returns null if wasn't found. /// If found, the current offset is incremented by the length of the returned data /// /// Terminating byte value /// public byte[] TryReadBytesUntil(byte value) { var currentOffset = this.currentOffset; var endOffset = this.endOffset; // This is also true if 'this' is the 'default' instance ('stream' is null) if (currentOffset == endOffset) return null; if (!stream.TryGetOffsetOf(currentOffset, endOffset, value, out var valueOffset)) return null; int length = (int)(valueOffset - currentOffset); if (length < 0) return null; return ReadBytes(length); } /// /// Reads a zero-terminated UTF-8 string or returns null if the string couldn't be read. /// If successful, the current offset is incremented past the terminating zero. /// /// public string TryReadZeroTerminatedUtf8String() => TryReadZeroTerminatedString(Encoding.UTF8); /// /// Reads a zero-terminated string or returns null if the string couldn't be read. /// If successful, the current offset is incremented past the terminating zero. /// /// Encoding /// public string TryReadZeroTerminatedString(Encoding encoding) { if (encoding is null) ThrowArgumentNullException(nameof(encoding)); VerifyState(); var currentOffset = this.currentOffset; var endOffset = this.endOffset; // This is also true if 'this' is the 'default' instance ('stream' is null) if (currentOffset == endOffset) return null; if (!stream.TryGetOffsetOf(currentOffset, endOffset, 0, out var valueOffset)) return null; int length = (int)(valueOffset - currentOffset); if (length < 0) return null; var value = length == 0 ? string.Empty : stream.ReadString(currentOffset, length, encoding); this.currentOffset = valueOffset + 1; VerifyState(); return value; } /// /// Reads a UTF-8 encoded string /// /// Number of bytes to read (not characters) /// public string ReadUtf8String(int byteCount) => ReadString(byteCount, Encoding.UTF8); /// /// Reads a string /// /// Number of bytes to read (not characters) /// Encoding /// public string ReadString(int byteCount, Encoding encoding) { if (byteCount < 0) ThrowInvalidArgument(nameof(byteCount)); if (encoding is null) ThrowArgumentNullException(nameof(encoding)); if (byteCount == 0) return string.Empty; if ((uint)byteCount > Length) ThrowInvalidArgument(nameof(byteCount)); VerifyState(); var currentOffset = this.currentOffset; var value = stream.ReadString(currentOffset, byteCount, encoding); this.currentOffset = currentOffset + (uint)byteCount; VerifyState(); return value; } /// /// Creates a that can access this content. The caller doesn't have to dispose of the returned stream. /// /// public readonly Stream AsStream() => new DataReaderStream(this); readonly byte[] AllocTempBuffer() => new byte[(int)Math.Min(0x2000, BytesLeft)]; /// /// Copies the data, starting from , to /// /// Destination /// Number of bytes written public void CopyTo(DataWriter destination) { if (destination is null) ThrowArgumentNullException(nameof(destination)); if (Position >= Length) return; CopyTo(destination.InternalStream, AllocTempBuffer()); } /// /// Copies the data, starting from , to /// /// Destination /// Temp buffer during writing /// Number of bytes written public void CopyTo(DataWriter destination, byte[] dataBuffer) { if (destination is null) ThrowArgumentNullException(nameof(destination)); CopyTo(destination.InternalStream, dataBuffer); } /// /// Copies the data, starting from , to /// /// Destination /// Number of bytes written public void CopyTo(BinaryWriter destination) { if (destination is null) ThrowArgumentNullException(nameof(destination)); if (Position >= Length) return; CopyTo(destination.BaseStream, AllocTempBuffer()); } /// /// Copies the data, starting from , to /// /// Destination /// Temp buffer during writing /// Number of bytes written public void CopyTo(BinaryWriter destination, byte[] dataBuffer) { if (destination is null) ThrowArgumentNullException(nameof(destination)); CopyTo(destination.BaseStream, dataBuffer); } /// /// Copies the data, starting from , to /// /// Destination /// Number of bytes written public void CopyTo(Stream destination) { if (destination is null) ThrowArgumentNullException(nameof(destination)); if (Position >= Length) return; CopyTo(destination, AllocTempBuffer()); } /// /// Copies the data, starting from , to /// /// Destination /// Temp buffer during writing /// Number of bytes written public void CopyTo(Stream destination, byte[] dataBuffer) { if (destination is null) ThrowArgumentNullException(nameof(destination)); if (dataBuffer is null) ThrowArgumentNullException(nameof(dataBuffer)); if (Position >= Length) return; if (dataBuffer.Length == 0) ThrowInvalidArgument(nameof(dataBuffer)); uint lenLeft = BytesLeft; while (lenLeft > 0) { int num = (int)Math.Min((uint)dataBuffer.Length, lenLeft); lenLeft -= (uint)num; ReadBytes(dataBuffer, 0, num); destination.Write(dataBuffer, 0, num); } } } }