From b60184aad27ee7f4f6f45d8239cb226adb0c4f4d Mon Sep 17 00:00:00 2001 From: VNC Date: Sun, 12 Jan 2025 11:32:38 +0100 Subject: [PATCH 1/5] Refactor reader classes and improve error handling --- .../BinaryEndianConverter.cs | 16 +- .../Interfaces/IBinaryModelMetadata.cs | 8 +- src/Snowberry.IO.Common/Reader/Analyzer.cs | 3 +- .../Reader/Interfaces/IEndianReader.Span.cs | 6 + .../Reader/Interfaces/IEndianReader.cs | 32 +- src/Snowberry.IO.Common/Reader/RegionRange.cs | 2 - .../BinaryModelGenerator.Generator.cs | 4 +- .../BinaryModelGenerator.cs | 5 +- src/Snowberry.IO.Tests/ReaderTests.cs | 16 +- src/Snowberry.IO.Tests/SharedTests.cs | 16 +- .../Reader/BaseEndianReader.Primitives.cs | 24 +- .../Reader/BaseEndianReader.Read.cs | 368 ++++++++++++++++++ .../Reader/BaseEndianReader.Span.cs | 29 +- src/Snowberry.IO/Reader/BaseEndianReader.cs | 360 +---------------- .../Reader/EndianStreamReader.Span.cs | 22 ++ src/Snowberry.IO/Reader/EndianStreamReader.cs | 12 +- .../Reader/Win32ProcessReader.Span.cs | 4 +- src/Snowberry.IO/Reader/Win32ProcessReader.cs | 65 +++- 18 files changed, 565 insertions(+), 427 deletions(-) create mode 100644 src/Snowberry.IO/Reader/BaseEndianReader.Read.cs diff --git a/src/Snowberry.IO.Common/BinaryEndianConverter.cs b/src/Snowberry.IO.Common/BinaryEndianConverter.cs index 8a99b63..586e745 100644 --- a/src/Snowberry.IO.Common/BinaryEndianConverter.cs +++ b/src/Snowberry.IO.Common/BinaryEndianConverter.cs @@ -12,7 +12,7 @@ public static partial class BinaryEndianConverter #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe int ToInt32(Span data, EndianType endian) { @@ -25,7 +25,7 @@ public static unsafe int ToInt32(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe uint ToUInt32(Span data, EndianType endian) { @@ -38,7 +38,7 @@ public static unsafe uint ToUInt32(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe ushort ToUInt16(Span data, EndianType endian) { @@ -51,7 +51,7 @@ public static unsafe ushort ToUInt16(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe short ToInt16(Span data, EndianType endian) { @@ -64,7 +64,7 @@ public static unsafe short ToInt16(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe float ToFloat(Span data, EndianType endian) { @@ -82,7 +82,7 @@ public static unsafe float ToFloat(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe double ToDouble(Span data, EndianType endian) { @@ -95,7 +95,7 @@ public static unsafe double ToDouble(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe long ToLong(Span data, EndianType endian) { @@ -109,7 +109,7 @@ public static unsafe long ToLong(Span data, EndianType endian) #if NETCOREAPP3_0_OR_GREATER [SkipLocalsInit] - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif public static unsafe ulong ToULong(Span data, EndianType endian) { diff --git a/src/Snowberry.IO.Common/Interfaces/IBinaryModelMetadata.cs b/src/Snowberry.IO.Common/Interfaces/IBinaryModelMetadata.cs index b5a7d46..8f9dc43 100644 --- a/src/Snowberry.IO.Common/Interfaces/IBinaryModelMetadata.cs +++ b/src/Snowberry.IO.Common/Interfaces/IBinaryModelMetadata.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Snowberry.IO.Common.Interfaces; +namespace Snowberry.IO.Common.Interfaces; /// /// Used for metadata about a binary model. diff --git a/src/Snowberry.IO.Common/Reader/Analyzer.cs b/src/Snowberry.IO.Common/Reader/Analyzer.cs index c5f6c9c..71ddc1a 100644 --- a/src/Snowberry.IO.Common/Reader/Analyzer.cs +++ b/src/Snowberry.IO.Common/Reader/Analyzer.cs @@ -20,6 +20,5 @@ public abstract class Analyzer /// The current reader instance. /// The span of bytes to analyze. /// The amount of bytes that were read. - /// The offset in the for the new read data, otherwise . - public abstract void AnalyzeReadBytes(IEndianReader reader, Span buffer, int amount, long offset = -1); + public abstract void AnalyzeReadBytes(IEndianReader reader, Span buffer, int amount); } \ No newline at end of file diff --git a/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.Span.cs b/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.Span.cs index a675ccd..5b0f2f6 100644 --- a/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.Span.cs +++ b/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.Span.cs @@ -13,4 +13,10 @@ public partial interface IEndianReader /// of bytes requested if that many bytes are not currently available, or zero if the end of the stream is reached. /// int Read(Span buffer); + + /// + /// Reads bytes from the current stream and advances the position within the stream until the is filled. + /// + /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the current stream. + void ReadExactly(Span buffer); } diff --git a/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.cs b/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.cs index 6d316a5..89bc082 100644 --- a/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.cs +++ b/src/Snowberry.IO.Common/Reader/Interfaces/IEndianReader.cs @@ -26,13 +26,15 @@ public partial interface IEndianReader : IDisposable /// /// Reads a single byte from the current stream. /// - /// - /// This method does not check if the current is greater than the current . - /// Avoid using this method in loops as it can lead to infinite loops if the end of the stream is reached. - /// /// The byte read. byte ReadByte(); + /// + /// Reads a signed byte from the current stream. + /// + /// A signed byte read from the current stream. + sbyte ReadSByte(); + /// /// Reads a single byte from the current stream, returning -1 if the is greater than the . /// @@ -51,6 +53,14 @@ public partial interface IEndianReader : IDisposable /// int Read(byte[] buffer, int offset, int byteCount); + /// + /// Reads bytes from the current stream and advances the position within the stream until the specified number of bytes is read. + /// + /// The byte array to which the data will be read. When this method returns, the specified region of this array is replaced by the bytes read from the current stream. + /// The zero-based byte offset in the at which to begin storing the data read from the stream. + /// The exact number of bytes to read from the stream. + void ReadExactly(byte[] buffer, int offset, int byteCount); + /// /// Reads a specified number of bytes from the current stream into the internal buffer. /// @@ -66,14 +76,14 @@ public partial interface IEndianReader : IDisposable /// Reads a specified number of bytes from the current stream. /// /// The number of bytes to read. - /// A byte array containing the read bytes. + /// A byte array containing the read bytes. This might be less than the number of bytes requested if the end of the stream is reached. byte[] ReadBytes(int count); /// /// Reads a null-terminated string from the current stream. /// /// A zero-terminated string, or if the end of the stream is reached. - string? ReadCString(); + string ReadCString(); /// /// Reads a fixed-size null-terminated string from the current stream. @@ -85,13 +95,13 @@ public partial interface IEndianReader : IDisposable /// The size of the string to read. /// Automatically adjust the by the unread count of . /// The fixed-size string. - string? ReadSizedCString(int size, bool adjustPosition = true); + string ReadSizedCString(int size, bool adjustPosition = true); /// /// Reads a size-prefixed string from the current stream. /// /// The size-prefixed string, or if the end of the stream is reached. - string? ReadString(); + string ReadString(); /// /// Reads a line of characters from the current stream. @@ -123,14 +133,14 @@ public partial interface IEndianReader : IDisposable /// /// The endianness to use. /// The read 64-bit signed integer. - long ReadLong(EndianType endian = EndianType.LITTLE); + long ReadInt64(EndianType endian = EndianType.LITTLE); /// /// Reads a 64-bit unsigned integer from the current stream with the specified endianness. /// /// The endianness to use. /// The read 64-bit unsigned integer. - ulong ReadULong(EndianType endian = EndianType.LITTLE); + ulong ReadUInt64(EndianType endian = EndianType.LITTLE); /// /// Reads a 32-bit unsigned integer from the current stream with the specified endianness. @@ -268,9 +278,7 @@ public partial interface IEndianReader : IDisposable /// /// Gets the internal buffer of the reader. /// -#pragma warning disable CA1819 // Properties should not return arrays byte[] Buffer { get; } -#pragma warning restore CA1819 // Properties should not return arrays /// /// Gets a value indicating whether the reader is disposed. diff --git a/src/Snowberry.IO.Common/Reader/RegionRange.cs b/src/Snowberry.IO.Common/Reader/RegionRange.cs index 42e2f50..432e309 100644 --- a/src/Snowberry.IO.Common/Reader/RegionRange.cs +++ b/src/Snowberry.IO.Common/Reader/RegionRange.cs @@ -8,10 +8,8 @@ namespace Snowberry.IO.Common.Reader; /// public struct RegionRange : IEquatable { -#pragma warning disable CA1051 // Do not declare visible instance fields public long StartPosition; public long Size; -#pragma warning restore CA1051 // Do not declare visible instance fields /// /// Uses the to create a new . diff --git a/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.Generator.cs b/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.Generator.cs index 5aee796..ef2bfeb 100644 --- a/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.Generator.cs +++ b/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.Generator.cs @@ -237,8 +237,8 @@ string GetReadMethodName(ITypeSymbol? typeSymbol, SpecialType? specialType) SpecialType.System_UInt16 => nameof(IEndianReader.ReadUInt16), SpecialType.System_Int32 => nameof(IEndianReader.ReadInt32), SpecialType.System_UInt32 => nameof(IEndianReader.ReadUInt32), - SpecialType.System_Int64 => nameof(IEndianReader.ReadLong), - SpecialType.System_UInt64 => nameof(IEndianReader.ReadULong), + SpecialType.System_Int64 => nameof(IEndianReader.ReadInt64), + SpecialType.System_UInt64 => nameof(IEndianReader.ReadUInt64), SpecialType.System_Double => nameof(IEndianReader.ReadDouble), SpecialType.System_Single => nameof(IEndianReader.ReadFloat), _ => null diff --git a/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.cs b/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.cs index 71d2595..b760685 100644 --- a/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.cs +++ b/src/Snowberry.IO.SourceGenerator/BinaryModelGenerator.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; -using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using Snowberry.IO.Common; @@ -30,7 +27,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) namespace {{c_CustomNamespace}} { - // Generated at: {{DateTime.UtcNow.ToString("yyyy-MM-dd")}} + // Generated at: {{DateTime.UtcNow:yyyy-MM-dd}} /// /// Enables the generation of the binary model. diff --git a/src/Snowberry.IO.Tests/ReaderTests.cs b/src/Snowberry.IO.Tests/ReaderTests.cs index a75fb38..57a33e5 100644 --- a/src/Snowberry.IO.Tests/ReaderTests.cs +++ b/src/Snowberry.IO.Tests/ReaderTests.cs @@ -9,7 +9,7 @@ namespace Snowberry.IO.Tests; public class ReaderTests { - public static Random Random = new(); + public static readonly Random Random = new(); [Theory] [InlineData(0, BaseEndianReader.MinBufferSize)] @@ -141,4 +141,18 @@ private void ReadWithOffsets() offset += 8; Assert.Equal(hash, reader.ReadSha1At(offset)); } + + [Fact] + private void ReadUntilEndOfStreamException() + { + var memory = new MemoryStream( + [ + 33, 92, 82, 33 + ]); + + using var reader = new EndianStreamReader(memory); + byte[] data = reader.ReadUntilEnd(); + + Assert.Throws(() => reader.ReadInt16()); + } } diff --git a/src/Snowberry.IO.Tests/SharedTests.cs b/src/Snowberry.IO.Tests/SharedTests.cs index f7edac4..0944184 100644 --- a/src/Snowberry.IO.Tests/SharedTests.cs +++ b/src/Snowberry.IO.Tests/SharedTests.cs @@ -50,7 +50,7 @@ private void ReadWrite_SizedCString(Encoding encoding, string expected, bool isU }, (x, _) => { - string? read = x.ReadSizedCString(actualStringPayloadSize); + string read = x.ReadSizedCString(actualStringPayloadSize); read = x.ReadSizedCString(actualStringPayloadSize); Assert.NotNull(read); @@ -71,7 +71,7 @@ private void ReadWrite_CString(string expected) }, (x, _) => { - string? read = x.ReadCString(); + string read = x.ReadCString(); Assert.NotNull(read); Assert.Equal(expected, read); @@ -91,7 +91,7 @@ private void ReadWrite_SizePrefixedString(string expected) }, (x, _) => { - string? read = x.ReadString(); + string read = x.ReadString(); Assert.NotNull(read); Assert.Equal(expected, read); @@ -106,10 +106,12 @@ private void ReadWrite_NewLine(string lineText) CreateShared((x, _) => { x.WriteLine(lineText); + x.WriteLine(lineText); }, (x, _) => { Assert.Equal(lineText, x.ReadLine()); + Assert.Equal(lineText, x.ReadLine()); }); } @@ -331,8 +333,8 @@ private void ReadWrite_Int64_LE_BE(long value) }, (x, _) => { - Assert.Equal(value, x.ReadLong()); - Assert.Equal(value, x.ReadLong(EndianType.BIG)); + Assert.Equal(value, x.ReadInt64()); + Assert.Equal(value, x.ReadInt64(EndianType.BIG)); int bufferSize = offset + (sizeof(long) * 2); x.EnsureBufferSize(bufferSize); @@ -363,8 +365,8 @@ private void ReadWrite_UInt64_LE_BE(ulong value) }, (x, _) => { - Assert.Equal(value, x.ReadULong()); - Assert.Equal(value, x.ReadULong(EndianType.BIG)); + Assert.Equal(value, x.ReadUInt64()); + Assert.Equal(value, x.ReadUInt64(EndianType.BIG)); int bufferSize = offset + (sizeof(ulong) * 2); x.EnsureBufferSize(bufferSize); diff --git a/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs b/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs index 1de9e45..daa3532 100644 --- a/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs +++ b/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs @@ -8,23 +8,23 @@ public partial class BaseEndianReader public Sha1 ReadSha1() { Span bytes = stackalloc byte[Sha1.StructSize]; - Read(bytes); + ReadExactly(bytes); return new(bytes); } /// - public long ReadLong(EndianType endian = EndianType.LITTLE) + public long ReadInt64(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[8]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToLong(bytes, endian); } /// - public ulong ReadULong(EndianType endian = EndianType.LITTLE) + public ulong ReadUInt64(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[8]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToULong(bytes, endian); } @@ -32,7 +32,7 @@ public ulong ReadULong(EndianType endian = EndianType.LITTLE) public uint ReadUInt32(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[4]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToUInt32(bytes, endian); } @@ -40,7 +40,7 @@ public uint ReadUInt32(EndianType endian = EndianType.LITTLE) public ushort ReadUInt16(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[2]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToUInt16(bytes, endian); } @@ -48,7 +48,7 @@ public ushort ReadUInt16(EndianType endian = EndianType.LITTLE) public int ReadInt32(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[4]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToInt32(bytes, endian); } @@ -56,7 +56,7 @@ public int ReadInt32(EndianType endian = EndianType.LITTLE) public short ReadInt16(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[2]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToInt16(bytes, endian); } @@ -64,7 +64,7 @@ public short ReadInt16(EndianType endian = EndianType.LITTLE) public Guid ReadGuid(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[16]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToGuid(bytes, 0, endian); } @@ -72,7 +72,7 @@ public Guid ReadGuid(EndianType endian = EndianType.LITTLE) public float ReadFloat(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[4]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToFloat(bytes, endian); } @@ -80,7 +80,7 @@ public float ReadFloat(EndianType endian = EndianType.LITTLE) public unsafe double ReadDouble(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[8]; - Read(bytes); + ReadExactly(bytes); return BinaryEndianConverter.ToDouble(bytes, endian); } } diff --git a/src/Snowberry.IO/Reader/BaseEndianReader.Read.cs b/src/Snowberry.IO/Reader/BaseEndianReader.Read.cs new file mode 100644 index 0000000..97d9f27 --- /dev/null +++ b/src/Snowberry.IO/Reader/BaseEndianReader.Read.cs @@ -0,0 +1,368 @@ +using System.Runtime.CompilerServices; +using System.Text; +using Snowberry.IO.Common; + +namespace Snowberry.IO.Reader; + +public partial class BaseEndianReader +{ + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public byte ReadByte() + { + ThrowIfDisposed(); + + if (ReadInInternalBuffer(1, 0) == 0) + ThrowEndOfStreamException(); + + return _buffer[0]; + } + + /// + public sbyte ReadSByte() + { + return (sbyte)ReadByte(); + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int ReadByteSafe() + { + int read = ReadInInternalBuffer(1, 0); + + if (read == 0) + return -1; + + return _buffer[0]; + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int Read(byte[] buffer, int offset, int byteCount) + { + return Read(buffer.AsSpan()[offset..byteCount]); + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public void ReadExactly(byte[] buffer, int offset, int byteCount) + { + ReadExactly(buffer.AsSpan()[offset..byteCount]); + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public int ReadInInternalBuffer(int byteCount, int offset) + { + return Read(Buffer, offset, byteCount); + } + + /// + public byte[] ReadBytes(int count) + { + if (count == 0) + return Array.Empty(); + + byte[] result = new byte[count]; + + int numRead = 0; + + do + { + int read = Read(result, numRead, count); + + if (read == 0) + break; + + numRead += read; + count -= read; + } while (count > 0); + + return result; + } + + /// + public string ReadString() + { + // NOTE(VNC): + // + // Implementation is based on the original BinaryReader BCL type. + // + // https://source.dot.net/#System.Private.CoreLib/BinaryReader.cs + // + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + // Length of the string in bytes, not chars. + int stringLength = Read7BitEncodedInt(); + + if (stringLength < 0) + throw new IOException("String length can't be less than 0."); + + if (stringLength == 0) + return string.Empty; + + int position = 0; + int n; + int readLength; + + var sb = new StringBuilder(); + + Span charBytes = stackalloc byte[MaxCharBytesSize]; + _charBuffer ??= new char[_maxCharsSize]; + + do + { + readLength = ((stringLength - position) > MaxCharBytesSize) ? MaxCharBytesSize : (stringLength - position); + + n = Read(charBytes[..readLength]); + + if (n == 0) + ThrowEndOfStreamException(); + + if (position == 0 && n == stringLength) +#if NETSTANDARD2_0 + return _encoding.GetString(charBytes[..n].ToArray()); +#else + return _encoding.GetString(charBytes[..n]); +#endif + +#if NETSTANDARD2_0 + int charsRead = _decoder.GetChars(charBytes[..n].ToArray(), 0, n, _charBuffer, 0); +#else + int charsRead = _decoder.GetChars(charBytes[..n], _charBuffer, flush: false); +#endif + + sb.Append(_charBuffer, 0, charsRead); + position += n; + + } while (position < stringLength); + + return sb.ToString(); + } + + /// + public string? ReadLine() + { + if (!CanReadData) + return null; + + _stringBuilder.Clear(); + + { + // NOTE(VNC): Character value... + int value = 0; + + var decoder = _encoding.GetDecoder(); + Span singleByteSpan = stackalloc byte[1]; + Span decodedCharSpan = stackalloc char[_maxCharsSize]; + + //if (!_encoding.IsSingleByte) + //decoder = _encoding.GetDecoder(); + + while (value is not '\r' and not '\n') + { + value = ReadByteSafe(); + + if (value == -1) + break; + + if (value == '\r' || value == '\n' || !CanReadData) + break; + + singleByteSpan[0] = (byte)value; + AppendCharacters(decoder, singleByteSpan, decodedCharSpan, out _); + } + + // CR LF + if (value == '\r' && CanReadData) + ReadByte(); + } + + return _stringBuilder.ToString(); + } + + /// + public virtual string ReadCString() + { + if (!CanReadData) + ThrowEndOfStreamException(); + + _stringBuilder.Clear(); + + { + var decoder = _encoding.GetDecoder(); + Span singleByteSpan = stackalloc byte[1]; + Span decodedCharSpan = stackalloc char[_maxCharsSize]; + + while (true) + { + int value = ReadByteSafe(); + + if (value is -1 or 0) + break; + + singleByteSpan[0] = (byte)value; + AppendCharacters(decoder, singleByteSpan, decodedCharSpan, out _); + } + } + + return _stringBuilder.ToString(); + } + + /// + public string ReadSizedCString(int size, bool adjustPosition = true) + { + _stringBuilder.Clear(); + + var decoder = _encoding.GetDecoder(); + + Span decodedCharSpan = stackalloc char[_maxCharsSize]; + Span charBytes = stackalloc byte[MaxCharBytesSize]; + + int readLength; + int n; + int startSize = size; + + do + { + + readLength = size > MaxCharBytesSize ? MaxCharBytesSize : size; + n = Read(charBytes[..readLength]); + + if (n == 0) + ThrowEndOfStreamException(); + + size -= n; + + int endIndex = charBytes.IndexOf(0); + AppendCharacters(decoder, endIndex > 0 ? charBytes[..endIndex] : charBytes, decodedCharSpan, out int decodedCharCount); + + if (size > 0 && endIndex != -1) + { + if (adjustPosition) + Position += size; + + break; + } + + } while (size > 0); + + return _stringBuilder.ToString(); + } + + private void AppendCharacters(Decoder decoder, Span bytes, Span chars, out int decodedCharCount) + { + _ = decoder ?? throw new ArgumentNullException(nameof(decoder)); + +#if NETSTANDARD2_0 + char[] charBuffer = new char[chars.Length]; + decodedCharCount = decoder.GetChars(bytes.ToArray(), 0, bytes.Length, charBuffer, 0); + + if (decodedCharCount != 0) + _stringBuilder.Append(charBuffer.AsSpan()[..decodedCharCount].ToArray()); +#else + decodedCharCount = decoder.GetChars(bytes, chars, false); + + if (decodedCharCount != 0) + _stringBuilder.Append(chars[..decodedCharCount]); +#endif + } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public void ReadAlignment(byte alignment) + { + long position = Position; + BinaryUtils.ApplyAlignment(ref position, alignment); + Position = position; + } + + /// + public byte[] ReadUntilEnd(int maxBufferSize = 16 * 1024) + { + byte[] buffer = new byte[maxBufferSize]; + + using var ms = new MemoryStream(); + + int read; + while ((read = Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + + return ms.ToArray(); + } + + /// + public bool ReadBool() + { + return ReadByte() > 0; + } + + /// + public virtual long Read7BitEncodedLong() + { + long output = 0; + int shiftVariable = 0; + + while (true) + { + int b = ReadByte(); + + output |= (long)(b & 0x7F) << shiftVariable; + + if (b >> 0x7 == 0) + break; + + shiftVariable += 0x7; + } + + return output; + } + + /// + public virtual int Read7BitEncodedInt() + { + int output = 0; + int shiftVariable = 0; + + while (true) + { + int b = ReadByte(); + + output |= (b & 0x7F) << shiftVariable; + + if (b >> 0x7 == 0) + break; + + shiftVariable += 0x7; + } + + return output; + } +} diff --git a/src/Snowberry.IO/Reader/BaseEndianReader.Span.cs b/src/Snowberry.IO/Reader/BaseEndianReader.Span.cs index 755d6c9..22b7fc3 100644 --- a/src/Snowberry.IO/Reader/BaseEndianReader.Span.cs +++ b/src/Snowberry.IO/Reader/BaseEndianReader.Span.cs @@ -4,19 +4,10 @@ namespace Snowberry.IO.Reader; public partial class BaseEndianReader { - /// - /// Reads a specified number of bytes from the current stream into a buffer. - /// - /// The span of bytes to store the read bytes. - /// - /// The total number of bytes read into the buffer. This can be less than the number - /// of bytes requested if that many bytes are not currently available, or zero if the end of the stream is reached. - /// - /// - /// This method is intended to be implemented by derived classes to provide the actual logic for reading bytes. - /// protected abstract int InternalReadBytes(Span inBuffer); + protected abstract void InternalReadBytesExactly(Span inBuffer); + /// #if NETCOREAPP3_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveOptimization)] @@ -28,7 +19,21 @@ public int Read(Span buffer) if (IsRegionViewEnabled) _viewOffset += read; - Analyzer?.AnalyzeReadBytes(this, buffer, read, 0); + Analyzer?.AnalyzeReadBytes(this, buffer, read); return read; } + + /// +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveOptimization)] +#endif + public void ReadExactly(Span buffer) + { + InternalReadBytesExactly(buffer); + + if (IsRegionViewEnabled) + _viewOffset += buffer.Length; + + Analyzer?.AnalyzeReadBytes(this, buffer, buffer.Length); + } } diff --git a/src/Snowberry.IO/Reader/BaseEndianReader.cs b/src/Snowberry.IO/Reader/BaseEndianReader.cs index aae79a6..ffc9d9b 100644 --- a/src/Snowberry.IO/Reader/BaseEndianReader.cs +++ b/src/Snowberry.IO/Reader/BaseEndianReader.cs @@ -73,6 +73,13 @@ protected BaseEndianReader(Analyzer? analyzer, int bufferSize, Encoding encoding _2BytesPerChar = encoding is UnicodeEncoding; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ThrowEndOfStreamException() + { + throw new EndOfStreamException(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void ThrowIfDisposed() { if (Disposed) @@ -100,356 +107,11 @@ protected void ThrowIfDisposed() /// public abstract void CopyTo(Stream destination, int length, int bufferSize = 0x14000); - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public byte ReadByte() - { - ReadInInternalBuffer(1, 0); - return _buffer[0]; - } - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public int ReadByteSafe() - { - int read = ReadInInternalBuffer(1, 0); - - if (read == 0) - return -1; - - return _buffer[0]; - } - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public int Read(byte[] buffer, int offset, int byteCount) - { - return Read(buffer.AsSpan()[offset..byteCount]); - } - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public int ReadInInternalBuffer(int byteCount, int offset) - { - return Read(Buffer, offset, byteCount); - } - - /// - public byte[] ReadBytes(int count) - { - if (count == 0) - return Array.Empty(); - - byte[] result = new byte[count]; - - int numRead = 0; - - do - { - int read = Read(result, numRead, count); - - if (read == 0) - break; - - numRead += read; - count -= read; - } while (count > 0); - - return result; - } - - /// - public virtual string? ReadCString() - { - if (!CanReadData) - return null; - - _stringBuilder.Clear(); - - { - var decoder = _encoding.GetDecoder(); - Span singleByteSpan = stackalloc byte[1]; - Span decodedCharSpan = stackalloc char[_maxCharsSize]; - - while (true) - { - int value = ReadByteSafe(); - - if (value is -1 or 0) - break; - - singleByteSpan[0] = (byte)value; - AppendCharacters(decoder, singleByteSpan, decodedCharSpan, out _); - } - } - - return _stringBuilder.ToString(); - } - - /// - public string? ReadSizedCString(int size, bool adjustPosition = true) - { - if (!CanReadData) - return null; - - _stringBuilder.Clear(); - - var decoder = _encoding.GetDecoder(); - - Span decodedCharSpan = stackalloc char[_maxCharsSize]; - Span charBytes = stackalloc byte[MaxCharBytesSize]; - - int readLength; - int n; - int startSize = size; - - do - { - - readLength = size > MaxCharBytesSize ? MaxCharBytesSize : size; - n = Read(charBytes[..readLength]); - - if (n == 0) - throw new EndOfStreamException(); - - size -= n; - - int endIndex = charBytes.IndexOf(0); - AppendCharacters(decoder, endIndex > 0 ? charBytes[..endIndex] : charBytes, decodedCharSpan, out int decodedCharCount); - - if (size > 0 && endIndex != -1) - { - if (adjustPosition) - Position += size; - - break; - } - - } while (size > 0); - - return _stringBuilder.ToString(); - } - - /// - public string? ReadString() - { - if (!CanReadData) - return null; - - // NOTE(VNC): - // - // Implementation is based on the original BinaryReader BCL type. - // - // https://source.dot.net/#System.Private.CoreLib/BinaryReader.cs - // - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. - - // Length of the string in bytes, not chars. - int stringLength = Read7BitEncodedInt(); - - if (stringLength < 0) - throw new IOException("String length can't be less than 0."); - - if (stringLength == 0) - return string.Empty; - - int position = 0; - int n; - int readLength; - - var sb = new StringBuilder(); - - Span charBytes = stackalloc byte[MaxCharBytesSize]; - _charBuffer ??= new char[_maxCharsSize]; - - do - { - readLength = ((stringLength - position) > MaxCharBytesSize) ? MaxCharBytesSize : (stringLength - position); - - n = Read(charBytes[..readLength]); - - if (n == 0) - throw new EndOfStreamException(); - - if (position == 0 && n == stringLength) -#if NETSTANDARD2_0 - return _encoding.GetString(charBytes[..n].ToArray()); -#else - return _encoding.GetString(charBytes[..n]); -#endif - -#if NETSTANDARD2_0 - int charsRead = _decoder.GetChars(charBytes[..n].ToArray(), 0, n, _charBuffer, 0); -#else - int charsRead = _decoder.GetChars(charBytes[..n], _charBuffer, flush: false); -#endif - - sb.Append(_charBuffer, 0, charsRead); - position += n; - - } while (position < stringLength); - - return sb.ToString(); - } - - /// - public string? ReadLine() - { - if (!CanReadData) - return null; - - _stringBuilder.Clear(); - - // 13 = '\r' - // 10 = '\n' - { - // NOTE(VNC): Character value... - int value = 0; - - var decoder = _encoding.GetDecoder(); - Span singleByteSpan = stackalloc byte[1]; - Span decodedCharSpan = stackalloc char[_maxCharsSize]; - - //if (!_encoding.IsSingleByte) - //decoder = _encoding.GetDecoder(); - - while (value is not '\r' and not '\n') - { - value = ReadByteSafe(); - - if (value == -1) - break; - - if (value == '\r' || value == '\n' || !CanReadData) - break; - - singleByteSpan[0] = (byte)value; - AppendCharacters(decoder, singleByteSpan, decodedCharSpan, out _); - } - - // CR LF - if (value == '\r' && CanReadData) - ReadByte(); - } - - return _stringBuilder.ToString(); - } - - private void AppendCharacters(Decoder decoder, Span bytes, Span chars, out int decodedCharCount) - { - _ = decoder ?? throw new ArgumentNullException(nameof(decoder)); - -#if NETSTANDARD2_0 - char[] charBuffer = new char[chars.Length]; - decodedCharCount = decoder.GetChars(bytes.ToArray(), 0, bytes.Length, charBuffer, 0); - - if (decodedCharCount != 0) - _stringBuilder.Append(charBuffer.AsSpan()[..decodedCharCount].ToArray()); -#else - decodedCharCount = decoder.GetChars(bytes, chars, false); - - if (decodedCharCount != 0) - _stringBuilder.Append(chars[..decodedCharCount]); -#endif - } - - /// -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - public void ReadAlignment(byte alignment) - { - long position = Position; - BinaryUtils.ApplyAlignment(ref position, alignment); - Position = position; - } - - /// - public byte[] ReadUntilEnd(int maxBufferSize = 16 * 1024) - { - byte[] buffer = new byte[maxBufferSize]; - - using var ms = new MemoryStream(); - - int read; - while ((read = Read(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - - return ms.ToArray(); - } - - /// - public bool ReadBool() - { - return ReadByte() > 0; - } - - /// - public virtual long Read7BitEncodedLong() - { - long output = 0; - int shiftVariable = 0; - - while (true) - { - int b = ReadByte(); - - output |= (long)(b & 0x7F) << shiftVariable; - - if (b >> 0x7 == 0) - break; - - shiftVariable += 0x7; - } - - return output; - } - - /// - public virtual int Read7BitEncodedInt() - { - int output = 0; - int shiftVariable = 0; - - while (true) - { - int b = ReadByte(); - - output |= (b & 0x7F) << shiftVariable; - - if (b >> 0x7 == 0) - break; - - shiftVariable += 0x7; - } - - return output; - } - /// public void EnsureBufferSize(int bufferSize) { + ThrowIfDisposed(); + if (Buffer == null || Buffer.Length < bufferSize) Buffer = new byte[bufferSize]; } @@ -457,6 +119,8 @@ public void EnsureBufferSize(int bufferSize) /// public virtual void EnableRegionView(RegionRange region) { + ThrowIfDisposed(); + RegionView = region; _isRegionViewEnabled = true; @@ -466,6 +130,8 @@ public virtual void EnableRegionView(RegionRange region) /// public virtual void DisableRegionView() { + ThrowIfDisposed(); + _isRegionViewEnabled = false; _viewOffset = 0; } diff --git a/src/Snowberry.IO/Reader/EndianStreamReader.Span.cs b/src/Snowberry.IO/Reader/EndianStreamReader.Span.cs index 28dcca2..d2f309c 100644 --- a/src/Snowberry.IO/Reader/EndianStreamReader.Span.cs +++ b/src/Snowberry.IO/Reader/EndianStreamReader.Span.cs @@ -5,6 +5,8 @@ public partial class EndianStreamReader /// protected override int InternalReadBytes(Span inBuffer) { + ThrowIfDisposed(); + _ = Stream ?? throw new NullReferenceException(nameof(Stream)); // NOTE(VNC): Important because of region views. @@ -19,6 +21,26 @@ protected override int InternalReadBytes(Span inBuffer) #else return Stream.Read(inBuffer); #endif + } + + /// + protected override void InternalReadBytesExactly(Span inBuffer) + { + ThrowIfDisposed(); + + _ = Stream ?? throw new NullReferenceException(nameof(Stream)); + // NOTE(VNC): Important because of region views. + if (!CanReadData) + ThrowEndOfStreamException(); + +#if NET7_0_OR_GREATER + Stream.ReadExactly(inBuffer); +#else + int read = InternalReadBytes(inBuffer); + + if (read != inBuffer.Length) + ThrowEndOfStreamException(); +#endif } } diff --git a/src/Snowberry.IO/Reader/EndianStreamReader.cs b/src/Snowberry.IO/Reader/EndianStreamReader.cs index 753f7c9..d601291 100644 --- a/src/Snowberry.IO/Reader/EndianStreamReader.cs +++ b/src/Snowberry.IO/Reader/EndianStreamReader.cs @@ -57,7 +57,7 @@ public override void CopyTo(Stream destination, int length, int bufferSize = 0x1 if (IsRegionViewEnabled) _viewOffset += read; - Analyzer?.AnalyzeReadBytes(this, buffer.AsSpan(), read, 0); + Analyzer?.AnalyzeReadBytes(this, buffer.AsSpan(), read); destination.Write(buffer, 0, read); length -= read; @@ -105,6 +105,8 @@ public override long Position { get { + ThrowIfDisposed(); + _ = Stream ?? throw new NullReferenceException(nameof(Stream)); return GetViewOrPosition(Stream.Position); @@ -112,6 +114,8 @@ public override long Position set { + ThrowIfDisposed(); + _ = Stream ?? throw new NullReferenceException(nameof(Stream)); long position = Stream.Position; @@ -125,7 +129,7 @@ public override bool CanReadData { get { - if (Stream == null) + if (Stream == null || Disposed) return false; return Length > Position; @@ -147,6 +151,8 @@ public override long ActualPosition { get { + ThrowIfDisposed(); + _ = Stream ?? throw new NullReferenceException(nameof(Stream)); return Stream.Position; } @@ -157,6 +163,8 @@ public override long ActualLength { get { + ThrowIfDisposed(); + _ = Stream ?? throw new NullReferenceException(nameof(Stream)); return Stream.Length; } diff --git a/src/Snowberry.IO/Reader/Win32ProcessReader.Span.cs b/src/Snowberry.IO/Reader/Win32ProcessReader.Span.cs index e2b863b..bc0c2c6 100644 --- a/src/Snowberry.IO/Reader/Win32ProcessReader.Span.cs +++ b/src/Snowberry.IO/Reader/Win32ProcessReader.Span.cs @@ -7,6 +7,8 @@ public partial class Win32ProcessReader /// protected override int InternalReadBytes(Span inBuffer) { + ThrowIfDisposed(); + uint lpflOldProtect = 0u; int read = 0; @@ -28,7 +30,7 @@ protected override int InternalReadBytes(Span inBuffer) tempBuffer.CopyTo(inBuffer); _position += byteCount; - Analyzer?.AnalyzeReadBytes(this, inBuffer, byteCount, 0); + Analyzer?.AnalyzeReadBytes(this, inBuffer, byteCount); return byteCount; } diff --git a/src/Snowberry.IO/Reader/Win32ProcessReader.cs b/src/Snowberry.IO/Reader/Win32ProcessReader.cs index 257bdff..27cb9f0 100644 --- a/src/Snowberry.IO/Reader/Win32ProcessReader.cs +++ b/src/Snowberry.IO/Reader/Win32ProcessReader.cs @@ -1,6 +1,7 @@ using System.Runtime.Versioning; using System.Text; using Snowberry.IO.Common.Reader; +using Snowberry.IO.Common.Reader.Interfaces; using static Snowberry.IO.Utils.Win32Helper; namespace Snowberry.IO.Reader; @@ -48,6 +49,17 @@ protected override int InternalReadBytes(byte[] inBuffer, int offset, int byteCo return InternalReadBytes(inBuffer.AsSpan()[offset..byteCount]); } + /// + protected override void InternalReadBytesExactly(Span inBuffer) + { + ThrowIfDisposed(); + + int read = InternalReadBytes(inBuffer); + + if (read != inBuffer.Length) + ThrowEndOfStreamException(); + } + /// /// public override void CopyTo(Stream destination) @@ -106,7 +118,7 @@ public bool InjectDLL(string filePath) /// public override string ReadCString() { - long stringPointerPos = ReadLong(); + long stringPointerPos = ReadInt64(); long oldPosition = Position; Position = stringPointerPos; @@ -122,27 +134,64 @@ public override void Dispose() { GC.SuppressFinalize(this); + if (!CloseHandleOnDispose) + return; + _ = CloseHandle(_processHandle); } /// - /// Will always return . - public override long Length => 0; + public override bool CanReadData => _processHandle != IntPtr.Zero && !Disposed; /// - public override bool CanReadData => _processHandle != IntPtr.Zero; + public override long Position + { + get + { + ThrowIfDisposed(); + + return GetViewOrPosition(_position); + } + + set + { + ThrowIfDisposed(); + + long position = _position; + SetPosition(ref position, value); + _position = position; + } + } /// - public override long Position + /// Will always return if no region view is used. + public override long Length { - get => _position; - set => _position = value; + get + { + if (IsRegionViewEnabled) + return RegionView.Size; + + return 0; + } } /// - public override long ActualPosition => _position; + public override long ActualPosition + { + get + { + ThrowIfDisposed(); + return _position; + } + } /// /// Will always return . public override long ActualLength => 0; + + /// + /// Specifies whether the handle should be closed when the reader is disposed. + /// + public bool CloseHandleOnDispose { get; set; } = true; } From cb30cb820980ac52d062091ec324cbaa3bb65088 Mon Sep 17 00:00:00 2001 From: VNC Date: Sun, 12 Jan 2025 11:51:25 +0100 Subject: [PATCH 2/5] Move `Snowberry.IO.SingleFile` to this repository --- README.md | 6 +- src/NuGetPackage.props | 4 +- .../Conversions/Int64ConversionBenchmark.cs | 8 +- .../BinaryEndianConverter.Offsets.cs | 6 +- .../BinaryEndianConverter.cs | 6 +- src/Snowberry.IO.SingleFile.CLI/Options.cs | 12 + src/Snowberry.IO.SingleFile.CLI/Program.cs | 86 ++++ .../Properties/launchSettings.json | 8 + .../Snowberry.IO.SingleFile.CLI.csproj | 22 + .../BinarySearchHelper.cs | 74 ++++ src/Snowberry.IO.SingleFile/BundlerOptions.cs | 29 ++ .../Meta/BundleManifest.cs | 156 +++++++ src/Snowberry.IO.SingleFile/Meta/FileEntry.cs | 115 ++++++ .../Meta/FileLocation.cs | 44 ++ src/Snowberry.IO.SingleFile/Meta/FileType.cs | 39 ++ .../Meta/ModifiedFileEntryMeta.cs | 27 ++ src/Snowberry.IO.SingleFile/README.md | 41 ++ .../SingleFileBinaryData.cs | 386 ++++++++++++++++++ .../Snowberry.IO.SingleFile.csproj | 53 +++ src/Snowberry.IO.SingleFile/TargetInfo.cs | 14 + src/Snowberry.IO.SingleFile/ViewStream.cs | 216 ++++++++++ src/Snowberry.IO.sln | 19 + .../Extensions/ReaderExtensions.cs | 4 +- .../Reader/BaseEndianReader.Primitives.cs | 4 +- 24 files changed, 1360 insertions(+), 19 deletions(-) create mode 100644 src/Snowberry.IO.SingleFile.CLI/Options.cs create mode 100644 src/Snowberry.IO.SingleFile.CLI/Program.cs create mode 100644 src/Snowberry.IO.SingleFile.CLI/Properties/launchSettings.json create mode 100644 src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj create mode 100644 src/Snowberry.IO.SingleFile/BinarySearchHelper.cs create mode 100644 src/Snowberry.IO.SingleFile/BundlerOptions.cs create mode 100644 src/Snowberry.IO.SingleFile/Meta/BundleManifest.cs create mode 100644 src/Snowberry.IO.SingleFile/Meta/FileEntry.cs create mode 100644 src/Snowberry.IO.SingleFile/Meta/FileLocation.cs create mode 100644 src/Snowberry.IO.SingleFile/Meta/FileType.cs create mode 100644 src/Snowberry.IO.SingleFile/Meta/ModifiedFileEntryMeta.cs create mode 100644 src/Snowberry.IO.SingleFile/README.md create mode 100644 src/Snowberry.IO.SingleFile/SingleFileBinaryData.cs create mode 100644 src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj create mode 100644 src/Snowberry.IO.SingleFile/TargetInfo.cs create mode 100644 src/Snowberry.IO.SingleFile/ViewStream.cs diff --git a/README.md b/README.md index 68a34f6..4ad02b9 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ writer.BaseStream.Position = 0; using var reader = new EndianStreamReader(stream); _ = reader.ReadInt32(EndianType.BIG); -_ = reader.ReadLong(EndianType.BIG); +_ = reader.ReadInt64(EndianType.BIG); _ = reader.ReadFloat(EndianType.BIG); _ = reader.ReadUInt32(EndianType.BIG); _ = reader.ReadDouble(EndianType.BIG); @@ -79,8 +79,8 @@ var buffer = new byte[] { ... }; int offset = ...; var endianType = EndianType.BIG; -_ = BinaryEndianConverter.ToLong(buffer, endianType); -_ = BinaryEndianConverter.ToLong(buffer, offset, endianType); +_ = BinaryEndianConverter.ToInt64(buffer, endianType); +_ = BinaryEndianConverter.ToInt64(buffer, offset, endianType); ``` ## Supported data types diff --git a/src/NuGetPackage.props b/src/NuGetPackage.props index 262d2b6..ca67284 100644 --- a/src/NuGetPackage.props +++ b/src/NuGetPackage.props @@ -4,7 +4,7 @@ AnyCPU;x64;x86 Snowberry Software - 2.1.0.0 + 3.0.0.0 $(AssemblyVersion) true @@ -14,7 +14,7 @@ true true - Copyright © 2024 Snowberry Software + Copyright © 2025 Snowberry Software True https://github.com/snowberry-software/Snowberry.IO diff --git a/src/Snowberry.IO.Benchmarks/Conversions/Int64ConversionBenchmark.cs b/src/Snowberry.IO.Benchmarks/Conversions/Int64ConversionBenchmark.cs index ae9c2c5..a057e72 100644 --- a/src/Snowberry.IO.Benchmarks/Conversions/Int64ConversionBenchmark.cs +++ b/src/Snowberry.IO.Benchmarks/Conversions/Int64ConversionBenchmark.cs @@ -21,7 +21,7 @@ public long BitConverter_Int64() [Benchmark] public unsafe long BinaryEndianConverter_Int64() { - return BinaryEndianConverter.ToLong(_data.AsSpan(), EndianType.LITTLE); + return BinaryEndianConverter.ToInt64(_data.AsSpan(), EndianType.LITTLE); } [Benchmark] @@ -33,7 +33,7 @@ public long BitConverter_Offset_Int64() [Benchmark] public unsafe long BinaryEndianConverter_Offset_Int64() { - return BinaryEndianConverter.ToLong(_data.AsSpan(), 4, EndianType.LITTLE); + return BinaryEndianConverter.ToInt64(_data.AsSpan(), 4, EndianType.LITTLE); } [Benchmark] @@ -47,7 +47,7 @@ public long BitConverter_BigE_Int64() [Benchmark] public unsafe long BinaryEndianConverter_BigE_Int64() { - return BinaryEndianConverter.ToLong(_data.AsSpan(), EndianType.BIG); + return BinaryEndianConverter.ToInt64(_data.AsSpan(), EndianType.BIG); } [Benchmark] @@ -61,6 +61,6 @@ public long BitConverter_Offset_BigE_Int64() [Benchmark] public unsafe long BinaryEndianConverter_Offset_BigE_Int64() { - return BinaryEndianConverter.ToLong(_data.AsSpan(), 4, EndianType.BIG); + return BinaryEndianConverter.ToInt64(_data.AsSpan(), 4, EndianType.BIG); } } diff --git a/src/Snowberry.IO.Common/BinaryEndianConverter.Offsets.cs b/src/Snowberry.IO.Common/BinaryEndianConverter.Offsets.cs index b2d71ca..66bf55c 100644 --- a/src/Snowberry.IO.Common/BinaryEndianConverter.Offsets.cs +++ b/src/Snowberry.IO.Common/BinaryEndianConverter.Offsets.cs @@ -9,7 +9,7 @@ public static partial class BinaryEndianConverter [SkipLocalsInit] [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - public static unsafe long ToLong(Span data, int offset, EndianType endian) + public static unsafe long ToInt64(Span data, int offset, EndianType endian) { if (endian == EndianType.LITTLE) return Unsafe.ReadUnaligned(ref data[offset]); @@ -23,7 +23,7 @@ public static unsafe long ToLong(Span data, int offset, EndianType endian) [SkipLocalsInit] [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - public static unsafe ulong ToULong(Span data, int offset, EndianType endian) + public static unsafe ulong ToUInt64(Span data, int offset, EndianType endian) { if (endian == EndianType.LITTLE) return Unsafe.ReadUnaligned(ref data[offset]); @@ -137,7 +137,7 @@ public static unsafe double ToDouble(Span data, int offset, EndianType end if (endian == EndianType.LITTLE) return Unsafe.ReadUnaligned(ref data[offset]); - long temp = ToLong(data, offset, EndianType.BIG); + long temp = ToInt64(data, offset, EndianType.BIG); return BitConverter.Int64BitsToDouble(temp); } diff --git a/src/Snowberry.IO.Common/BinaryEndianConverter.cs b/src/Snowberry.IO.Common/BinaryEndianConverter.cs index 586e745..26b8790 100644 --- a/src/Snowberry.IO.Common/BinaryEndianConverter.cs +++ b/src/Snowberry.IO.Common/BinaryEndianConverter.cs @@ -89,7 +89,7 @@ public static unsafe double ToDouble(Span data, EndianType endian) if (endian == EndianType.LITTLE) return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(data)); - long temp = ToLong(data, EndianType.BIG); + long temp = ToInt64(data, EndianType.BIG); return BitConverter.Int64BitsToDouble(temp); } @@ -97,7 +97,7 @@ public static unsafe double ToDouble(Span data, EndianType endian) [SkipLocalsInit] [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif - public static unsafe long ToLong(Span data, EndianType endian) + public static unsafe long ToInt64(Span data, EndianType endian) { if (endian == EndianType.LITTLE) return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(data)); @@ -111,7 +111,7 @@ public static unsafe long ToLong(Span data, EndianType endian) [SkipLocalsInit] [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] #endif - public static unsafe ulong ToULong(Span data, EndianType endian) + public static unsafe ulong ToUInt64(Span data, EndianType endian) { if (endian == EndianType.LITTLE) return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(data)); diff --git a/src/Snowberry.IO.SingleFile.CLI/Options.cs b/src/Snowberry.IO.SingleFile.CLI/Options.cs new file mode 100644 index 0000000..8b842ee --- /dev/null +++ b/src/Snowberry.IO.SingleFile.CLI/Options.cs @@ -0,0 +1,12 @@ +using CommandLine; + +namespace Snowberry.IO.SingleFile.CLI; + +internal class Options +{ + [Option('i', "input", Required = true, HelpText = "Sets the input single file path.")] + public string InputFilePath { get; set; } = string.Empty; + + [Option('o', "output", Required = false, HelpText = "Sets the output directory.")] + public string OutputDirectory { get; set; } = string.Empty; +} diff --git a/src/Snowberry.IO.SingleFile.CLI/Program.cs b/src/Snowberry.IO.SingleFile.CLI/Program.cs new file mode 100644 index 0000000..a7441e7 --- /dev/null +++ b/src/Snowberry.IO.SingleFile.CLI/Program.cs @@ -0,0 +1,86 @@ +using CommandLine; +using Serilog; +using Snowberry.IO.SingleFile; +using Snowberry.IO.SingleFile.CLI; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() +#if DEBUG + .WriteTo.Debug() +#endif + .CreateLogger(); + +Parser.Default.ParseArguments(args) +.WithParsed(o => +{ + if (!File.Exists(o.InputFilePath)) + { + Log.Fatal("Could not find input file path: {input}", o.InputFilePath); + Environment.Exit(-1); + } + + Log.Information("Processing: {path}", o.InputFilePath); + + bool useOutputDirectory = !string.IsNullOrWhiteSpace(o.OutputDirectory); + + try + { + using var singleFileBinaryData = SingleFileBinaryData.GetFromFile(o.InputFilePath); + + if (singleFileBinaryData == null) + { + Log.Fatal("Specified file is not a published single file!"); + Environment.Exit(-1); + } + + if (singleFileBinaryData.BundleManifest == null) + return; + + var bundleManifest = singleFileBinaryData.BundleManifest; + Log.Information($"{"Bundle ID:",-24} {{id}}", bundleManifest.BundleID); + Log.Information($"{"Bundle Major Version:",-24} {{ver}}", bundleManifest.BundleMajorVersion); + Log.Information($"{"Bundle Minor Version:",-24} {{ver}}", bundleManifest.BundleMinorVersion); + Log.Information($"{"Bundle Flags:",-24} {{flags}}", bundleManifest.Flags); + Log.Information($"{"Bundle File Count:",-24} {{count}}", bundleManifest.FileEntries.Count); + Log.Information(""); + + string? outputDirectory = useOutputDirectory ? o.OutputDirectory : null; + + if (useOutputDirectory && !Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory!); + + for (int i = 0; i < bundleManifest.FileEntries.Count; i++) + { + var fileEntry = bundleManifest.FileEntries[i]; + + Log.Information("File: {type}/{path}", fileEntry.FileType, fileEntry.RelativePath); + + if (useOutputDirectory) + { + using var fileEntryStream = singleFileBinaryData.GetStream(fileEntry); + + if (fileEntryStream == null) + { + Log.Fatal(" Could not open stream for file entry!"); + Environment.Exit(-1); + } + + string filePath = Path.Combine(outputDirectory!, fileEntry.RelativePath); + string? directory = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + fileEntryStream.CopyTo(fs); + } + } + } + catch (Exception e) + { + Log.Fatal(e, "Could not process file: {input}", o.InputFilePath); + } + + Log.Information("Done..."); +}); \ No newline at end of file diff --git a/src/Snowberry.IO.SingleFile.CLI/Properties/launchSettings.json b/src/Snowberry.IO.SingleFile.CLI/Properties/launchSettings.json new file mode 100644 index 0000000..78ef372 --- /dev/null +++ b/src/Snowberry.IO.SingleFile.CLI/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Snowberry.IO.SingleFile.CLI": { + "commandName": "Project", + "commandLineArgs": "" + } + } +} \ No newline at end of file diff --git a/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj b/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj new file mode 100644 index 0000000..570c554 --- /dev/null +++ b/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + enable + enable + 1.0.0.0 + + + + + + + + + + + + + + diff --git a/src/Snowberry.IO.SingleFile/BinarySearchHelper.cs b/src/Snowberry.IO.SingleFile/BinarySearchHelper.cs new file mode 100644 index 0000000..b6c1c08 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/BinarySearchHelper.cs @@ -0,0 +1,74 @@ +using System.IO.MemoryMappedFiles; + +namespace Snowberry.IO.SingleFile; + +public static class BinarySearchHelper +{ + public static unsafe int SearchInView(MemoryMappedViewAccessor accessor, byte[] toSearch) + { + var safeBuffer = accessor.SafeMemoryMappedViewHandle; + byte* buffer = (byte*)safeBuffer.DangerousGetHandle(); + return SearchInBuffer(buffer, (int)accessor.Capacity, toSearch); + } + + public static unsafe int SearchInBuffer(byte* buffer, int length, byte[] toSearch) + { + if (toSearch.Length > length) + return -1; + + int[] table = BuildTable(toSearch); + + int i = 0; + int j = 0; + + while (i + j < length) + { + if (toSearch[j] == buffer[i + j]) + { + j++; + + if (j == toSearch.Length) + { + return i; + } + } + else + { + i += j - table[j]; + j = Math.Max(0, table[j]); + } + } + + return -1; + } + + private static int[] BuildTable(byte[] toSearch) + { + int[] table = new int[toSearch.Length]; + int position = 2; + int candidate = 0; + + table[0] = -1; + table[1] = 0; + + while (position < toSearch.Length) + { + if (toSearch[position - 1] == toSearch[candidate]) + { + table[position++] = ++candidate; + continue; + } + + if (candidate > 0) + { + candidate = table[candidate]; + continue; + } + + table[position++] = 0; + } + + return table; + } +} + diff --git a/src/Snowberry.IO.SingleFile/BundlerOptions.cs b/src/Snowberry.IO.SingleFile/BundlerOptions.cs new file mode 100644 index 0000000..6834794 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/BundlerOptions.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Snowberry.IO.SingleFile; + +/// +/// The bundler options. +/// +public class BundlerOptions +{ + /// + /// Gets or sets a value indicating whether to use compression for the files. + /// + /// It depends on the bundle version whether compressing is supported. + [JsonPropertyName(nameof(UseCompression))] + public bool UseCompression { get; set; } = true; + + /// + /// Gets or sets the compression threshold. + /// + /// If the compressed size is smaller than the original size multiplied by this value, the compressed data will be used. + [JsonPropertyName(nameof(CompressionThreshold))] + public float CompressionThreshold { get; set; } = 0.75F; + + /// + /// Gets or sets a value indicating whether to force compression for the files even if the compressed size exceeds the compression threshold. + /// + [JsonPropertyName(nameof(ForceCompression))] + public bool ForceCompression { get; set; } +} \ No newline at end of file diff --git a/src/Snowberry.IO.SingleFile/Meta/BundleManifest.cs b/src/Snowberry.IO.SingleFile/Meta/BundleManifest.cs new file mode 100644 index 0000000..05e46c0 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/Meta/BundleManifest.cs @@ -0,0 +1,156 @@ +using Snowberry.IO.Common.Reader.Interfaces; +using Snowberry.IO.Common.Writer.Interfaces; + +namespace Snowberry.IO.SingleFile.Meta; + +/// +/// The bundle manifest that contains the file entry metadata collection. +/// +public class BundleManifest : IDisposable +{ + public const int BundleIdLength = 12; + + /// + /// Reads the data of the bundle manifest. + /// + /// The reader. + public void Read(IEndianReader reader) + { + FileEntries.Clear(); + + BundleMajorVersion = reader.ReadUInt32(); + BundleMinorVersion = reader.ReadUInt32(); + int fileCount = reader.ReadInt32(); + BundleID = reader.ReadString() ?? string.Empty; + + if (BundleMajorVersion >= 2) + { + DepsJsonLocation = FileLocation.Read(reader); + RuntimeConfigJsonLocation = FileLocation.Read(reader); + + Flags = (HeaderFlags)reader.ReadUInt64(); + } + + for (int i = 0; i < fileCount; i++) + { + var entry = new FileEntry(this); + entry.Read(reader); + + if ((DepsJsonFile == null || RuntimeConfigFile == null) && BundleMajorVersion >= 2) + { + if (entry.Location.Offset == DepsJsonLocation.Offset && entry.Location.Size == DepsJsonLocation.Size) + DepsJsonFile = entry; + + if (entry.Location.Offset == RuntimeConfigJsonLocation.Offset && entry.Location.Size == RuntimeConfigJsonLocation.Size) + RuntimeConfigFile = entry; + } + + FileEntries.Add(entry); + } + } + + /// + /// Writes the data of the bundle manifest. + /// + /// The writer. + /// Whether to include the file metadata. + /// The new bundle id. + /// The new *.deps.json file location. + /// The new *.runtimeconfig.json file location. + public void Write( + IEndianWriter writer, + bool includeFileMetadata, + string bundleId, + FileLocation depsLocation, + FileLocation runtimeLocation) + { + writer.Write(BundleMajorVersion); + writer.Write(BundleMinorVersion); + + writer.Write(FileEntries.Count); + writer.Write(bundleId); + + if (BundleMajorVersion >= 2) + { + depsLocation.Write(writer); + runtimeLocation.Write(writer); + + writer.Write((ulong)Flags); + } + + if (includeFileMetadata) + for (int i = 0; i < FileEntries.Count; i++) + { + var entry = FileEntries[i]; + entry.Write(writer); + } + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + + for (int i = 0; i < FileEntries.Count; i++) + { + var fileEntry = FileEntries[i]; + fileEntry.Dispose(); + } + } + + /// + /// The *.deps.json file location. + /// + /// Will only be read and is not a direct reference to the . Writing will not use this. + public FileLocation DepsJsonLocation { get; set; } + + /// + /// The *.runtimeconfig.json file location. + /// + /// Will only be read and is not a direct reference to the . Writing will not use this. + public FileLocation RuntimeConfigJsonLocation { get; set; } + + /// + /// The *.deps.json file. + /// + public FileEntry? DepsJsonFile { get; set; } + + /// + /// The *.runtimeconfig.json file. + /// + public FileEntry? RuntimeConfigFile { get; set; } + + /// + /// The bundle major version. + /// + public uint BundleMajorVersion { get; set; } + + /// + /// The bundle minor version. + /// + public uint BundleMinorVersion { get; set; } + + /// + /// The bundle id. + /// + public string BundleID { get; set; } = string.Empty; + + /// + /// The bundle file collection. + /// + public List FileEntries { get; set; } = []; + + /// + /// The bundle manifest flags. + /// + public HeaderFlags Flags { get; set; } + + /// + /// The bundle manifest header flags. + /// + public enum HeaderFlags : ulong + { + None = 0, + NetcoreApp3CompatMode = 1 + } +} diff --git a/src/Snowberry.IO.SingleFile/Meta/FileEntry.cs b/src/Snowberry.IO.SingleFile/Meta/FileEntry.cs new file mode 100644 index 0000000..f43ea62 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/Meta/FileEntry.cs @@ -0,0 +1,115 @@ +using Snowberry.IO.Common.Reader.Interfaces; +using Snowberry.IO.Common.Writer.Interfaces; + +namespace Snowberry.IO.SingleFile.Meta; + +// https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs + +/// +/// The metadata of a file in a single-file bundle. +/// +public class FileEntry(BundleManifest bundleManifest) : IDisposable +{ + public const char DirectorySeparatorChar = '/'; + + /// + public override string ToString() + { + return RelativePath; + } + + /// + /// Reads the metadata of the file entry. + /// + /// The reader. + public void Read(IEndianReader reader) + { + FileMetadataOffset = reader.Position; + Location = FileLocation.Read(reader); + + if (BundleManifest.BundleMajorVersion >= 6) + CompressedSize = reader.ReadInt64(); + + FileType = (FileType)reader.ReadByte(); + RelativePath = reader.ReadString() ?? string.Empty; + } + + /// + /// Writes the metadata of the file entry. + /// + /// The writer. + public void Write(IEndianWriter writer) + { + WriteWith(writer, Location, CompressedSize); + } + + /// + /// Writes the metadata of the file entry using custom properties. + /// + /// The writer. + /// The location. + /// The compressed size. + public void WriteWith(IEndianWriter writer, FileLocation location, long compressedSize) + { + location.Write(writer); + + if (BundleManifest.BundleMajorVersion >= 6) + writer.Write(compressedSize); + + writer.Write((byte)FileType); + writer.Write(RelativePath); + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + + if (ModifiedFileEntryMeta == null) + return; + + ModifiedFileEntryMeta.Dispose(); + } + + /// + /// The owning bundle manifest. + /// + public BundleManifest BundleManifest { get; set; } = bundleManifest; + + /// + /// The location of the data inside the bundle. + /// + public FileLocation Location { get; set; } + + /// + /// The compressed size of the file. + /// + public long CompressedSize { get; set; } + + /// + /// The relative path of the file. + /// + public string RelativePath { get; set; } = string.Empty; + + /// + /// The file type. + /// + public FileType FileType { get; set; } + + /// + /// The offset of the file metadata in the application. + /// + /// Only used when reading. + public long FileMetadataOffset { get; set; } + + /// + /// The actual size of the file. + /// + /// Uses the compressed size if available, otherwise the location size. + public long ActualSize => CompressedSize > 0 ? CompressedSize : Location.Size; + + /// + /// The modified file data. + /// + public ModifiedFileEntryMeta? ModifiedFileEntryMeta { get; set; } +} diff --git a/src/Snowberry.IO.SingleFile/Meta/FileLocation.cs b/src/Snowberry.IO.SingleFile/Meta/FileLocation.cs new file mode 100644 index 0000000..e7f2471 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/Meta/FileLocation.cs @@ -0,0 +1,44 @@ +using Snowberry.IO.Common.Reader.Interfaces; +using Snowberry.IO.Common.Writer.Interfaces; + +namespace Snowberry.IO.SingleFile.Meta; + +/// +/// Represents a file location in the application binary. +/// +public struct FileLocation +{ + /// + /// Offset of the file in the application binary. + /// + public long Offset; + + /// + /// Size of the file in the application binary (uncompressed). + /// + public long Size; + + /// + /// Reads a from the given . + /// + /// The reader. + /// The read . + public static FileLocation Read(IEndianReader reader) + { + return new() + { + Offset = reader.ReadInt64(), + Size = reader.ReadInt64() + }; + } + + /// + /// Writes the to the given . + /// + /// The writer. + public readonly void Write(IEndianWriter writer) + { + writer.Write(Offset); + writer.Write(Size); + } +} diff --git a/src/Snowberry.IO.SingleFile/Meta/FileType.cs b/src/Snowberry.IO.SingleFile/Meta/FileType.cs new file mode 100644 index 0000000..396f48e --- /dev/null +++ b/src/Snowberry.IO.SingleFile/Meta/FileType.cs @@ -0,0 +1,39 @@ +namespace Snowberry.IO.SingleFile.Meta; + +// https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileType.cs + +/// +/// The type of a file in a single-file bundle. +/// +public enum FileType : byte +{ + /// + /// Unknown file type. + /// + Unknown, + + /// + /// Assembly file. + /// + Assembly, + + /// + /// A native binary. + /// + NativeBinary, + + /// + /// The *.deps.json file. + /// + DepsJson, + + /// + /// The *.runtimeconfig.json file. + /// + RuntimeConfigJson, + + /// + /// Debug symbols file. + /// + Symbols +} \ No newline at end of file diff --git a/src/Snowberry.IO.SingleFile/Meta/ModifiedFileEntryMeta.cs b/src/Snowberry.IO.SingleFile/Meta/ModifiedFileEntryMeta.cs new file mode 100644 index 0000000..18035b3 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/Meta/ModifiedFileEntryMeta.cs @@ -0,0 +1,27 @@ +namespace Snowberry.IO.SingleFile.Meta; + +/// +/// Stores the modification of a . +/// +public class ModifiedFileEntryMeta : IDisposable +{ + /// + public void Dispose() + { + GC.SuppressFinalize(this); + + ModifiedDataStream?.Dispose(); + } + + /// + /// The stream that contains the modified data. + /// + /// This should not be accessed directly. + public Stream? ModifiedDataStream; + + /// + /// The modified data location in the . + /// + /// This should not be accessed directly. + public FileLocation FileLocation { get; set; } +} diff --git a/src/Snowberry.IO.SingleFile/README.md b/src/Snowberry.IO.SingleFile/README.md new file mode 100644 index 0000000..ad36ccd --- /dev/null +++ b/src/Snowberry.IO.SingleFile/README.md @@ -0,0 +1,41 @@ +[![NuGet Version](https://img.shields.io/nuget/v/Snowberry.IO.SingleFile.svg?logo=nuget)](https://www.nuget.org/packages/Snowberry.IO.SingleFile/) + +# Snowberry.IO.SingleFile + +A library for reading and modifying bundles from single-file published .NET applications. + +## How to use it + +### Reading and modifying the data from a single-file published .NET application + +```csharp +using var singleFileBinaryData = SingleFileBinaryData.GetFromFile(o.InputFilePath); + +// To access the bundle manifest +var bundleManifest = singleFileBinaryData.BundleManifest; + +// To access information about the bundle's files +var fileEntry = bundleManifest.FileEntries[0]; + +// To read the file's data +using var fileEntryStream = singleFileBinaryData.GetStream(fileEntry); + +// Do stuff... + +// Modify the file entry +SingleFileBinaryData.ModifyFileEntry(fileEntry, new byte[]); +// or +fileEntry.ModifiedFileEntryMeta?.Dispose(); +fileEntry.ModifiedFileEntryMeta = new() +{ + ModifiedDataStream = stream, + FileLocation = new() + { + Offset = 0, + Size = stream.Length + } +}; + +// Repack the application +singleFileBinaryData.Save("output.exe", TargetInfo.Windows, new BundlerOptions()); +``` diff --git a/src/Snowberry.IO.SingleFile/SingleFileBinaryData.cs b/src/Snowberry.IO.SingleFile/SingleFileBinaryData.cs new file mode 100644 index 0000000..2709f59 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/SingleFileBinaryData.cs @@ -0,0 +1,386 @@ +using System.IO.Compression; +using System.IO.MemoryMappedFiles; +using System.Security.Cryptography; +using Snowberry.IO.Reader; +using Snowberry.IO.SingleFile.Meta; +using Snowberry.IO.Writer; + +namespace Snowberry.IO.SingleFile; + +public class SingleFileBinaryData : IDisposable +{ + private Stream? _modifiedBundleStream; + + // https://github.com/dotnet/runtime/blob/release/8.0/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs#L173 + private static readonly byte[] s_BundleSignature = + [ + // 32 bytes represent the bundle signature: SHA-256 for ".net core bundle" + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae + ]; + + /// + /// Gets the binary file data from the file. + /// + /// Returns if the bundle could not be found! + /// The file path. + /// The binary file data. + public static SingleFileBinaryData? GetFromFile(string filePath) + { + using var binaryFileMap = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + using var accessor = binaryFileMap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); + + int offset = BinarySearchHelper.SearchInView(accessor, s_BundleSignature); + + if (offset == -1) + return null; + + long bundleManifestOffset = accessor.ReadInt64(offset - 8); + + using var reader = new EndianStreamReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + reader.Position = bundleManifestOffset; + var bundleManifest = new BundleManifest(); + bundleManifest.Read(reader); + + long offsetMin = long.MaxValue; + long offsetMax = long.MinValue; + long bundleSize = 0; + for (int i = 0; i < bundleManifest.FileEntries.Count; i++) + { + var file = bundleManifest.FileEntries[i]; + offsetMin = Math.Min(offsetMin, file.Location.Offset); + offsetMax = Math.Max(offsetMax, file.Location.Offset + file.ActualSize); + + bundleSize += file.ActualSize; + } + + var result = new SingleFileBinaryData() + { + FilePath = filePath, + BundleSignatureOffset = offset, + BundleManifestOffset = bundleManifestOffset, + BundleManifest = bundleManifest, + IsBundleLastBinaryData = reader.Length == reader.Position && offsetMax == bundleManifestOffset, + BundleOffset = offsetMin, + SignatureOffset = offset + }; + + return result; + } + + /// + /// Saves the modified single file published binary application to the given target file path. + /// + /// The target file path. + /// The target info. + /// The bundler options. + public void Save(string targetFilePath, TargetInfo targetInfo, BundlerOptions? options = null) + { + Save(new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite), + targetInfo, options); + } + + /// + /// Saves the modified single file published binary application to the given target file path. + /// + /// The target stream. + /// The target info. + /// The bundler options. + public void Save(Stream targetStream, TargetInfo targetInfo, BundlerOptions? options = null) + { + options ??= new(); + + using var writer = new EndianStreamWriter(targetStream, false); + using var binaryReader = new EndianStreamReader(new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + using var bundleHash = SHA256.Create(); + + binaryReader.CopyTo(writer.BaseStream, (int)BundleOffset); + + using var fileListWriter = new EndianStreamWriter(new MemoryStream(), false); + + int assemblyFileAlignment = targetInfo switch + { + TargetInfo.Windows => 4096, + TargetInfo.Arm64 => 4096, + TargetInfo.Other => 64, + _ => 1 + }; + + long fileOffsetStart = writer.BaseStream.Position; + + var depsJsonLocation = new FileLocation(); + var runtimeJsonLocation = new FileLocation(); + + for (int i = 0; i < BundleManifest.FileEntries.Count; i++) + { + var fileEntry = BundleManifest.FileEntries[i]; + var fileResult = AddToBundle( + fileEntry, + assemblyFileAlignment, + options, + fileOffsetStart, + bundleHash); + + var fileEntryLocation = new FileLocation() + { + Offset = fileOffsetStart + fileResult.Offset, + Size = fileResult.FileSize + }; + + fileEntry.WriteWith(fileListWriter, fileEntryLocation, fileResult.CompressedSize); + + if (fileEntry.FileType == FileType.DepsJson) + depsJsonLocation = fileEntryLocation; + else if (fileEntry.FileType == FileType.RuntimeConfigJson) + runtimeJsonLocation = fileEntryLocation; + } + + ModifiedBundleStream.Position = 0; + ModifiedBundleStream.CopyTo(writer.BaseStream); + + long bundleManifestOffset = writer.Position; + + BundleManifest.Write( + writer, + false, + GenerateDeterministicId(bundleHash), + depsJsonLocation, + runtimeJsonLocation); + + fileListWriter.Position = 0; + fileListWriter.BaseStream.CopyTo(writer.BaseStream); + + writer.Position = SignatureOffset - 8; + writer.Write(bundleManifestOffset); + + DisposeModifiedBundleStream(); + } + + private (long Offset, long CompressedSize, long FileSize) AddToBundle( + FileEntry fileEntry, + int assemblyFileAlignment, + BundlerOptions options, + long bundleStartOffset, + SHA256 bundleHash) + { + long offset = ModifiedBundleStream.Position; + + using var fileEntryStream = GetStream(fileEntry) ?? throw new IOException($"Could not get stream to file entry: `{fileEntry.RelativePath}`"); + long fileSize = fileEntryStream.Length; + + // Entry file hash + { + byte[] hashBytes = ComputeSha256Hash(fileEntryStream); + bundleHash.TransformBlock(hashBytes, 0, hashBytes.Length, hashBytes, 0); + } + + fileEntryStream.Position = 0; + + if (options.UseCompression && ShouldCompress(fileEntry.FileType)) + { + var smallestSize = (CompressionLevel)3; + using (var compressionStream = new DeflateStream(ModifiedBundleStream, Enum.IsDefined(typeof(CompressionLevel), smallestSize) ? smallestSize : CompressionLevel.Optimal, leaveOpen: true)) + { + fileEntryStream.CopyTo(compressionStream); + } + + long compressedSize = ModifiedBundleStream.Position - offset; + + if (options.ForceCompression || compressedSize < fileSize * options.CompressionThreshold) + return (offset, compressedSize, fileSize); + + ModifiedBundleStream.Seek(offset, SeekOrigin.Begin); + } + + if (fileEntry.FileType == FileType.Assembly && assemblyFileAlignment != 1) + { + long misalignment = (ModifiedBundleStream.Position + bundleStartOffset) % assemblyFileAlignment; + + if (misalignment != 0) + { + long padding = assemblyFileAlignment - misalignment; + ModifiedBundleStream.Position += padding; + } + } + + fileEntryStream.Position = 0; + offset = ModifiedBundleStream.Position; + + fileEntryStream.CopyTo(ModifiedBundleStream); + return (offset, 0, fileSize); + } + + /// + /// Changes the modified state of the given . + /// + /// The file entry. + /// The modified data. + public static void ModifyFileEntry(FileEntry fileEntry, byte[] data) + { + fileEntry.ModifiedFileEntryMeta?.Dispose(); + fileEntry.ModifiedFileEntryMeta = new() + { + ModifiedDataStream = new MemoryStream(data), + FileLocation = new() + { + Offset = 0, + Size = data.Length + } + }; + } + + /// + /// Gets a data stream to the given file entry. + /// + /// + /// The file entry must exist in . + /// Be careful, if the file is modified it will reuse the modified file stream. + /// + /// The file entry. + /// The stream to the file entry data, + public Stream? GetStream(FileEntry fileEntry) + { + if (BundleManifest == null || !BundleManifest.FileEntries.Contains(fileEntry)) + return null; + + if (!File.Exists(FilePath)) + return null; + + if (fileEntry.ModifiedFileEntryMeta != null && fileEntry.ModifiedFileEntryMeta.ModifiedDataStream != null) + { + var modifiedStream = fileEntry.ModifiedFileEntryMeta.ModifiedDataStream; + var viewStream = new StreamView( + modifiedStream, + fileEntry.ModifiedFileEntryMeta.FileLocation.Offset, + fileEntry.ModifiedFileEntryMeta.FileLocation.Size, + true); + + return viewStream; + } + + using var reader = new EndianStreamReader(new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + reader.Position = fileEntry.Location.Offset; + + MemoryStream memoryStream = new(); + if (fileEntry.CompressedSize > 0) + { + using var compressionStream = new DeflateStream(reader.Stream!, CompressionMode.Decompress, leaveOpen: true); + + byte[] buffer = new byte[4096 * 2]; + int bytesRead; + + while ((bytesRead = compressionStream.Read(buffer, 0, buffer.Length)) > 0) + { + memoryStream.Write(buffer, 0, bytesRead); + + if (memoryStream.Length >= fileEntry.Location.Size) + break; + } + + memoryStream.Position = 0; + return memoryStream; + } + + reader.CopyTo(memoryStream, (int)fileEntry.Location.Size); + memoryStream.Position = 0; + return memoryStream; + } + + private static string GenerateDeterministicId(SHA256 bundleHash) + { + bundleHash.TransformFinalBlock([], 0, 0); + byte[] manifestHash = bundleHash.Hash!; + + return Convert.ToBase64String(manifestHash)[BundleManifest.BundleIdLength..].Replace('/', '_'); + } + + private static byte[] ComputeSha256Hash(Stream stream) + { + using var sha = SHA256.Create(); + return sha.ComputeHash(stream); + } + + private static bool ShouldCompress(FileType type) + { + return type switch + { + FileType.DepsJson or FileType.RuntimeConfigJson => false, + _ => true, + }; + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + + BundleManifest?.Dispose(); + DisposeModifiedBundleStream(); + } + + private void DisposeModifiedBundleStream() + { + _modifiedBundleStream?.Dispose(); + _modifiedBundleStream = null; + } + + /// + /// The offset of the signature where the manifest offset is located. + /// + public long SignatureOffset { get; set; } + + /// + /// The file path. + /// + public string FilePath { get; private set; } = string.Empty; + + /// + /// Determines whether the bundle is the last binary data in the file after the host code etc. + /// + public bool IsBundleLastBinaryData { get; set; } + + /// + /// The signature offset of the bundle. + /// + public int BundleSignatureOffset { get; set; } + + /// + /// The offset of the bundle manifest. + /// + public long BundleManifestOffset { get; set; } + + /// + /// The offset where the bundle starts. + /// + public long BundleOffset { get; set; } + + /// + /// The modified bundle stream. + /// + /// + /// The existing stream will be dispoed when the setter is used. + /// By default a is used. + /// + public Stream ModifiedBundleStream + { + get + { + _modifiedBundleStream ??= new MemoryStream(); + + return _modifiedBundleStream; + } + + set + { + DisposeModifiedBundleStream(); + _modifiedBundleStream = value; + } + } + + /// + /// The bundle manifest. + /// + public BundleManifest BundleManifest { get; set; } = new(); +} diff --git a/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj b/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj new file mode 100644 index 0000000..0600b4d --- /dev/null +++ b/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj @@ -0,0 +1,53 @@ + + + + + + net8.0;net9.0 + enable + enable + true + + A library for reading and modifying bundles from single-file published .NET applications. + single-file-publish, single-file, dotnet, csharp, cs, singlefilepublish + + README.md + + + + + true + + + true + + + embedded + + + + + true + + + + true + + + + + + + + + + + True + \ + + + + + + + diff --git a/src/Snowberry.IO.SingleFile/TargetInfo.cs b/src/Snowberry.IO.SingleFile/TargetInfo.cs new file mode 100644 index 0000000..6fb1b26 --- /dev/null +++ b/src/Snowberry.IO.SingleFile/TargetInfo.cs @@ -0,0 +1,14 @@ +namespace Snowberry.IO.SingleFile; + +// https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/TargetInfo.cs#L58 +public enum TargetInfo +{ + /// + /// Applies no padding to the bundle file entries. + /// + Unknown, + + Other, + Windows, + Arm64 +} diff --git a/src/Snowberry.IO.SingleFile/ViewStream.cs b/src/Snowberry.IO.SingleFile/ViewStream.cs new file mode 100644 index 0000000..bfd5fdd --- /dev/null +++ b/src/Snowberry.IO.SingleFile/ViewStream.cs @@ -0,0 +1,216 @@ +namespace Snowberry.IO.SingleFile; + +public class StreamView : Stream +{ + private readonly bool _leaveOpen; + private readonly Stream _baseStream; + private readonly long _offset; + private readonly long _length; + private long _currentPosition; + + public StreamView(Stream baseStream, long offset, long length, bool leaveOpen) + { + _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + if (!baseStream.CanRead) + throw new ArgumentException("Stream must be readable", nameof(baseStream)); + if (offset < 0 || length < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "offset/length must be non-negative"); + if (offset + length > baseStream.Length) + throw new ArgumentOutOfRangeException(nameof(offset), "offset + length exceeds stream length"); + + _baseStream.Seek(offset, SeekOrigin.Begin); + _offset = offset; + _length = length; + _currentPosition = 0; + _leaveOpen = leaveOpen; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (_currentPosition >= _length) + return 0; + if (_currentPosition + count > _length) + count = (int)(_length - _currentPosition); + + int read = _baseStream.Read(buffer, offset, count); + _currentPosition += read; + return read; + } + + /// + public override int Read(Span buffer) + { + if (_currentPosition >= _length) + return 0; + + int count = (int)Math.Min(buffer.Length, _length - _currentPosition); + int read = _baseStream.Read(buffer[..count]); + _currentPosition += read; + return read; + } + + /// + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_currentPosition >= _length) + return 0; + + count = (int)Math.Min(count, _length - _currentPosition); + int read = await _baseStream.ReadAsync(buffer, offset, count, cancellationToken); + _currentPosition += read; + return read; + } + + /// + public override int ReadByte() + { + if (_currentPosition >= _length) + return -1; + + int result = _baseStream.ReadByte(); + if (result != -1) + _currentPosition++; + + return result; + } + + /// + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (_currentPosition >= _length) + return 0; + + int count = (int)Math.Min(buffer.Length, _length - _currentPosition); + int read = await _baseStream.ReadAsync(buffer[..count], cancellationToken); + _currentPosition += read; + return read; + } + + /// + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + count = _currentPosition >= _length ? 0 : (int)Math.Min(count, _length - _currentPosition); + return _baseStream.BeginRead(buffer, offset, count, callback, state); + } + + /// + public override int EndRead(IAsyncResult asyncResult) + { + int read = _baseStream.EndRead(asyncResult); + _currentPosition += read; + return read; + } + + /// + public override void CopyTo(Stream destination, int bufferSize) + { + byte[] buffer = new byte[bufferSize]; + int read; + while ((read = Read(buffer, 0, buffer.Length)) > 0) + destination.Write(buffer, 0, read); + } + + /// + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + byte[] buffer = new byte[bufferSize]; + int read; + while ((read = await ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + await destination.WriteAsync(buffer, 0, read, cancellationToken); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + long newPosition = origin switch + { + SeekOrigin.Begin => _offset + offset, + SeekOrigin.Current => _baseStream.Position + offset, + SeekOrigin.End => _offset + _length + offset, + _ => throw new ArgumentException("Invalid SeekOrigin", nameof(origin)) + }; + + if (newPosition < _offset || newPosition > _offset + _length) + throw new ArgumentOutOfRangeException(nameof(offset)); + + _baseStream.Seek(newPosition, SeekOrigin.Begin); + _currentPosition = newPosition - _offset; + return _currentPosition; + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _baseStream.FlushAsync(cancellationToken); + } + + /// + public override void Flush() + { + _baseStream.Flush(); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing && !_leaveOpen) + _baseStream.Dispose(); + + base.Dispose(disposing); + } + + /// + public override bool CanRead => _baseStream.CanRead; + + /// + public override bool CanSeek => _baseStream.CanSeek; + + /// + public override bool CanWrite => false; + + /// + public override long Length => _length; + + /// + public override long Position + { + get => _currentPosition; + set + { + if (value < 0 || value > _length) + throw new ArgumentOutOfRangeException(nameof(value)); + _currentPosition = value; + _baseStream.Position = _offset + value; + } + } + + /// + public override bool CanTimeout => _baseStream.CanTimeout; + + /// + public override int ReadTimeout + { + get => _baseStream.ReadTimeout; + set => _baseStream.ReadTimeout = value; + } + + /// + public override int WriteTimeout + { + get => _baseStream.WriteTimeout; + set => _baseStream.WriteTimeout = value; + } +} \ No newline at end of file diff --git a/src/Snowberry.IO.sln b/src/Snowberry.IO.sln index 3872247..69d6f74 100644 --- a/src/Snowberry.IO.sln +++ b/src/Snowberry.IO.sln @@ -17,6 +17,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowberry.IO.SourceGenerator", "Snowberry.IO.SourceGenerator\Snowberry.IO.SourceGenerator.csproj", "{17701010-A115-4E43-9BC0-0D0FFD0E4C20}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Addons", "Addons", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SingleFile", "SingleFile", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowberry.IO.SingleFile", "Snowberry.IO.SingleFile\Snowberry.IO.SingleFile.csproj", "{135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowberry.IO.SingleFile.CLI", "Snowberry.IO.SingleFile.CLI\Snowberry.IO.SingleFile.CLI.csproj", "{F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +51,14 @@ Global {17701010-A115-4E43-9BC0-0D0FFD0E4C20}.Debug|Any CPU.Build.0 = Debug|Any CPU {17701010-A115-4E43-9BC0-0D0FFD0E4C20}.Release|Any CPU.ActiveCfg = Release|Any CPU {17701010-A115-4E43-9BC0-0D0FFD0E4C20}.Release|Any CPU.Build.0 = Release|Any CPU + {135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE}.Release|Any CPU.Build.0 = Release|Any CPU + {F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -50,6 +66,9 @@ Global GlobalSection(NestedProjects) = preSolution {029FFEBB-9ABD-4C80-839E-D8F0C363BF24} = {BF911002-AABA-4993-AD65-63808B1F6E96} {C1B7175E-3128-492A-8DCB-5DEA4415CAB7} = {48C2A5C3-51D7-4E22-8BE2-12B120DD3D09} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {961967CF-F3F5-41D4-9BCF-3A1C1C588103} diff --git a/src/Snowberry.IO/Extensions/ReaderExtensions.cs b/src/Snowberry.IO/Extensions/ReaderExtensions.cs index 4dddd95..8f936ff 100644 --- a/src/Snowberry.IO/Extensions/ReaderExtensions.cs +++ b/src/Snowberry.IO/Extensions/ReaderExtensions.cs @@ -18,7 +18,7 @@ public static class ReaderExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ReadLongAt(this IEndianReader reader, EndianType endian = EndianType.LITTLE, int offset = 0) { - return BinaryEndianConverter.ToLong(reader.Buffer, offset, endian); + return BinaryEndianConverter.ToInt64(reader.Buffer, offset, endian); } /// @@ -31,7 +31,7 @@ public static long ReadLongAt(this IEndianReader reader, EndianType endian = End [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong ReadULongAt(this IEndianReader reader, EndianType endian = EndianType.LITTLE, int offset = 0) { - return BinaryEndianConverter.ToULong(reader.Buffer, offset, endian); + return BinaryEndianConverter.ToUInt64(reader.Buffer, offset, endian); } /// diff --git a/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs b/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs index daa3532..f8b666c 100644 --- a/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs +++ b/src/Snowberry.IO/Reader/BaseEndianReader.Primitives.cs @@ -17,7 +17,7 @@ public long ReadInt64(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[8]; ReadExactly(bytes); - return BinaryEndianConverter.ToLong(bytes, endian); + return BinaryEndianConverter.ToInt64(bytes, endian); } /// @@ -25,7 +25,7 @@ public ulong ReadUInt64(EndianType endian = EndianType.LITTLE) { Span bytes = stackalloc byte[8]; ReadExactly(bytes); - return BinaryEndianConverter.ToULong(bytes, endian); + return BinaryEndianConverter.ToUInt64(bytes, endian); } /// From 008e8be4c406fb0604d98ae555f36d6500b0e070 Mon Sep 17 00:00:00 2001 From: VNC Date: Sun, 12 Jan 2025 11:51:57 +0100 Subject: [PATCH 3/5] Update NUKE --- build/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/_build.csproj b/build/_build.csproj index 5f838b0..fe96c46 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -14,7 +14,7 @@ - + From de6c7e805a51958cb6ebe888bacd23162002d0eb Mon Sep 17 00:00:00 2001 From: VNC Date: Sun, 12 Jan 2025 12:01:31 +0100 Subject: [PATCH 4/5] Cleanup --- src/FrameworkCompatibility.props | 11 ++++++++ src/NuGetPackage.props | 11 -------- .../Snowberry.IO.Common.csproj | 2 ++ .../Snowberry.IO.SingleFile.csproj | 26 ------------------- src/Snowberry.IO.sln | 10 +++---- src/Snowberry.IO/Snowberry.IO.csproj | 2 +- 6 files changed, 19 insertions(+), 43 deletions(-) create mode 100644 src/FrameworkCompatibility.props diff --git a/src/FrameworkCompatibility.props b/src/FrameworkCompatibility.props new file mode 100644 index 0000000..50b1479 --- /dev/null +++ b/src/FrameworkCompatibility.props @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/NuGetPackage.props b/src/NuGetPackage.props index ca67284..922b8e1 100644 --- a/src/NuGetPackage.props +++ b/src/NuGetPackage.props @@ -49,15 +49,4 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Snowberry.IO.Common/Snowberry.IO.Common.csproj b/src/Snowberry.IO.Common/Snowberry.IO.Common.csproj index d6c3e8a..13fd069 100644 --- a/src/Snowberry.IO.Common/Snowberry.IO.Common.csproj +++ b/src/Snowberry.IO.Common/Snowberry.IO.Common.csproj @@ -22,4 +22,6 @@ \ + + \ No newline at end of file diff --git a/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj b/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj index 0600b4d..9c5a269 100644 --- a/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj +++ b/src/Snowberry.IO.SingleFile/Snowberry.IO.SingleFile.csproj @@ -14,32 +14,6 @@ README.md - - - true - - - true - - - embedded - - - - - true - - - - true - - - - - - - - True diff --git a/src/Snowberry.IO.sln b/src/Snowberry.IO.sln index 69d6f74..1a4c37e 100644 --- a/src/Snowberry.IO.sln +++ b/src/Snowberry.IO.sln @@ -17,14 +17,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowberry.IO.SourceGenerator", "Snowberry.IO.SourceGenerator\Snowberry.IO.SourceGenerator.csproj", "{17701010-A115-4E43-9BC0-0D0FFD0E4C20}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Addons", "Addons", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SingleFile", "SingleFile", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowberry.IO.SingleFile", "Snowberry.IO.SingleFile\Snowberry.IO.SingleFile.csproj", "{135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snowberry.IO.SingleFile.CLI", "Snowberry.IO.SingleFile.CLI\Snowberry.IO.SingleFile.CLI.csproj", "{F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AddOns", "AddOns", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SingleFile", "SingleFile", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,9 +66,9 @@ Global GlobalSection(NestedProjects) = preSolution {029FFEBB-9ABD-4C80-839E-D8F0C363BF24} = {BF911002-AABA-4993-AD65-63808B1F6E96} {C1B7175E-3128-492A-8DCB-5DEA4415CAB7} = {48C2A5C3-51D7-4E22-8BE2-12B120DD3D09} - {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {135FBE22-9D4D-FF2A-0D6B-B09EF3D1C6FE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {F5A3FDCC-6423-F4E5-D1CC-5D4DE54EA124} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {961967CF-F3F5-41D4-9BCF-3A1C1C588103} diff --git a/src/Snowberry.IO/Snowberry.IO.csproj b/src/Snowberry.IO/Snowberry.IO.csproj index e496307..dc5ade5 100644 --- a/src/Snowberry.IO/Snowberry.IO.csproj +++ b/src/Snowberry.IO/Snowberry.IO.csproj @@ -25,4 +25,4 @@ - + \ No newline at end of file From 3a48fc051f953c073813c0cc43e7878a1a3bb550 Mon Sep 17 00:00:00 2001 From: VNC Date: Sun, 12 Jan 2025 12:06:17 +0100 Subject: [PATCH 5/5] Updated NuGets --- .../Snowberry.IO.SingleFile.CLI.csproj | 2 +- .../Snowberry.IO.SourceGenerator.csproj | 2 +- src/Snowberry.IO.Tests/Snowberry.IO.Tests.csproj | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj b/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj index 570c554..6ca7555 100644 --- a/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj +++ b/src/Snowberry.IO.SingleFile.CLI/Snowberry.IO.SingleFile.CLI.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Snowberry.IO.SourceGenerator/Snowberry.IO.SourceGenerator.csproj b/src/Snowberry.IO.SourceGenerator/Snowberry.IO.SourceGenerator.csproj index d2bfc84..fbae325 100644 --- a/src/Snowberry.IO.SourceGenerator/Snowberry.IO.SourceGenerator.csproj +++ b/src/Snowberry.IO.SourceGenerator/Snowberry.IO.SourceGenerator.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Snowberry.IO.Tests/Snowberry.IO.Tests.csproj b/src/Snowberry.IO.Tests/Snowberry.IO.Tests.csproj index 1360010..be14820 100644 --- a/src/Snowberry.IO.Tests/Snowberry.IO.Tests.csproj +++ b/src/Snowberry.IO.Tests/Snowberry.IO.Tests.csproj @@ -12,13 +12,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all