diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..821957a --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "gitversion.tool": { + "version": "6.6.0", + "commands": [ + "dotnet-gitversion" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1d6ad4..d4429ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - name: Setup uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 10.0.x source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json - name: Pack @@ -40,5 +40,4 @@ jobs: run: dotnet test - name: Push - if: github.ref == 'refs/heads/main' run: dotnet nuget push "../bin/Release/*.nupkg" -k ${{ secrets.PACKAGE_REGISTRY_TOKEN }} -s https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --skip-duplicate diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index f4a491f..1933cd4 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -27,7 +27,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 10.0.x source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json - name: Pack diff --git a/CODE_ANALYSIS.md b/CODE_ANALYSIS.md new file mode 100644 index 0000000..dbdf81f --- /dev/null +++ b/CODE_ANALYSIS.md @@ -0,0 +1,603 @@ +# AutoByte Code Analysis Report + +## Executive Summary + +AutoByte is a well-designed, high-performance library with excellent use of C# source generators and ref structs. However, there are several code quality issues, potential bugs, and some areas that could be simplified. + +**Overall Assessment:** 7/10 - Good architecture, needs refactoring in specific areas + +--- + +## What's Good βœ… + +### 1. **Excellent Use of Ref Struct (`ByteSlide`)** +- Stack-allocated, zero-heap-allocation design is excellent for performance +- Prevents accidental boxing through ref struct constraint +- Proper use of `ReadOnlySpan` for zero-copy slicing +- **Impact:** Minimal GC pressure in tight parsing loops + +### 2. **Smart Inlining Strategy** +- All ByteSlide methods marked with `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +- JIT compiler will inline these, reducing method call overhead +- **Impact:** Critical for performance in performance-sensitive parsing code + +### 3. **Source Generator Architecture** +- Uses Roslyn to generate code at compile-time +- No runtime reflection overhead +- Type-safe verification at build-time +- Clean separation of concerns (syntax reception β†’ semantic analysis β†’ code generation) + +### 4. **Endianness Flexibility** +- Both little-endian and big-endian readers available +- Generic versions allow enum casting +- Covers both common use cases (ZIP, most protocols use little-endian; network protocols use big-endian) + +### 5. **Attribute-Driven Configuration** +- Clean, declarative API via `[AutoByteStructure]`, `[AutoByteField]`, `[AutoByteString]` +- `SizeFromProperty` enables dynamic sizing based on previously-parsed fields +- Reduced boilerplate compared to manual implementations + +### 6. **DateTime Support** +- `GetCDateUtcLittleEndian()` / `GetHfsDateUtcBigEndian()` for real-world format support +- Shows consideration for actual use cases (file systems, timestamps) + +--- + +## What's Bad ❌ + +### 1. **CRITICAL BUG: `PeekStructure()` Corrupts ByteSlide State** πŸ› + +**Location:** [ByteSlide.cs](ByteSlide.cs#L346-L355) + +```csharp +public T PeekStructure() where T : IByteStructure, new() +{ + var structure = new T(); + structure.Deserialize(ref this); // ⚠️ MODIFIES 'this' even though it's a peek! + return structure; +} +``` + +**The Root Cause: How `ref` Works with Structs** + +In C#, structs are value types. When passed normally, they're copied: +```csharp +public void ModifyStructCopy(ByteSlide slide) +{ + slide._slide = slide._slide[4..]; // Modifies COPY only, original unchanged +} +``` + +But with `ref`, the original is modified: +```csharp +public void ModifyStructRef(ref ByteSlide slide) +{ + slide._slide = slide._slide[4..]; // Modifies ORIGINAL via reference +} +``` + +The `PeekStructure` bug passes `ref this` to `Deserialize()`, which then modifies the **original** ByteSlide's internal state. + +**Real-World Impact Example** + +Developer's code: +```csharp +var slide = new ByteSlide(data); + +// Look ahead without consuming bytes +var peeked = slide.PeekStructure(); +Console.WriteLine($"Signature: {peeked.Signature}"); + +// Now read the actual structure (should read same bytes) +var actual = slide.GetStructure(); +Console.WriteLine($"Actual Signature: {actual.Signature}"); + +// Developer expects these to be identical +Assert.Equal(peeked.Signature, actual.Signature); // ⚠️ FAILS! +``` + +What happens: +1. `PeekStructure()` calls `Deserialize(ref this)` +2. Inside Deserialize: `slide.GetInt32LittleEndian()` advances the pointer by 4 bytes +3. `PeekStructure()` returns, but **the original ByteSlide pointer has been advanced** +4. `GetStructure()` now reads from DIFFERENT bytes (4 bytes ahead) +5. Signature values don't match β†’ **silent data corruption** + +**Silent Nature of Bug** + +No exceptions are thrown. The developer might see: +- ZIP headers parsing fine initially +- Suddenly getting garbage data from file entries +- Spending hours debugging "parsing logic" when it's really method behavior +- Testing might pass if peek/get are never used together on same data + +**Violates Standard Semantics** + +All standard data structures define peek as non-destructive: +```csharp +var stack = new Stack { 1, 2, 3 }; +int peeked = stack.Peek(); // Returns 3, stack unchanged +int popped = stack.Pop(); // Returns 3, same value! + +var queue = new Queue { 1, 2, 3 }; +int peeked = queue.Peek(); // Returns 1, queue unchanged +int dequeued = queue.Dequeue(); // Returns 1, same value! +``` + +**Recommendation:** +```csharp +public T PeekStructure() where T : IByteStructure, new() +{ + var savedSlide = this; // 1. Save original state + + var structure = new T(); + structure.Deserialize(ref this); // 2. Deserialize (modifies this._slide) + + this = savedSlide; // 3. Restore original state + + return structure; // 4. Return structure, pointer unchanged +} +``` + +Now both calls read from the same position and return identical values. + +--- + +### 2. **CRITICAL BUG: `GetCString()` Logic Error** πŸ› + +**Location:** [ByteSlide.cs](ByteSlide.cs#L241-L276) + +```csharp +public string GetCString(Encoding encoding, int maxLength) +{ + var zeroIndex = _slide.IndexOf((byte)0); + + if (zeroIndex != -1 && (zeroIndex - 1) <= maxLength) // ⚠️ OFF-BY-ONE! + { + // ... + } + + return encoding.GetString(Slide(maxLength)); +} +``` + +**Problem:** +- Condition `(zeroIndex - 1) <= maxLength` is incorrect +- Should be `zeroIndex < maxLength` or `zeroIndex <= maxLength - 1` +- Example: if `zeroIndex = 5` and `maxLength = 5`, condition evaluates to `4 <= 5` (true) + - But string of length 5 should NOT fit in maxLength of 5 +- Off-by-one error causes incorrect string length validation + +**Recommendation:** +```csharp +if (zeroIndex != -1 && zeroIndex < maxLength) +{ + // C string fits within maxLength +} +``` + +--- + +### 3. **CRITICAL BUG: `GetStructure()` Alignment Logic is Broken** πŸ› + +**Location:** [ByteSlide.cs](ByteSlide.cs#L298-L330) + +```csharp +public T GetStructure() where T : IByteStructure, new() +{ + int more; + var structure = new T(); + var start = _slide.Length; // ⚠️ Gets REMAINING length, not offset + + var size = structure.Deserialize(ref this); + + if (size > 0) + { + more = (start - _slide.Length) % size; // ⚠️ Broken math + + if (more > 0) + { + if (more > _slide.Length) + throw new ByteSlideException(...); + + _slide = _slide[more..]; // ⚠️ Skips wrong number of bytes + } + } + + return structure; +} +``` + +**Problem:** +- `start` stores remaining bytes before deserialization +- After deserialization, `start - _slide.Length` gives bytes READ +- Then `% size` tries to align to structure size +- **Logic is fundamentally flawed:** + - Should calculate: `bytes_read = start - _slide.Length` + - Should align to: how many bytes to skip to reach next multiple of `size` + - Current code: `more = bytes_read % size` then skips `more` bytes + - This means it skips 0 bytes if perfectly aligned, or skips remainder if not + - **Backward!** Should skip complement to reach next boundary + +**Example Bug:** +``` +start = 100 bytes remaining +structure.Deserialize reads 8 bytes +_slide.Length = 92 remaining + +bytes_read = 100 - 92 = 8 +more = 8 % 16 = 8 +Skips 8 bytes (wrong! structure was already 8 bytes, now you skip another 8!) +``` + +**Recommendation:** +```csharp +if (size > 0) +{ + var bytesRead = start - _slide.Length; + var remainder = bytesRead % size; + + if (remainder > 0) + { + int bytesToSkip = size - remainder; // Skip to next boundary + + if (bytesToSkip > _slide.Length) + throw new ByteSlideException(...); + + _slide = _slide[bytesToSkip..]; + } +} +``` + +--- + +### 4. **MAJOR: `GetByteArrayAlignTo()` Has Same Alignment Bug** πŸ› + +**Location:** [ByteSlide.cs](ByteSlide.cs#L281-L295) + +```csharp +public byte[] GetByteArrayAlignTo(int length, int align) +{ + var result = Slide(length).ToArray(); + + var alignment = length % align; // ⚠️ Same broken logic + + if (alignment > 0) + _slide = _slide[alignment..]; // ⚠️ Skips wrong direction + + return result; +} +``` + +**Problem:** Same as above - alignment logic is inverted + +**Recommendation:** +```csharp +public byte[] GetByteArrayAlignTo(int length, int align) +{ + var result = Slide(length).ToArray(); + + var remainder = length % align; + + if (remainder > 0) + { + int bytesToSkip = align - remainder; + if (bytesToSkip > _slide.Length) + throw new ByteSlideException(...); + _slide = _slide[bytesToSkip..]; + } + + return result; +} +``` + +--- + +### 5. **MAJOR: Debugger.Launch() in Production Code** πŸ› + +**Location:** [AutoByteSourceGenerator.cs](AutoByteSourceGenerator.cs#L15-L20) + +```csharp +public void Initialize(GeneratorInitializationContext context) +{ +#if DEBUG + if (!Debugger.IsAttached) + { + Debugger.Launch(); // ⚠️ Blocks compilation, waits for debugger + } +#endif + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); +} +``` + +**Problem:** +- Even with `#if DEBUG`, this severely impacts build performance +- Calls `Debugger.Launch()` which **blocks entire build** waiting for debugger attachment +- Makes debugging the generator easier but ruins developer experience +- Makes CI/CD builds extremely slow if DEBUG builds are used + +**Recommendation:** +- Remove entirely, use Visual Studio debugger attach-to-process instead +- Or move to separate diagnostic build configuration +- If kept, add environment variable guard: + ```csharp + if (Environment.GetEnvironmentVariable("AUTOBYTE_DEBUG_GENERATOR") == "1") + { + if (!Debugger.IsAttached) + Debugger.Launch(); + } + ``` + +--- + +### 6. **MAJOR: Generic Exception Throwing in Source Generator** πŸ› + +**Location:** [AutoByteSourceGenerator.cs](AutoByteSourceGenerator.cs#L138, 191-192) + +```csharp +string method = propertyType switch +{ + // ... cases ... + _ => throw new Exception($"AutoByte code generator does not support {propertyType}."), +}; +``` + +**Problem:** +- Throws bare `Exception` instead of `InvalidOperationException` +- Compiler doesn't generate proper error messages through Roslyn +- Hard to diagnose which property failed and why +- Should use `context.ReportDiagnostic()` for proper compiler integration + +**Recommendation:** +```csharp +private void ReportUnsupportedType(GeneratorExecutionContext context, + ClassDeclarationSyntax classDeclaration, string propertyType) +{ + var diagnostic = Diagnostic.Create( + new DiagnosticDescriptor( + "AB001", + "Unsupported Type", + $"AutoByte does not support type '{propertyType}'", + "AutoByte", + DiagnosticSeverity.Error, + true), + classDeclaration.GetLocation()); + + context.ReportDiagnostic(diagnostic); +} +``` + +--- + +### 7. **MAJOR: Throwing in SyntaxReceiver** πŸ› + +**Location:** [AutoByteSourceGenerator.cs](AutoByteSourceGenerator.cs#L209) + +```csharp +public void OnVisitSyntaxNode(SyntaxNode syntaxNode) +{ + if (syntaxNode is ClassDeclarationSyntax classDeclaration) + { + if (!classDeclaration.IsPartial()) + throw new Exception("Use partial class with AutoByteStructure attribute."); + } +} +``` + +**Problem:** +- Throwing in `OnVisitSyntaxNode` kills the entire generator +- Should queue diagnostic and continue +- Better to report all errors at once than fail on first + +**Recommendation:** +```csharp +if (!classDeclaration.IsPartial()) +{ + _errors.Add((classDeclaration, "Use partial class with AutoByteStructure attribute.")); +} +else +{ + CandidateClasses.Add(classDeclaration); +} +``` + +Then report diagnostics in Execute phase with proper Roslyn diagnostics. + +--- + +### 8. **Code Duplication: 48+ Nearly-Identical Methods** πŸ“Š + +**Location:** [ByteSlide.cs](ByteSlide.cs#L62-L186) + +```csharp +public short GetInt16LittleEndian() => ... +public T GetInt16LittleEndian() => ... +public ushort GetUInt16LittleEndian() => ... +public T GetUInt16LittleEndian() => ... +public short GetInt16BigEndian() => ... +public T GetInt16BigEndian() => ... +// ... repeated for int, long, uint, ulong, float, double ... +``` + +**Impact:** +- 400+ lines of nearly identical code +- Difficult to maintain +- Harder to spot bugs (and bugs exist in this section!) +- Makes API surface bloated + +**Could be eliminated with helper methods:** +```csharp +private T ReadPrimitive(Func, T> reader, int size) +{ + return reader(Slide(size)); +} + +public short GetInt16LittleEndian() + => ReadPrimitive(BinaryPrimitives.ReadInt16LittleEndian, sizeof(short)); +``` + +--- + +### 9. **Unsafe Type Casting in Generic Methods** + +**Location:** [ByteSlide.cs](ByteSlide.cs#L68, 73, etc.) + +```csharp +public T GetByte() => (T)(object)Slide(sizeof(byte))[0]; +public T GetInt16LittleEndian() => (T)(object)BinaryPrimitives.ReadInt16LittleEndian(Slide(sizeof(short))); +``` + +**Problem:** +- `(T)(object)value` is unsafe for generic types +- If T is `string` or `object`, runtime throws `InvalidCastException` +- No type safety checking at compile-time +- Generates bloated IL code + +**Better approach:** +```csharp +public T GetInt16(Func, T> converter) + => converter(Slide(sizeof(short))); + +// Usage: +GetInt16(BinaryPrimitives.ReadInt16LittleEndian) +``` + +Or use constraints: +```csharp +public T GetByte() where T : struct +{ + var value = Slide(sizeof(byte))[0]; + return (T)(object)value; // At least T is struct +} +``` + +--- + +## What's Overengineered πŸ—οΈ + +### 1. **`AlignTo()` Method Seems Unused** + +**Location:** [ByteSlide.cs](ByteSlide.cs#L333-L343) + +```csharp +public void AlignTo(int size, int align) +{ + var alignment = size % align; + if (alignment > 0) + _slide = _slide[alignment..]; +} +``` + +**Problem:** +- Not used in tests +- Alignment logic is wrong anyway (same as #3 above) +- Unclear use case + +**Recommendation:** Remove or clarify with real-world examples in tests. + +--- + +### 2. **Generic Overloads for Every Type** + +**Location:** [ByteSlide.cs](ByteSlide.cs#L68, 73, 79, etc.) + +```csharp +public short GetInt16LittleEndian() => ... +public T GetInt16LittleEndian() => ... // When would you use this? +``` + +**Problem:** +- Who needs to parse `int` as `ulong`? +- Generic version adds 50% more methods without clear benefit +- Just use explicit methods or extension methods + +**Recommendation:** Keep only concrete types, remove generic versions (or move to `unsafe` extension methods that require opt-in). + +--- + +### 3. **Multiple String Encodings via Attributes** + +**Location:** [AutoByteStringAttribute.cs](AutoByteStringAttribute.cs) + +```csharp +public string Encoding { get; set; } // UTF8, ASCII, Unicode, BigEndianUnicode... +public int CodePage { get; set; } // For System.Text.Encoding.CodePages +``` + +**Problem:** +- Most code uses UTF-8 +- Support for 7+ encodings adds complexity to generator +- CodePage requires extra package dependency + +**Recommendation:** Keep UTF-8 as default, provide clean extension point for custom encodings: +```csharp +[AutoByteString(Size = 32, Encoding = "custom:GetCustomEncoding()")] +``` + +--- + +## Obvious Bugs Summary πŸ› + +| Bug | Severity | Location | Impact | +|-----|----------|----------|--------| +| `PeekStructure()` advances pointer | CRITICAL | ByteSlide.cs:346 | Silent data corruption | +| `GetCString()` off-by-one check | CRITICAL | ByteSlide.cs:266 | Wrong string length validation | +| `GetStructure()` alignment broken | CRITICAL | ByteSlide.cs:310 | Incorrect alignment, data misalignment | +| `GetByteArrayAlignTo()` alignment | MAJOR | ByteSlide.cs:288 | Same alignment bug | +| `Debugger.Launch()` in generator | MAJOR | AutoByteSourceGenerator.cs:16 | Blocks builds | +| Generic exceptions in generator | MAJOR | AutoByteSourceGenerator.cs:138 | Poor error reporting | +| Exceptions in SyntaxReceiver | MAJOR | AutoByteSourceGenerator.cs:209 | Stops after first error | +| Unsafe generic casting | MEDIUM | ByteSlide.cs:68+ | Runtime errors if misused | + +--- + +## Recommendations by Priority + +### πŸ”΄ P0: Critical Fixes Needed + +1. **Fix `PeekStructure()`** - Silent data corruption is unacceptable +2. **Fix `GetStructure()` alignment** - Alignment bugs cause data misinterpretation +3. **Fix `GetCString()` off-by-one** - String parsing is broken +4. **Remove `Debugger.Launch()`** - Breaks build productivity + +### 🟠 P1: Should Fix Soon + +5. **Replace generic exceptions with Roslyn diagnostics** - Better error UX +6. **Move error reporting to Execute() phase** - Collect all errors before failing +7. **Fix `GetByteArrayAlignTo()` alignment** - Same root cause as #2 + +### 🟑 P2: Nice to Have + +8. **Consolidate duplicate methods** - Reduce code duplication via helpers +9. **Remove generic overloads** - Not clear they're needed +10. **Move DateTime helpers to extension library** - Keep core focused +11. **Review encoding support** - Simplify to UTF-8 + extension point + +### πŸ”΅ P3: Polish + +12. Add comprehensive tests for all edge cases +13. Document alignment semantics (or remove if not used) +14. Add XML docs to public API +15. Consider source link for debugging generated code + +--- + +## Code Quality Metrics + +| Metric | Score | Notes | +|--------|-------|-------| +| Architecture | 8/10 | Excellent source generator + ref struct design | +| API Design | 6/10 | Good core, cluttered with generics/encodings | +| Correctness | 3/10 | Multiple critical bugs in alignment and peek logic | +| Maintainability | 5/10 | High code duplication (400+ lines duplicated) | +| Performance | 9/10 | Excellent use of inlining and ref structs | +| Error Handling | 3/10 | Bare exceptions, poor diagnostics | +| Test Coverage | 4/10 | Basic tests exist, but gaps in edge cases | + +--- + +## Conclusion + +AutoByte has solid fundamentals with excellent architectural decisions around ref structs and source generators. However, **critical bugs in alignment logic and the peek method must be fixed immediately** before production use. The codebase would benefit from consolidating duplicated methods and improving error reporting through proper Roslyn diagnostics. + +**Priority: Fix critical bugs first, then refactor duplication, then optimize API surface.** diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..91007d2 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,527 @@ +# AutoByte Design Document + +## Overview + +AutoByte is a high-performance .NET library for deserializing binary data structures. It combines C# source generators with a zero-allocation parsing utility to provide compile-time code generation and runtime efficiency for binary protocol parsing. + +## Architecture + +### Layered Design + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User Code (decorated classes) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ AutoByte Core Library β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ IByteStructure Interface β”‚ β”‚ +β”‚ β”‚ [AutoByteStructure] Attribute β”‚ β”‚ +β”‚ β”‚ [AutoByteField] Attribute β”‚ β”‚ +β”‚ β”‚ [AutoByteString] Attribute β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ ByteSlide - Parsing Utility β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ReadOnlySpan wrapper β”‚ β”‚ +β”‚ β”‚ Position tracking β”‚ β”‚ +β”‚ β”‚ Primitive type readers β”‚ β”‚ +β”‚ β”‚ Array/String readers β”‚ β”‚ +β”‚ β”‚ Structure recursion support β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ AutoByte.Generators (Compile-Time) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ISourceGenerator Implementation β”‚ β”‚ +β”‚ β”‚ Syntax tree analysis β”‚ β”‚ +β”‚ β”‚ Code generation logic β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ System.Buffers.Binary (BinaryPrimitives) β”‚ +β”‚ ReadOnlySpan (Framework) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Core Components + +### 1. IByteStructure Interface + +**Location:** `AutoByte/IByteStructure.cs` + +**Purpose:** Contract for deserialization implementations + +```csharp +public partial interface IByteStructure +{ + /// + /// Deserialize bytes using byte slide. + /// + /// Byte slide deserializer + /// Return the size of the structure to move the byte pointer by the size + /// of the structure. Return zero to skip moving the byte pointer. + int Deserialize(ref ByteSlide slide); +} +``` + +**Design Rationale:** +- Ref parameter for ByteSlide allows position tracking without reference types +- Return value enables flexible pointer movement (0 for manual, or size for automatic skip) +- Partial interface allows extension through partial class declarations + +### 2. ByteSlide - The Parsing Engine + +**Location:** `AutoByte/ByteSlide.cs` + +**Type:** `ref struct` (stack-allocated, cannot be boxed or stored in fields) + +**Key Design Decisions:** + +#### Zero-Allocation Performance +- Uses `ReadOnlySpan` instead of managed arrays +- Ref struct allocation on stack instead of heap +- Slicing with `[..]` syntax creates new span views without copying data + +#### Position Tracking +```csharp +private ReadOnlySpan _slide; + +public ReadOnlySpan Slide(int size) +{ + if (size > _slide.Length) + throw new ByteSlideException(...); + + var slice = _slide[..size]; // Get first 'size' bytes + _slide = _slide[size..]; // Advance pointer + return slice; +} +``` + +#### Little-Endian and Big-Endian Support +- Delegates to `System.Buffers.Binary.BinaryPrimitives` +- Both conversion methods provided: + - `GetInt32LittleEndian()` / `GetInt32BigEndian()` + - Generic versions: `GetInt32LittleEndian()` + +#### Method Optimization +- All reader methods marked with `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +- JIT compiler inlines these methods for maximum performance +- Reduces method call overhead for tight loops + +### 3. Attribute System + +#### AutoByteStructure Attribute + +**Location:** `AutoByte/AutoByteStructureAttribute.cs` + +Marks a `partial class` for code generation. + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] +public class AutoByteStructureAttribute : Attribute +{ +} +``` + +**Usage Pattern:** +```csharp +[AutoByteStructure] +public partial class MyStructure +{ + public int Field1 { get; set; } +} +``` + +#### AutoByteField Attribute + +**Location:** `AutoByte/AutoByteFieldAttribute.cs` + +Configures variable-length byte array parsing. + +```csharp +public class AutoByteFieldAttribute : Attribute +{ + /// Skip number of bytes before reading the field. + public int Skip { get; set; } + + /// Size of the field or length of the array. + public int Size { get; set; } + + /// Use deserialized property value as the size of decorated field. + public string SizeFromProperty { get; set; } +} +``` + +**Design Patterns:** + +1. **Fixed Size:** + ```csharp + [AutoByteField(Size = 3)] + public byte[] StartingCHS { get; set; } + ``` + +2. **Dynamic Size from Previous Property:** + ```csharp + public short FileNameLength { get; set; } + + [AutoByteField(SizeFromProperty = "FileNameLength")] + public byte[] FileName { get; set; } + ``` + +3. **Skip Bytes:** + ```csharp + [AutoByteField(Skip = 4)] + public byte[] Padding { get; set; } + ``` + +#### AutoByteString Attribute + +**Location:** `AutoByte/AutoByteStringAttribute.cs` + +Specialized version of AutoByteField for UTF-8 strings. + +Uses same configuration as AutoByteField but returns `string` instead of `byte[]`. + +### 4. ByteExtensions Helper + +**Location:** `AutoByte/ByteExtensions.cs` + +**Purpose:** Convenience method for parsing structures from spans + +```csharp +public static T GetStructure(this ReadOnlySpan span) + where T : IByteStructure, new() +{ + var structure = new T(); + var slide = new ByteSlide(span); + structure.Deserialize(ref slide); + return structure; +} +``` + +**Usage:** +```csharp +byte[] data = GetBinaryData(); +var result = data.GetStructure(); +``` + +## Source Generator Design + +### AutoByteSourceGenerator + +**Location:** `AutoByte.Generators/AutoByteSourceGenerator.cs` + +**Type:** Roslyn `ISourceGenerator` implementation + +**Execution Pipeline:** + +``` +1. Initialize + └─ Register SyntaxReceiver for syntax tree notifications + +2. Execute + β”œβ”€ Get all candidate classes decorated with [AutoByteStructure] + β”œβ”€ Build semantic model for each class + β”œβ”€ Extract properties and analyze attributes + β”œβ”€ Generate Deserialize method implementation + └─ Add to compilation as new source file +``` + +### Key Generation Phases + +#### Phase 1: Syntax Reception +```csharp +public class SyntaxReceiver : ISyntaxReceiver +{ + public List CandidateClasses { get; } + + public void OnVisitSyntaxNode(SyntaxNode node) + { + // Collect all partial classes with [AutoByteStructure] + } +} +``` + +**Purpose:** Pre-filter syntax tree to identify target classes before semantic analysis (performance optimization) + +#### Phase 2: Semantic Analysis +- Resolve class symbols using semantic model +- Extract attribute information +- Identify property types and names +- Validate structure + +#### Phase 3: Code Generation +- Build C# code as string +- Use StringBuilder for efficiency +- Generate complete Deserialize method +- Handle nested structures recursively + +### Generated Code Example + +**Input:** +```csharp +[AutoByteStructure] +public partial class ZipFileHeader +{ + public int Signature { get; set; } + public short VersionNeededToExtract { get; set; } + + [AutoByteString(SizeFromProperty = "FileNameLength")] + public string FileName { get; set; } +} +``` + +**Generated Output:** +```csharp +public partial class ZipFileHeader : IByteStructure +{ + public int Deserialize(ref ByteSlide slide) + { + Signature = slide.GetInt32LittleEndian(); + VersionNeededToExtract = slide.GetInt16LittleEndian(); + FileName = slide.GetUtf8String(FileNameLength); + return 0; + } +} +``` + +### Generation Strategy + +**Type Mapping:** +| C# Type | ByteSlide Method | +|---------|------------------| +| `byte` | `GetByte()` | +| `short` | `GetInt16LittleEndian()` | +| `int` | `GetInt32LittleEndian()` | +| `long` | `GetInt64LittleEndian()` | +| `ushort` | `GetUInt16LittleEndian()` | +| `uint` | `GetUInt32LittleEndian()` | +| `ulong` | `GetUInt64LittleEndian()` | +| `byte[]` | `Slide().ToArray()` (with size) | +| `string` | `GetUtf8String()` (with size) | +| `T : IByteStructure` | `GetStructure()` | + +## Design Patterns Used + +### 1. Source Generator Pattern +Uses Roslyn APIs to generate code at compile-time, eliminating runtime reflection and improving performance. + +**Benefit:** Zero-cost abstractions, type safety verified at compile-time + +### 2. Ref Struct Pattern +`ByteSlide` uses ref struct to remain stack-allocated, preventing accidental heap allocations. + +**Benefit:** Predictable memory layout, no GC pressure for tight parsing loops + +### 3. Extension Method Pattern +`ByteExtensions.GetStructure()` provides convenience API on `ReadOnlySpan`. + +**Benefit:** Fluent API, discoverable through IntelliSense + +### 4. Attribute-Driven Configuration +Properties can be configured declaratively via attributes. + +**Benefit:** Declarative, separates concern from implementation + +### 5. Partial Class Extension +Generated code extends user-written partial classes. + +**Benefit:** Clean separation, generated code invisible in editor, but still modifiable + +## Data Flow + +### Deserialization Flow + +``` +Binary Data (byte[]) + ↓ +ByteSlide (ref struct, ReadOnlySpan) + β”œβ”€ Slide(4) β†’ reads 4 bytes, advances position + β”œβ”€ GetInt32LittleEndian() β†’ Slide(4) + BinaryPrimitives conversion + β”œβ”€ GetUtf8String(len) β†’ Slide(len) + UTF-8 decode + └─ GetStructure() β†’ Deserialize(ref this) + ↓ +Generated Deserialize Method (IByteStructure) + β”œβ”€ Signature = slide.GetInt32LittleEndian() + β”œβ”€ Version = slide.GetInt16LittleEndian() + β”œβ”€ FileName = slide.GetUtf8String(FileNameLength) + └─ return 0 + ↓ +User Object (T : IByteStructure) + (all properties populated from binary data) +``` + +### Example: ZIP Header Parsing + +``` +Input: byte[] { 0x50, 0x4B, 0x03, 0x04, ... } + └─ ZIP signature: 0x04034B50 (little-endian) + +↓ + +var slide = new ByteSlide(data); +var header = slide.GetStructure(); + +↓ + +Generated Code: + Signature = slide.GetInt32LittleEndian(); // reads bytes [0:4] + Version = slide.GetInt16LittleEndian(); // reads bytes [4:6] + // ... more properties ... + +↓ + +Output: ZipFileHeader +{ + Signature = 0x04034B50, + Version = 20, + // ... properties populated ... +} +``` + +## Exception Handling + +### ByteSlideException + +**Location:** `AutoByte/ByteSlideException.cs` + +Thrown when attempting to read beyond available bytes. + +```csharp +public class ByteSlideException : Exception +{ + // Custom exception for buffer overrun detection +} +``` + +**Example:** +```csharp +var slide = new ByteSlide(new byte[10]); +slide.GetInt32LittleEndian(); // OK, 4 bytes available +slide.GetInt32LittleEndian(); // OK, 4 bytes available +slide.GetInt32LittleEndian(); // Throws: only 2 bytes left +``` + +## Performance Characteristics + +### Time Complexity +- **Deserialization:** O(n) where n = total bytes to read +- **Attribute Resolution:** O(p) during generation, where p = properties +- **Code Generation:** O(p) where p = properties + +### Space Complexity +- **ByteSlide:** O(1) - only stores span reference and length +- **Generated Code:** O(p) - one method with p statements +- **Runtime Memory:** O(n) for object properties, no intermediate allocations + +### Optimization Techniques + +1. **Method Inlining:** All ByteSlide readers marked `[MethodImpl(MethodImplOptions.AggressiveInlining)]` + +2. **Stack Allocation:** ByteSlide is ref struct, never heap-allocated + +3. **Zero-Copy Slicing:** `ReadOnlySpan[..]` creates views without copying + +4. **Compile-Time Code Generation:** No reflection or dynamic invocation at runtime + +## Extension Points + +### Custom Deserialization + +Users can implement `IByteStructure` manually for complex logic: + +```csharp +public class ComplexStructure : IByteStructure +{ + public byte[] BootCode { get; set; } + public PartitionEntry[] PartitionEntries { get; set; } + + public int Deserialize(ref ByteSlide slide) + { + // Manual parsing with custom logic + BootCode = slide.GetByteArray(446); + + PartitionEntries = new PartitionEntry[4]; + for (int i = 0; i < 4; i++) + PartitionEntries[i] = slide.GetStructure(); + + return 0; + } +} +``` + +### Endianness Handling + +ByteSlide provides both: +- `GetInt32LittleEndian()` +- `GetInt32BigEndian()` + +Choose appropriate method for your binary format. + +### Nested Structures + +Structures can contain other structures: + +```csharp +[AutoByteStructure] +public partial class Outer +{ + [AutoByteStructure] + public partial class Inner + { + public int Value { get; set; } + } + + public Inner Child { get; set; } // Generated: GetStructure() +} +``` + +## Compatibility + +- **Target Frameworks:** .NET Standard 2.0, 2.1 +- **C# Version:** 10 (for source generators) +- **Dependencies:** + - `System.Buffers.Binary` (included in .NET Standard 2.0+) + - Microsoft.CodeAnalysis.CSharp (only for generators) + +## Testing Strategy + +### Unit Tests in AutoByte.Tests + +**Test Categories:** + +1. **Code Generation Tests (`CodeGenerator_Must.cs`)** + - Verify generated code produces correct deserialization + - Example: ZIP header, Master Boot Record + +2. **ByteSlide Tests (`ByteSlide_Must.cs`)** + - Boundary conditions (reading at end of buffer) + - Byte order conversions + - String parsing + +3. **Structure Tests (`ByteStructure_Must.cs`)** + - End-to-end deserialization + - Nested structures + - Dynamic sizing with `SizeFromProperty` + +4. **Real-World Format Tests** + - Structures in `Structures/` folder (ZipFileHeader, MasterBootRecord, etc.) + - Validates against actual binary file formats + +## Future Enhancement Opportunities + +1. **Big-Endian Code Generation:** Generate big-endian readers automatically +2. **Validation Attributes:** Add validation rules (min/max, enum checks) +3. **Offset Tracking:** Return byte offset information from Deserialize +4. **Streaming Support:** Parse from Stream instead of just byte arrays +5. **Serialization:** Generate serialization (Serialize method) to complement deserialization +6. **Compression Support:** Built-in DEFLATE/GZIP decompression +7. **Custom Type Converters:** Allow user-defined type mapping + +## Summary + +AutoByte combines three powerful concepts: + +1. **C# Source Generators** for compile-time code generation +2. **Ref Structs** for zero-allocation parsing +3. **Binary Primitives** for efficient endianness-aware reading + +The result is a type-safe, high-performance binary parsing library with zero runtime overhead and maximum developer ergonomics. diff --git a/GitVersion.yml b/GitVersion.yml index b6d3892..dbe0921 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,9 +1,13 @@ mode: ContinuousDeployment assembly-versioning-scheme: MajorMinorPatch -assembly-file-versioning-scheme: MajorMinorPatchTag -assembly-informational-format: '{SemVer}' -continuous-delivery-fallback-tag: main +assembly-informational-format: '{MajorMinorPatch}-{EscapedBranchName}.{VersionSourceDistance}' +increment: Patch branches: - master: - regex: main + main: + regex: ^main$ + label: 'main' + + unknown: + regex: ^(?.+)$ + label: '{BranchName}' \ No newline at end of file diff --git a/README.md b/README.md index e63d8b2..7752a63 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,15 @@ [![NuGet Version](https://img.shields.io/nuget/v/AutoByte?style=flat&label=Version)](https://www.nuget.org/packages/AutoByte/) [![NuGet Downloads](https://img.shields.io/nuget/dt/AutoByte.svg?style=flat&label=Downloads)](https://www.nuget.org/packages/AutoByte) +Fast .NET data structure deserializer and parser for parsing binary data formats. -Fast .NET data structure deserializer and parser. +* Automatically generate deserialization implementation from class properties using C# source generators. -* Automatically generate implementation from the class properties. +* Provides `ByteSlide` utility for safe byte-by-byte parsing with position tracking. -* Provides `ByteSlide` parser. +* Uses `BinaryPrimitives` and `ReadOnlySpan` for efficient, zero-allocation reading. -* Use `BinaryPrimitives` and `ReadOnlySpan` to read the data. +* Supports parsing of arbitrary binary protocols and file formats (ZIP headers, Master Boot Records, custom binary structures, etc.). @@ -372,7 +373,83 @@ public void Test1() -### Resources +## Installation + +Install the NuGet package: + +```bash +dotnet add package AutoByte +``` + +Or via Package Manager Console: + +```powershell +Install-Package AutoByte +``` + +## Key Features + +### Source Generators +AutoByte uses C# 10 source generators to automatically generate the `Deserialize` method implementation. Simply decorate your class with `[AutoByteStructure]` and make it `partial`, and the code generator creates the deserialization logic for you. + +### ByteSlide Utility +The `ByteSlide` ref struct provides a safe, efficient way to parse binary data: +- Tracks position automatically as you read bytes +- Prevents reading past the buffer boundary +- Uses `ReadOnlySpan` for zero-allocation performance +- Supports both little-endian and big-endian byte order +- Methods for reading primitive types, arrays, and strings + +### Attribute-Based Configuration +* `[AutoByteStructure]` - Marks a class for code generation +* `[AutoByteField]` - Configures array/byte field parsing with `Size` or `SizeFromProperty` +* `[AutoByteString]` - Configures string field parsing (UTF-8) + +## Supported Data Types + +ByteSlide supports reading: +- Primitive types: `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong` +- Floating point: `float`, `double` +- Enums (as their underlying type) +- Strings (UTF-8) +- Byte arrays +- Nested structures (via `IByteStructure` interface) + +## API Reference + +### ByteSlide Methods +- `Slide(int size)` - Read and advance by `size` bytes +- `Skip(int size)` - Advance by `size` bytes without reading +- `GetByte()` / `GetUInt16LittleEndian()` / `GetInt32LittleEndian()` - Read primitive types +- `GetUtf8String(int length)` - Read UTF-8 string +- `GetByteArray(int length)` - Read byte array +- `GetStructure()` - Parse nested structure + +### IByteStructure Interface +Implement this interface to manually define deserialization logic: + +```csharp +public interface IByteStructure +{ + int Deserialize(ref ByteSlide slide); +} +``` + +Return value indicates bytes to skip (usually 0). + +## Project Structure + +- `AutoByte/` - Main library with attributes and ByteSlide implementation +- `AutoByte.Generators/` - Source generator for automatic code generation +- `AutoByte.Tests/` - Unit tests with examples + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +Copyright (c) 2023 Matt Janda + +## Resources Byte icon was downloaded from [Flaticon](https://www.flaticon.com/free-icon/byte_5044438) diff --git a/src/AutoByte.Tests/AutoByte.Tests.csproj b/src/AutoByte.Tests/AutoByte.Tests.csproj index 9f0d529..320cc71 100644 --- a/src/AutoByte.Tests/AutoByte.Tests.csproj +++ b/src/AutoByte.Tests/AutoByte.Tests.csproj @@ -1,7 +1,7 @@ ο»Ώ - net7.0 + net10.0 enable false true @@ -9,13 +9,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/AutoByte.Tests/ByteSlide_Must.cs b/src/AutoByte.Tests/ByteSlide_Must.cs index ec11161..c7a92b2 100644 --- a/src/AutoByte.Tests/ByteSlide_Must.cs +++ b/src/AutoByte.Tests/ByteSlide_Must.cs @@ -1,5 +1,6 @@ ο»Ώusing System.Buffers.Binary; using System.Text; +using AutoByte.Tests.Structures; namespace AutoByte.Tests { @@ -140,6 +141,82 @@ public void GetPascalString_ReturnsExcpectedStrings() } + /// + /// Test that demonstrates the PeekStructure bug. + /// + /// BUG: PeekStructure is supposed to be non-destructive (like Stack.Peek or Queue.Peek), + /// but it advances the internal pointer due to passing `ref this` to Deserialize(). + /// + /// This test WILL FAIL with the current buggy implementation because: + /// 1. PeekStructure calls Deserialize(ref this), which advances the pointer + /// 2. GetStructure then reads from the ADVANCED position, not the original + /// 3. The signatures will differ (peeked reads correct bytes, actual reads garbage) + /// + /// The test demonstrates "silent data corruption" - no exception is thrown, + /// the methods just return different results from the same data position. + /// + [Fact] + public void PeekStructure_ShouldNotAdvancePointer_BUG() + { + // ZIP header data: 0x50 0x4B 0x03 0x04 (signature PK\x03\x04) + var data = new byte[] + { + 0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x63, 0x54, + 0x96, 0x56, 0x45, 0x7F, 0x6A, 0xBD, 0x5B, 0x02, 0x00, 0x00, 0xF4, 0x08, + 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x41, 0x75, 0x74, 0x6F, 0x42, 0x79, + 0x74, 0x65, 0x2E, 0x73, 0x6C, 0x6E + }; + + var slide = new ByteSlide(data); + + // Peek at the structure (should NOT advance pointer) + var peeked = slide.PeekStructure(); + + // Now get the actual structure (should read from SAME position as peek) + var actual = slide.GetStructure(); + + // These should be IDENTICAL because peek should not advance the pointer + // If they differ, it proves the bug: peek advanced the pointer + Assert.Equal(peeked.Signature, actual.Signature); + Assert.Equal(peeked.VersionNeededToExtract, actual.VersionNeededToExtract); + Assert.Equal(peeked.GeneralPurposeBitFlag, actual.GeneralPurposeBitFlag); + Assert.Equal(peeked.CompressionMethod, actual.CompressionMethod); + Assert.Equal(peeked.LastModifiedFileTime, actual.LastModifiedFileTime); + Assert.Equal(peeked.LastModifiedFileDate, actual.LastModifiedFileDate); + Assert.Equal(peeked.Crc32, actual.Crc32); + Assert.Equal(peeked.CompressedSize, actual.CompressedSize); + Assert.Equal(peeked.UncompressedSize, actual.UncompressedSize); + Assert.Equal(peeked.FileNameLength, actual.FileNameLength); + Assert.Equal(peeked.ExtraFieldLength, actual.ExtraFieldLength); + Assert.Equal(peeked.FileName, actual.FileName); + } + + /// + /// Test demonstrating that PeekString (non-generic method) works correctly. + /// This shows the pattern that Peek methods SHOULD follow: non-destructive reads. + /// + [Fact] + public void PeekString_DoesNotAdvancePointer() + { + byte[] data = { + 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x00 + }; + + var slide = new ByteSlide(data); + + // Peek at the string + string peeked = slide.PeekString(Encoding.ASCII, 5); + Assert.Equal("Hello", peeked); + + // Read the string (should get same data) + string read = slide.GetString(Encoding.ASCII, 5); + Assert.Equal("Hello", read); + + // If pointer wasn't advanced by peek, the next 6 bytes should be " World" + string next = slide.GetString(Encoding.ASCII, 6); + Assert.Equal(" World", next); + } + diff --git a/src/AutoByte/AutoByte.csproj b/src/AutoByte/AutoByte.csproj index e6be8ba..331020c 100644 --- a/src/AutoByte/AutoByte.csproj +++ b/src/AutoByte/AutoByte.csproj @@ -7,7 +7,7 @@ AutoByte AutoByte AutoByte - Fast .NET data strucure deserializer and parser + Fast .NET data structure deserializer and parser LICENSE README.md autobyte byteslide data structure deserializer @@ -26,7 +26,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AutoByte/ByteSlide.cs b/src/AutoByte/ByteSlide.cs index bb82b32..c9ecdf4 100644 --- a/src/AutoByte/ByteSlide.cs +++ b/src/AutoByte/ByteSlide.cs @@ -353,8 +353,13 @@ public void AlignTo(int size, int align) [MethodImpl(MethodImplOptions.AggressiveInlining)] public T PeekStructure() where T : IByteStructure, new() { + var slide = this; // Save the original state + var structure = new T(); - structure.Deserialize(ref this); + structure.Deserialize(ref this); // Deserialize (may modify this._slide) + + this = slide; // Restore the original state + return structure; } }