diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..544027d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,58 @@ +--- +name: Bug Report +about: Report a bug to help us improve Stardust.Utilities +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Description + +A clear and concise description of the bug. + +## To Reproduce + +Steps to reproduce the behavior: + +1. Define a struct with `[BitFields(typeof(...))]` +2. Add properties with `[BitField(...)]` or `[BitFlag(...)]` +3. Call method '...' +4. See error + +## Expected Behavior + +A clear and concise description of what you expected to happen. + +## Actual Behavior + +What actually happened, including any error messages or stack traces. + +## Code Sample + +```csharp +// Minimal code to reproduce the issue +[BitFields(typeof(byte))] +public partial struct MyRegister +{ + [BitFlag(0)] public partial bool Flag { get; set; } +} +``` + +## Generated Code (if applicable) + +If the issue is with generated code, please include the relevant portion: + +```csharp +// Paste generated code here (found in obj/Generated/...) +``` + +## Environment + +- **Stardust.Utilities version**: [e.g., 0.9.2] +- **.NET version**: [e.g., .NET 10.0] +- **OS**: [e.g., Windows 11, macOS 14, Ubuntu 24.04] +- **IDE**: [e.g., Visual Studio 2026, VS Code, Rider] + +## Additional Context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4ed3df0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/dhadner/Stardust.Utilities/blob/main/README.md + about: Check the documentation before opening an issue + - name: Security Vulnerabilities + url: https://github.com/dhadner/Stardust.Utilities/security/advisories/new + about: Report security vulnerabilities privately (do NOT open a public issue) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..0a7fa18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,52 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement for Stardust.Utilities +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Summary + +A clear and concise description of the feature you'd like to see. + +## Problem Statement + +Describe the problem this feature would solve. What are you trying to accomplish that isn't possible or is difficult today? + +**Example:** "I'm frustrated when I have to manually write boilerplate code for..." + +## Proposed Solution + +Describe the solution you'd like. How would this feature work? + +```csharp +// Example of how you'd like to use the feature +[BitFields(typeof(byte))] +public partial struct MyRegister +{ + // Proposed new attribute or syntax +} +``` + +## Alternatives Considered + +Describe any alternative solutions or features you've considered. Why wouldn't these work as well? + +## Use Case + +Describe the real-world use case for this feature: + +- What type of application would benefit? (e.g., hardware emulation, network protocols, file formats) +- How common is this need? +- Are there workarounds you're currently using? + +## Additional Context + +Add any other context, screenshots, or examples about the feature request here. + +## Checklist + +- [ ] I have searched existing issues to ensure this isn't a duplicate +- [ ] I have considered if this fits the library's scope (bit manipulation, error handling, big-endian types) +- [ ] I am willing to help implement this feature (optional) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..d66d8d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,35 @@ +--- +name: Question +about: Ask a question about using Stardust.Utilities +title: '[QUESTION] ' +labels: question +assignees: '' +--- + +## Question + +What would you like to know? + +## Context + +Provide context about what you're trying to accomplish: + +- What feature are you using? (BitFields, Result types, Big-Endian types, Extensions) +- What have you tried so far? +- Have you checked the documentation? + +## Code (if applicable) + +```csharp +// Share relevant code if it helps explain your question +``` + +## Documentation Checked + +- [ ] I have read the [README](https://github.com/dhadner/Stardust.Utilities/blob/main/README.md) +- [ ] I have read the relevant detailed docs: + - [ ] [BITFIELDS.md](https://github.com/dhadner/Stardust.Utilities/blob/main/BITFIELDS.md) + - [ ] [RESULT.md](https://github.com/dhadner/Stardust.Utilities/blob/main/RESULT.md) + - [ ] [ENDIAN.md](https://github.com/dhadner/Stardust.Utilities/blob/main/ENDIAN.md) + - [ ] [EXTENSIONS.md](https://github.com/dhadner/Stardust.Utilities/blob/main/EXTENSIONS.md) +- [ ] I have searched existing issues for similar questions diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..523e55a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,55 @@ +## Description + +Please include a summary of the changes and which issue is fixed (if applicable). + +Fixes # (issue number) + +## Type of Change + +Please check the relevant option: + +- [ ] ?? Bug fix (non-breaking change that fixes an issue) +- [ ] ? New feature (non-breaking change that adds functionality) +- [ ] ?? Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] ?? Documentation update +- [ ] ?? Refactoring (no functional changes) +- [ ] ? Test additions or updates + +## Changes Made + +List the specific changes made: + +- +- +- + +## Testing + +Please describe the tests you ran to verify your changes: + +- [ ] Unit tests pass (`dotnet test`) +- [ ] New tests added for new functionality +- [ ] Manual testing performed + +**Test Configuration:** +- .NET version: +- OS: + +## Checklist + +- [ ] My code follows the project's code style +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published + +## Screenshots (if applicable) + +Add screenshots to help explain your changes if they affect visual output or generated code. + +## Additional Notes + +Add any other context about the pull request here. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..9f74673 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,16 @@ +# Copilot Instructions + +## General Guidelines +- Use plain text only in responses - do not use markdown hyperlinks. Output gets garbled when it contains hyperlinks due to a known bug. + +## Project-Specific Rules +- The package version is defined in Directory.Build.props at the repo root. Demo app csproj files reference it via $(Version) automatically. +- Always use the local NuGet package for Stardust.Utilities. Do not change the version in Directory.Build.props unless asked. +- Only rebuild the NuGet package (Build-Combined-NuGetPackages.ps1) when generator code or library code changes. Demo app changes only need a regular dotnet build since the generator is already packaged. +- When generator changes are made, rebuild using .\Build-Combined-NuGetPackages.ps1 -SkipTests (version is read from Directory.Build.props automatically). +- When changing features (adding, modifying, removing), ensure that appropriate test coverage is also updated (added, modified, removed). +- Features that include user or external input will require fuzz testing to ensure robustness and correct operation. +- Ensure that the Stardust.Utilities project handles all conceivable issues elegantly; edge cases are not acceptable. The library and its demos must always work as expected to generate trust with users evaluating it. +- Don't let a human or downstream user find an error that unit testing could have caught. +- Don't run the Performance test category when initially checking for regressions. Only run it when you have a specific reason to check for performance regressions, as it takes a long time to run and times out the terminal. +- All constants (const fields and const locals) must use SCREAMING_SNAKE_CASE (e.g., SIZE_IN_BYTES, LAST_WORD_MASK, NORMALIZATION_AND_MASK). This applies to both generator source code and the code the generators emit. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba53635..449fd85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,73 +1,55 @@ name: CI/CD +permissions: + contents: read on: push: - branches: [ main, master ] - tags: [ 'v*' ] + branches: [ main, master, dev ] pull_request: branches: [ main, master ] -env: - DOTNET_VERSION: '10.0.x' - jobs: build: runs-on: windows-latest - + steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET + - name: Setup .NET 7, 8, 9, and 10 uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-version: | + 7.0.x + 8.0.x + 9.0.x + 10.0.x + + # Restore projects explicitly (not via solution) so SolutionPath is unset + # and the test project restores all three TFMs: net8.0, net9.0, net10.0. + - name: Restore Generator + run: dotnet restore Generators/Stardust.Generators.csproj - - name: Restore dependencies - run: dotnet restore + - name: Restore Library + run: dotnet restore Stardust.Utilities.csproj + + - name: Restore Tests + run: dotnet restore Test/Stardust.Utilities.Tests.csproj - name: Build Generator run: dotnet build Generators/Stardust.Generators.csproj -c Release --no-restore - - name: Build Library + - name: 'Build Library (multi-target: net7.0, net8.0, net9.0, net10.0)' run: dotnet build Stardust.Utilities.csproj -c Release --no-restore - - name: Build Tests + - name: 'Build Tests (multi-target: net8.0, net9.0, net10.0)' run: dotnet build Test/Stardust.Utilities.Tests.csproj -c Release --no-restore - - name: Run Tests - run: dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --no-build --verbosity normal - - - name: Pack NuGet packages - if: startsWith(github.ref, 'refs/tags/v') - run: | - dotnet pack Generators/Stardust.Generators.csproj -c Release --no-build -o ./artifacts - dotnet pack Stardust.Utilities.csproj -c Release --no-build -o ./artifacts - - - name: Upload artifacts - if: startsWith(github.ref, 'refs/tags/v') - uses: actions/upload-artifact@v4 - with: - name: nuget-packages - path: ./artifacts/*.nupkg + - name: Run Tests (.NET 8) + run: dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --no-build --framework net8.0 --filter "Category!=Performance" --verbosity normal - publish: - needs: build - runs-on: windows-latest - if: startsWith(github.ref, 'refs/tags/v') - - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: nuget-packages - path: ./artifacts - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Run Tests (.NET 9) + run: dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --no-build --framework net9.0 --filter "Category!=Performance" --verbosity normal - - name: Publish to NuGet.org - run: | - dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + - name: Run Tests (.NET 10) + run: dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --no-build --framework net10.0 --filter "Category!=Performance" --verbosity normal diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 0000000..7b8a265 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,44 @@ +name: Dependency Submission + +# Replaces GitHub's automatic NuGet dependency submission, which fails when +# demo apps reference a Stardust.Utilities version not yet on NuGet.org. +# This workflow packs the library locally first so all project restores succeed. +# +# After creating this workflow, disable the automatic NuGet submission at: +# Settings > Code security > Dependency graph > Automatic dependency submission +on: + push: + branches: [ main ] + +permissions: + contents: write + +jobs: + submit-nuget: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 7, 8, 9, and 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 7.0.x + 8.0.x + 9.0.x + 10.0.x + + # Demo apps reference Stardust.Utilities via PackageReference using the + # version from Directory.Build.props. During development the version may + # not yet be published to NuGet.org. Build and pack the library locally + # so all project restores succeed. + - name: Pack library locally + run: dotnet pack Stardust.Utilities.csproj -c Release -o ./nupkg + + - name: Register local NuGet source + run: dotnet nuget add source "${{ github.workspace }}/nupkg" --name local + + - name: Submit dependencies + uses: advanced-security/component-detection-dependency-submission-action@v0.1.1 diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml new file mode 100644 index 0000000..436c84f --- /dev/null +++ b/.github/workflows/deploy-demo.yml @@ -0,0 +1,81 @@ +name: Deploy Demo to GitHub Pages + +# Runs on push to main when demo files change, or manually via workflow_dispatch. +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'Demo/BitFields.DemoWeb/**' + - 'Demo/BitFields.DemoApp/Models/**' + - 'Demo/BitFields.DemoApp/Utilities/**' + +permissions: + pages: write + id-token: write + contents: read + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 7, 8, 9, and 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 7.0.x + 8.0.x + 9.0.x + 10.0.x + + # The DemoWeb app references Stardust.Utilities via PackageReference using + # the version from Directory.Build.props. During development the version + # may not yet be published to NuGet.org. Build and pack the library locally + # so the publish restore succeeds. The pack needs all four SDKs because the + # library multi-targets net7.0, net8.0, net9.0, and net10.0. + - name: Pack library locally + run: dotnet pack Stardust.Utilities.csproj -c Release -o ./nupkg + + - name: Register local NuGet source + run: dotnet nuget add source "${{ github.workspace }}/nupkg" --name local + + # Set base href for GitHub Pages subdirectory + - name: Set base href + run: sed -i 's|||' Demo/BitFields.DemoWeb/wwwroot/index.html + + # Disable WASM SIMD so the published app works even when the browser + # disables JIT (e.g. Edge Enhanced Security Mode "Strict"). + - name: Install wasm-tools workload + run: dotnet workload install wasm-tools + + - name: Publish WASM app + run: dotnet publish Demo/BitFields.DemoWeb/BitFields.DemoWeb.csproj -c Release -o output + + # GitHub Pages needs .nojekyll to serve _framework directory + - name: Prepare for GitHub Pages + run: | + touch output/wwwroot/.nojekyll + cp output/wwwroot/index.html output/wwwroot/404.html + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: output/wwwroot + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 18a0d5f..02a5827 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,11 @@ # This .gitignore file was automatically created by Microsoft(R) Visual Studio. ################################################################################ -/Generators/bin -/Generators/obj -/obj -/bin -/Test/bin -/Test/obj +*.apikey + +# Build output +**/bin/ +**/obj/ # Test results (exclude test run artifacts, but keep generated source files for inspection) /Test/TestResults/*.trx @@ -16,4 +15,8 @@ # NuGet packages (don't commit built packages) /nupkg/ -/.vs + +# Visual Studio +/.vs/ +**/.vs/ +/OSSRequestForm-v4.xlsx diff --git a/BITFIELD.md b/BITFIELD.md deleted file mode 100644 index 9ab97cb..0000000 --- a/BITFIELD.md +++ /dev/null @@ -1,684 +0,0 @@ -# BitField Source Generator - -Type-safe, high-performance bitfield manipulation for hardware register emulation. - -## Quick Start - -```csharp -[BitFields(typeof(ushort))] -public partial struct MyRegister -{ - [BitField(0, 6)] public partial byte KeyCode { get; set; } // bits 0..=6 (7 bits) - [BitFlag(7)] public partial bool KeyUp { get; set; } - [BitField(8, 14)] public partial byte SecondKey { get; set; } // bits 8..=14 (7 bits) - [BitFlag(15)] public partial bool SecondKeyUp { get; set; } -} - -// Usage - use implicit conversion or constructor -MyRegister reg = 0xFFFF; // Implicit conversion from ushort -var reg2 = new MyRegister(0x1234); // Constructor -reg.KeyUp = false; -ushort raw = reg; // Implicit conversion to ushort -``` - -The generator creates: -- A private `Value` field of the specified storage type -- A constructor taking the storage type -- Property implementations with inline bit manipulation -- Static `{Name}Bit` properties for each `[BitFlag]` -- Static `{Name}Mask` properties for each `[BitField]` -- Fluent `With{Name}` methods for each property -- Arithmetic operators: `+`, `-`, `*`, `/`, `%`, unary `-` -- Bitwise operators: `|`, `&`, `^`, `~` -- Shift operators: `<<`, `>>`, `>>>` -- Comparison operators: `<`, `<=`, `>`, `>=` -- Equality operators: `==`, `!=` -- Mixed-type operators (struct with storage type) -- Implicit conversion operators to/from the storage type -- Parsing support via `IParsable` and `ISpanParsable` interfaces -- Formatting support via `IFormattable` and `ISpanFormattable` interfaces -- Comparison interfaces: `IComparable`, `IComparable`, `IEquatable` - -## Attributes - -| Attribute | Usage | Description | -|-----------|-------|-------------| -| `[BitFields(typeof(T))]` | Struct | Enables generation with storage type T | -| `[BitField(startBit, endBit)]` | Property | Multi-bit field (Rust-style inclusive range) | -| `[BitFlag(bit)]` | Property | Single-bit flag definition | - -**BitField Examples:** -- `[BitField(0, 2)]` - 3-bit field at bits 0, 1, 2 (like Rust's `0..=2`) -- `[BitField(4, 7)]` - 4-bit field at bits 4, 5, 6, 7 (like Rust's `4..=7`) -- `[BitField(3, 3)]` - 1-bit field at bit 3 only - -## Supported Storage Types - -| Storage | Bits | Signed Alternative | -|---------|------|-------------------| -| `byte` | 8 | `sbyte` | -| `ushort` | 16 | `short` | -| `uint` | 32 | `int` | -| `ulong` | 64 | `long` | - -## Parsing - -BitFields structs implement `IParsable` and `ISpanParsable` interfaces, allowing parsing from strings in multiple formats: - -```csharp -// Decimal parsing -MyRegister dec = MyRegister.Parse("255"); - -// Hexadecimal parsing (0x or 0X prefix) -MyRegister hex = MyRegister.Parse("0xFF"); -MyRegister hex2 = MyRegister.Parse("0XAB"); - -// Binary parsing (0b or 0B prefix) -MyRegister bin = MyRegister.Parse("0b11111111"); -MyRegister bin2 = MyRegister.Parse("0B10101010"); - -// C#-style underscore digit separators (all formats) -MyRegister d1 = MyRegister.Parse("1_000"); // Decimal: 1000 -MyRegister h1 = MyRegister.Parse("0xFF_00"); // Hex: 0xFF00 -MyRegister b1 = MyRegister.Parse("0b1111_0000"); // Binary: 0xF0 - -// TryParse pattern for safe parsing -if (MyRegister.TryParse("0b1010_1010", out var result)) -{ - Console.WriteLine($"Parsed: {result}"); // 0xAA -} - -// With IFormatProvider for culture-specific parsing -var reg3 = MyRegister.Parse("42", CultureInfo.InvariantCulture); - -// ReadOnlySpan overloads for performance -var span = "0xFF".AsSpan(); -var reg4 = MyRegister.Parse(span, null); -``` - -**Supported Formats:** -- Decimal: `"255"`, `"1234"`, `"1_000_000"` -- Hexadecimal with prefix: `"0xFF"`, `"0XFF"`, `"0x1234_ABCD"` -- Binary with prefix: `"0b1010"`, `"0B1111_0000"` - -## Formatting - -BitFields structs implement `IFormattable` and `ISpanFormattable` interfaces: - -```csharp -MyRegister value = 0xAB; - -// Standard format strings -string hex = value.ToString("X2", null); // "AB" -string dec = value.ToString("D", null); // "171" - -// Default ToString returns hex -string str = value.ToString(); // "0xAB" - -// Allocation-free formatting with Span -Span buffer = stackalloc char[10]; -if (value.TryFormat(buffer, out int written, "X4", null)) -{ - // buffer[..written] contains "00AB" -} -``` - -## Performance - -Benchmarks show the generated code performs within **1%** of hand-coded bit manipulation. All bit manipulation uses **compile-time constants** for zero runtime overhead. - -## Examples - -### VIA Register (8-bit) - -```csharp -[BitFields(typeof(byte))] -public partial struct ViaRegB -{ - [BitField(0, 2)] public partial byte SoundVolume { get; set; } // bits 0..=2 (3 bits) - [BitFlag(3)] public partial bool SoundBuffer { get; set; } - [BitFlag(4)] public partial bool OverlayRom { get; set; } - [BitFlag(5)] public partial bool HeadSelect { get; set; } - [BitFlag(6)] public partial bool VideoPage { get; set; } - [BitFlag(7)] public partial bool SccAccess { get; set; } -} -``` - -### ADB Keyboard Register (16-bit) - -```csharp -[BitFields(typeof(ushort))] -public partial struct KeyboardReg0 -{ - [BitField(0, 6)] public partial byte SecondKeyCode { get; set; } // bits 0..=6 (7 bits) - [BitFlag(7)] public partial bool SecondKeyUp { get; set; } - [BitField(8, 14)] public partial byte FirstKeyCode { get; set; } // bits 8..=14 (7 bits) - [BitFlag(15)] public partial bool FirstKeyUp { get; set; } -} -``` - -### 64-bit Status Register - -```csharp -[BitFields(typeof(ulong))] -public partial struct StatusReg64 -{ - [BitField(0, 7)] public partial byte Status { get; set; } // bits 0..=7 (8 bits) - [BitField(8, 23)] public partial ushort DataWord { get; set; } // bits 8..=23 (16 bits) - [BitField(24, 55)] public partial uint Address { get; set; } // bits 24..=55 (32 bits) - [BitFlag(56)] public partial bool Enable { get; set; } - [BitFlag(57)] public partial bool Ready { get; set; } - [BitFlag(58)] public partial bool Error { get; set; } -} -``` - -### Signed Storage Type (32-bit) - -For hardware registers that use signed values: - -```csharp -[BitFields(typeof(int))] -public partial struct SignedReg32 -{ - [BitFlag(0)] public partial bool Flag0 { get; set; } - [BitFlag(31)] public partial bool Sign { get; set; } // Sign bit - [BitField(1, 15)] public partial ushort LowWord { get; set; } // bits 1..=15 (15 bits) - [BitField(16, 30)] public partial ushort HighWord { get; set; } // bits 16..=30 (15 bits) -} -``` - -## Generated Code - -For the following user-defined struct: - -```csharp -[BitFields(typeof(byte))] -public partial struct StatusRegister -{ - [BitFlag(0)] public partial bool Ready { get; set; } - [BitFlag(1)] public partial bool Error { get; set; } - [BitField(2, 4)] public partial byte Mode { get; set; } // bits 2..=4 (3 bits) -} -``` - -The generator creates the following complete implementation: - -```csharp -public partial struct StatusRegister -{ - // ═══════════════════════════════════════════════════════════════════ - // Storage - // ═══════════════════════════════════════════════════════════════════ - - private byte Value; - - /// Creates a new StatusRegister with the specified raw value. - public StatusRegister(byte value) { Value = value; } - - // ═══════════════════════════════════════════════════════════════════ - // BitFlag Properties (single-bit) - // ═══════════════════════════════════════════════════════════════════ - - public partial bool Ready - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => (Value & 0x01) != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => Value = value ? (byte)(Value | 0x01) : (byte)(Value & 0xFE); - } - - public partial bool Error - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => (Value & 0x02) != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => Value = value ? (byte)(Value | 0x02) : (byte)(Value & 0xFD); - } - - // ═══════════════════════════════════════════════════════════════════ - // BitField Properties (multi-bit) - // ═══════════════════════════════════════════════════════════════════ - - public partial byte Mode - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => (byte)((Value >> 2) & 0x07); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => Value = (byte)((Value & 0xE3) | ((value << 2) & 0x1C)); - } - - // ═══════════════════════════════════════════════════════════════════ - // Static Bit Properties (for BitFlags) - // Returns a struct with only the specified bit set - // ═══════════════════════════════════════════════════════════════════ - - /// Returns a StatusRegister with only the Ready bit set. - public static StatusRegister ReadyBit => new(0x01); - - /// Returns a StatusRegister with only the Error bit set. - public static StatusRegister ErrorBit => new(0x02); - - // ═══════════════════════════════════════════════════════════════════ - // Static Mask Properties (for BitFields) - // Returns a struct with the mask for the specified field - // ═══════════════════════════════════════════════════════════════════ - - /// Returns a StatusRegister with the mask for the Mode field (bits 2-4). - public static StatusRegister ModeMask => new(0x1C); - - // ═══════════════════════════════════════════════════════════════════ - // Fluent With{Name} Methods - // Returns a new struct with the specified value changed - // ═══════════════════════════════════════════════════════════════════ - - /// Returns a new StatusRegister with the Ready flag set to the specified value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public StatusRegister WithReady(bool value) => - new(value ? (byte)(Value | 0x01) : (byte)(Value & 0xFE)); - - /// Returns a new StatusRegister with the Error flag set to the specified value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public StatusRegister WithError(bool value) => - new(value ? (byte)(Value | 0x02) : (byte)(Value & 0xFD)); - - /// Returns a new StatusRegister with the Mode field set to the specified value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public StatusRegister WithMode(byte value) => - new((byte)((Value & 0xE3) | ((value << 2) & 0x1C))); - - // ═══════════════════════════════════════════════════════════════════ - // Bitwise Operators - // ═══════════════════════════════════════════════════════════════════ - - /// Bitwise complement operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator ~(StatusRegister a) => new((byte)~a.Value); - - /// Bitwise OR operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator |(StatusRegister a, StatusRegister b) => - new((byte)(a.Value | b.Value)); - - /// Bitwise AND operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator &(StatusRegister a, StatusRegister b) => - new((byte)(a.Value & b.Value)); - - /// Bitwise XOR operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator ^(StatusRegister a, StatusRegister b) => - new((byte)(a.Value ^ b.Value)); - - // ═══════════════════════════════════════════════════════════════════ - // Mixed-Type Bitwise Operators (struct with storage type) - // ═══════════════════════════════════════════════════════════════════ - - /// Bitwise OR with storage type (struct | byte). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator |(StatusRegister a, byte b) => - new((byte)(a.Value | b)); - - /// Bitwise OR with storage type (byte | struct). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator |(byte a, StatusRegister b) => - new((byte)(a | b.Value)); - - /// Bitwise AND with storage type (struct & byte). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator &(StatusRegister a, byte b) => - new((byte)(a.Value & b)); - - /// Bitwise AND with storage type (byte & struct). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator &(byte a, StatusRegister b) => - new((byte)(a & b.Value)); - - /// Bitwise XOR with storage type (struct ^ byte). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator ^(StatusRegister a, byte b) => - new((byte)(a.Value ^ b)); - - - /// Bitwise XOR with storage type (byte ^ struct). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator ^(byte a, StatusRegister b) => - new((byte)(a ^ b.Value)); - - // ═══════════════════════════════════════════════════════════════════ - // Arithmetic Operators - // ═══════════════════════════════════════════════════════════════════ - - /// Addition operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator +(StatusRegister a, StatusRegister b) => - new(unchecked((byte)(a.Value + b.Value))); - - /// Subtraction operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator -(StatusRegister a, StatusRegister b) => - new(unchecked((byte)(a.Value - b.Value))); - - /// Unary negation operator (two's complement). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator -(StatusRegister a) => - new(unchecked((byte)(0 - a.Value))); - - // ... plus *, /, % and mixed-type overloads - - // ═══════════════════════════════════════════════════════════════════ - // Shift Operators - // ═══════════════════════════════════════════════════════════════════ - - /// Left shift operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator <<(StatusRegister a, int b) => - new(unchecked((byte)(a.Value << b))); - - /// Right shift operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator >>(StatusRegister a, int b) => - new(unchecked((byte)(a.Value >> b))); - - /// Unsigned right shift operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusRegister operator >>>(StatusRegister a, int b) => - new(unchecked((byte)(a.Value >>> b))); - - // ═══════════════════════════════════════════════════════════════════ - // Comparison Operators - // ═══════════════════════════════════════════════════════════════════ - - /// Less than operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator <(StatusRegister a, StatusRegister b) => a.Value < b.Value; - - /// Greater than operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator >(StatusRegister a, StatusRegister b) => a.Value > b.Value; - - /// Less than or equal operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator <=(StatusRegister a, StatusRegister b) => a.Value <= b.Value; - - /// Greater than or equal operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator >=(StatusRegister a, StatusRegister b) => a.Value >= b.Value; - - // ═══════════════════════════════════════════════════════════════════ - // Equality Operators - // ═══════════════════════════════════════════════════════════════════ - - /// Equality operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(StatusRegister a, StatusRegister b) => a.Value == b.Value; - - /// Inequality operator. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(StatusRegister a, StatusRegister b) => a.Value != b.Value; - - /// Determines whether this instance equals another object. - public override bool Equals(object? obj) => obj is StatusRegister other && Value == other.Value; - - /// Returns the hash code for this instance. - public override int GetHashCode() => Value.GetHashCode(); - - /// Returns the hexadecimal string representation of the value. - public override string ToString() => $"0x{Value:X2}"; - - // ═══════════════════════════════════════════════════════════════════ - // Implicit Conversion Operators - // ═══════════════════════════════════════════════════════════════════ - - /// Implicit conversion to storage type. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator byte(StatusRegister value) => value.Value; - - /// Implicit conversion from storage type. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator StatusRegister(byte value) => new(value); - - // ═══════════════════════════════════════════════════════════════════ - // Interface Implementations (abbreviated) - // ═══════════════════════════════════════════════════════════════════ - - // IComparable - public int CompareTo(StatusRegister other) => Value.CompareTo(other.Value); - - // IEquatable - public bool Equals(StatusRegister other) => Value == other.Value; - - // IFormattable - public string ToString(string? format, IFormatProvider? formatProvider) => - Value.ToString(format, formatProvider); - - // ISpanFormattable - public bool TryFormat(Span destination, out int charsWritten, - ReadOnlySpan format, IFormatProvider? provider) => - Value.TryFormat(destination, out charsWritten, format, provider); - - // IParsable and ISpanParsable - Parse/TryParse methods - // (supports decimal, 0x hex, 0b binary, with underscores) -} -``` - - -## Operators and Static Properties - -### Static Bit Properties - -For each `[BitFlag]` property, a static `{Name}Bit` property is generated that returns a struct with only that bit set: - -```csharp -// Instead of: -IFRFields bit = 0; -bit.CA1_Vbl = true; -ClearInterrupt(bit); - -// Use the static Bit property: -ClearInterrupt(IFRFields.CA1_VblBit); - -// Combine multiple flags: -var flags = IFRFields.CA1_VblBit | IFRFields.CA2_RtcBit; -``` - -### Static Mask Properties - -For each `[BitField]` property, a static `{Name}Mask` property is generated that returns the mask for that field: - -```csharp -// Clear a multi-bit field: -var reg = ~RegAFields.SoundMask & someValue; - -// Check if any bits in a field are set: -if ((value & RegAFields.SoundMask) != 0) { ... } -``` - -### Arithmetic Operators - -Full support for arithmetic operations: - -```csharp -MyRegister a = 100; -MyRegister b = 50; - -// Binary arithmetic operators -var sum = a + b; // Addition: 150 -var diff = a - b; // Subtraction: 50 -var prod = a * 2; // Multiplication (with storage type) -var quot = a / b; // Division: 2 -var rem = a % 3; // Modulus - -// Unary operators -var pos = +a; // Unary plus (returns same value) -var neg = -a; // Unary negation (two's complement) - -// Mixed-type operations (struct with storage type) -var mixed = a + (byte)10; -var mixed2 = (byte)5 + b; -``` - -### Bitwise Operators - -Full support for bitwise operations without casting: - - - -```csharp -IFRFields a = 0x01; -IFRFields b = 0x02; - -// Binary operators (return struct type) -var or = a | b; // Bitwise OR -var and = a & b; // Bitwise AND -var xor = a ^ b; // Bitwise XOR -var inv = ~a; // Bitwise complement - -// Mixed-type operators -var mixed = a | (byte)0x80; -var mixed2 = (byte)0x40 | b; - -// Complex expressions -var result = IFR & ~IFRFields.EnableBit; // Clear a flag -``` - -### Shift Operators - -```csharp -MyRegister a = 0x0F; - -var shl = a << 4; // Left shift: 0xF0 -var shr = a >> 2; // Right shift: 0x03 -var ushr = a >>> 1; // Unsigned right shift (C# 11+) -``` - -### Comparison Operators - -```csharp -MyRegister a = 100; -MyRegister b = 200; - -bool lt = a < b; // Less than: true -bool le = a <= b; // Less than or equal: true -bool gt = a > b; // Greater than: false -bool ge = a >= b; // Greater than or equal: false - -// Useful for bounds checking -if (reg < MyRegister.ModeMask) { ... } -``` - -### Equality Operators - -```csharp -IFRFields a = 0x42; -IFRFields b = 0x42; - -if (a == b) { ... } // Equality -if (a != b) { ... } // Inequality -a.Equals(b); // Object equality -a.GetHashCode(); // Hash code -a.ToString(); // Returns "0x42" -``` - -## Interface Implementations - -Every BitFields type automatically implements the following interfaces: - -| Interface | Purpose | -|-----------|---------| -| `IComparable` | Non-generic comparison for sorting | -| `IComparable` | Generic comparison | -| `IEquatable` | Value equality | -| `IFormattable` | Format string support (`ToString("X2", null)`) | -| `ISpanFormattable` | Allocation-free formatting | -| `IParsable` | String parsing | -| `ISpanParsable` | Span-based parsing | - -```csharp -// IComparable - sorting -var registers = new[] { reg3, reg1, reg2 }; -Array.Sort(registers); // Sorts by underlying value - -// IEquatable - efficient equality -bool equal = reg1.Equals(reg2); - -// IComparable - comparison -int cmp = reg1.CompareTo(reg2); // -1, 0, or 1 -``` - -## Fluent With{Name} Methods (v0.8.0+) - -### The Struct-as-Property Problem - -When a BitFields struct is exposed as a property, direct setter calls don't work: - -```csharp -public class MyClass -{ - public IFRFields IFR { get; set; } -} - -// This modifies a COPY, not the original! -obj.IFR.Ready = true; // Compiles but doesn't work -``` - -This is a fundamental C# behavior: property getters return a copy of the struct. - -### Solution: With{Name} Methods - -For each property, a `With{Name}` method is generated that returns a new struct and does NOT modify the value of the current struct: - -```csharp -// This works correctly -obj.IFR = obj.IFR.WithReady(true); - -// Chain multiple changes -obj.IFR = obj.IFR.WithReady(true).WithError(false).WithMode(5); -``` - -### Examples - -```csharp -// BitFlags -IFRFields flags = 0; -flags = flags.WithReady(true); // Set flag -flags = flags.WithReady(false); // Clear flag - -// BitFields (multi-bit) -RegAFields reg = 0; -reg = reg.WithSound(5); // Set 3-bit field to 5 -reg = reg.WithPriority(2); // Set 2-bit field to 2 - -// Chaining -var result = reg - .WithSound(7) - .WithPage2(true) - .WithDriveSel(false); - -// With properties -container.Status = container.Status.WithReady(true).WithMode(3); -``` - -### Alternative: Bitwise Operators - -For simple flag operations, bitwise operators also work with properties: - -```csharp -// Set a flag -obj.IFR = obj.IFR | IFRFields.ReadyBit; - -// Clear a flag -obj.IFR = obj.IFR & ~IFRFields.ReadyBit; - -// Toggle a flag -obj.IFR = obj.IFR ^ IFRFields.ReadyBit; -``` - -### Summary: Choosing the Right Approach - -| Scenario | Recommended Approach | -|----------|---------------------| -| Struct is a local variable | Direct setter: `reg.Ready = true` | -| Struct is a property | With method: `obj.IFR = obj.IFR.WithReady(true)` | -| Setting multiple values | Chain: `reg = reg.WithStatus(1).WithMode(2).WithReady(true)` | -| | Bitwise chaining: `reg = (reg | IFRFields.ReadyBit) & ~IFRFields.ErrorBit` | -| Simple flag set/clear | Bitwise: `obj.IFR = obj.IFR | IFRFields.ReadyBit` | diff --git a/BITFIELDS.md b/BITFIELDS.md new file mode 100644 index 0000000..9aae120 --- /dev/null +++ b/BITFIELDS.md @@ -0,0 +1,1876 @@ +# BitFields + +Source-generated, type-safe, zero-overhead bit manipulation for .NET. + +Two attributes, one concept: define bit-level fields with `[BitField]` and `[BitFlag]`, then choose your backing strategy. + +| Attribute | Backing | Best for | +|-----------|---------|----------| +| `[BitFields]` | Value type (`byte`, `ushort`, `uint`, `nuint`, ...) | Hardware registers, opcodes, small bit-packed structs | +| `[BitFieldsView]` | `Memory` (zero-copy buffer view) | Network packets, file headers, DMA buffers | + +Both share the same `[BitField(start, end)]` and `[BitFlag(bit)]` attributes. Learn one, use both. + +## Table of Contents + +**Getting Started** +- [Quick Start -- BitFields (Value Type)](#quick-start----bitfields-value-type) +- [Quick Start -- BitFieldsView (Buffer View)](#quick-start----bitfieldsview-buffer-view) +- [Choosing Between BitFields and BitFieldsView](#choosing-between-bitfields-and-bitfieldsview) + +**Shared Concepts** +- [Attributes](#attributes) +- [Byte Order and Bit Order](#byte-order-and-bit-order) +- [Signed Property Types (Sign Extension)](#signed-property-types-sign-extension) + +**BitFields (Value Type)** +- [Supported Storage Types](#supported-storage-types) +- [Native Integer Types (nint / nuint)](#native-integer-types-nint--nuint) +- [Operators](#operators) +- [Parsing and Formatting](#parsing-and-formatting) +- [Static Bit and Mask Properties](#static-bit-and-mask-properties) +- [Fluent With Methods](#fluent-with-methods) +- [Undefined and Reserved Bits](#undefined-and-reserved-bits) +- [Interface Implementations](#interface-implementations) + +**BitFieldsView (Buffer View)** +- [Constructors](#constructors) +- [Zero-Copy Semantics](#zero-copy-semantics) +- [Generated Members](#generated-members) +- [Record Struct Equality](#record-struct-equality) + +**Composition** +- [BitFields Inside BitFields](#bitfields-inside-bitfields) +- [BitFields Inside BitFieldsView](#bitfields-inside-bitfieldsview) +- [Sub-View Nesting](#sub-view-nesting) +- [Mixed-Endian Nesting](#mixed-endian-nesting) + +**Pre-Defined Types** +- [Numeric Decomposition Types](#numeric-decomposition-types) + +**Real-World Examples** +- [Hardware Registers](#hardware-registers) +- [Network Protocol Headers (RFC)](#network-protocol-headers-rfc) +- [Parsing a Captured Network Packet](#parsing-a-captured-network-packet) +- [Mixed-Endian Capture File](#mixed-endian-capture-file) + +**Visualization** +- [RFC Diagram Generator](#rfc-diagram-generator) + - [Instance API](#instance-api) + - [Static Type-Based API](#static-type-based-api) + - [Static Field-Based API](#static-field-based-api) +- [Demo Application](#demo-application) + +**Reference** +- [Performance](#performance) +- [Generated Code Listing](#generated-code-listing) + +--- + +## Quick Start -- BitFields (Value Type) + +```csharp +[BitFields(StorageType.UInt16)] // StorageType enum -- preferred +public partial struct KeyboardReg +{ + [BitField(0, 6)] public partial byte KeyCode { get; set; } // bits 0..=6 (7 bits) + [BitFlag(7)] public partial bool KeyUp { get; set; } + [BitField(8, 14)] public partial byte SecondKey { get; set; } // bits 8..=14 (7 bits) + [BitFlag(15)] public partial bool SecondKeyUp { get; set; } +} + +KeyboardReg reg = 0xFFFF; // implicit conversion from ushort +reg.KeyUp = false; +ushort raw = reg; // implicit conversion to ushort +``` + +The `StorageType` enum is the recommended way to specify the backing type. IntelliSense +autocomplete shows all supported types as you type, so you discover valid choices instantly +without consulting documentation. If you choose an unsupported type via the older `typeof(T)` +form, the error is only discovered after writing the struct body and building, which can mean +significant rework. The `StorageType` enum moves that validation to the moment you write the +attribute -- making development faster, less error prone, and reducing late-stage surprises. + +The `typeof(T)` form is still fully supported for backward compatibility: + +```csharp +[BitFields(typeof(ushort))] // typeof(T) -- also valid +public partial struct KeyboardReg { ... } +``` + +`[BitFields]` generates a value type with inline bit manipulation, full operator support, +parsing, formatting, and implicit conversions. Zero heap allocations, zero abstraction penalty. + +## Quick Start -- BitFieldsView (Buffer View) + +```csharp +[BitFieldsView(ByteOrder.BigEndian, BitOrder.BitZeroIsMsb)] +public partial record struct IPv4HeaderView +{ + [BitField(0, 3)] public partial byte Version { get; set; } + [BitField(4, 7)] public partial byte Ihl { get; set; } + [BitField(16, 31)] public partial ushort TotalLength { get; set; } + [BitField(96, 127)] public partial uint SourceAddress { get; set; } + [BitField(128, 159)] public partial uint DestinationAddress { get; set; } +} + +byte[] packet = ReceiveFromNetwork(); +var header = new IPv4HeaderView(packet); +byte version = header.Version; // reads directly from packet buffer +header.TotalLength = 52; // writes directly to packet buffer +``` + +`[BitFieldsView]` generates a `record struct` that wraps `Memory`. All reads and writes go +directly through the buffer -- no copies, no allocations. The struct-level `ByteOrder` controls +how multi-byte fields are serialized; plain `ushort` and `uint` properties are all you need. + +## Choosing Between BitFields and BitFieldsView + +| | `[BitFields]` | `[BitFieldsView]` | +|---|---|---| +| Backing | Private value field | `Memory` (external buffer) | +| Copy cost | Copies all data on assignment | Copies only the 24-byte view header | +| Max size | ~16 KB | Unlimited | +| Operators | Full arithmetic, bitwise, comparison | None (it is a view, not a value) | +| Conversions | Implicit to/from storage type | Constructor from `byte[]` / `Memory` | +| Use case | Registers, opcodes, flags | Network packets, file formats, DMA buffers | + +**Use `[BitFields]`** when the data is a small, self-contained value -- hardware registers, +instruction opcodes, status flags, or anything that fits in a primitive and benefits from +operators and implicit conversions. + +**Use `[BitFieldsView]`** when the data lives in an external buffer and you want zero-copy +access -- network packets, memory-mapped file headers, DMA buffers. + +**Use both together** when a protocol has small reusable flag groups embedded in larger +buffer-backed headers. See [Composition](#bitfields-inside-bitfieldsview). + +--- + +## Attributes + +Both `[BitFields]` and `[BitFieldsView]` use the same field attributes: + +| Attribute | Parameters | Description | +|-----------|------------|-------------| +| `[BitFields(StorageType.X)]` | `StorageType` enum, optional `UndefinedBitsMustBe`, optional `BitOrder` | Preferred. Enum provides IntelliSense discovery of all supported types | +| `[BitFields(typeof(T))]` | Storage type, optional `UndefinedBitsMustBe`, optional `BitOrder` | Also supported. Equivalent to the enum form; exists for backward compatibility | +| `[BitFieldsView]` | Optional `ByteOrder`, optional `BitOrder` | Marks a `partial record struct` for buffer-view generation | +| `[BitField(startBit, endBit)]` | Rust-style inclusive range, optional `MustBe` | Multi-bit field (width = endBit - startBit + 1) | +| `[BitFlag(bit)]` | 0-based bit position, optional `MustBe` | Single-bit boolean flag | + +**BitField range examples:** +- `[BitField(0, 2)]` -- 3-bit field at bits 0, 1, 2 (like Rust's `0..=2`) +- `[BitField(4, 7)]` -- 4-bit field at bits 4, 5, 6, 7 +- `[BitField(3, 3)]` -- 1-bit field at bit 3 only + +## Byte Order and Bit Order + +Both attributes support configurable byte order and bit numbering: + +| Setting | Default | Meaning | Typical use | +|---------|---------|---------|-------------| +| `ByteOrder.LittleEndian` | Yes | LSB first in memory | x86 registers, USB, PCI, PE files | +| `ByteOrder.BigEndian` | No | MSB first in memory | Network protocols, Java class files | +| `ByteOrder.NetworkEndian` | No | Synonym for `BigEndian` | Readability in protocol code | +| `BitOrder.BitZeroIsLsb` | Yes | Bit 0 = least significant | Hardware datasheets, x86 convention | +| `BitOrder.BitZeroIsMsb` | No | Bit 0 = most significant | RFCs, IETF specifications | + +For `[BitFields]`, only `BitOrder` applies (the value is stored in a native integer). +For `[BitFieldsView]`, both `ByteOrder` and `BitOrder` apply. + +**LSB-first bit layout** (default): +``` +Byte 0: bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 + 0x80 0x40 0x20 0x10 0x08 0x04 0x02 0x01 +``` + +**MSB-first bit layout** (RFC convention): +``` +Byte 0: bit0 bit1 bit2 bit3 bit4 bit5 bit6 bit7 + 0x80 0x40 0x20 0x10 0x08 0x04 0x02 0x01 +``` + +With MSB-first, bit positions in `[BitField]` attributes match RFC diagrams directly: + +```csharp +// RFC 791 IPv4 header, first 32 bits: +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |Version| IHL |Type of Service| Total Length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +[BitFieldsView(ByteOrder.BigEndian, BitOrder.BitZeroIsMsb)] +public partial record struct IPv4Word0 +{ + [BitField(0, 3)] public partial byte Version { get; set; } // matches RFC diagram + [BitField(4, 7)] public partial byte Ihl { get; set; } + [BitField(8, 15)] public partial byte TypeOfService { get; set; } + [BitField(16, 31)] public partial ushort TotalLength { get; set; } +} +``` + +## Signed Property Types (Sign Extension) + +When a property type is signed (`sbyte`, `short`, `int`, `long`), the generator automatically +sign-extends the extracted field value. The **property type** determines sign extension, not the +storage type: + +```csharp +[BitFields(typeof(ushort))] +public partial struct MotionRegister +{ + [BitField(13, 15)] public partial sbyte DeltaX { get; set; } // 3-bit signed: -4 to +3 + [BitField(10, 12)] public partial byte Speed { get; set; } // 3-bit unsigned: 0 to 7 +} + +MotionRegister reg = 0; +reg.DeltaX = -3; +Console.WriteLine(reg.DeltaX); // -3 (correctly sign-extended) +reg.Speed = 5; +Console.WriteLine(reg.Speed); // 5 (unsigned, stays positive) +``` + +| Field Width | Signed Range | Unsigned Range | +|-------------|--------------|----------------| +| 3 bits | -4 to +3 | 0 to 7 | +| 4 bits | -8 to +7 | 0 to 15 | +| 8 bits | -128 to +127 | 0 to 255 | + +--- + +## Supported Storage Types + +`[BitFields]` supports these storage types. The `StorageType` enum is the preferred way to +specify the backing type because IntelliSense shows all valid choices as you type, preventing +mistakes before they happen. The `typeof(T)` form is also supported for backward compatibility. + +| `StorageType` Enum | `typeof(T)` | Size | Notes | +|--------------------|-------------|------|-------| +| `StorageType.Byte` / `.SByte` | `typeof(byte)` / `typeof(sbyte)` | 8 bits | | +| `StorageType.UInt16` / `.Int16` | `typeof(ushort)` / `typeof(short)` | 16 bits | | +| `StorageType.UInt32` / `.Int32` | `typeof(uint)` / `typeof(int)` | 32 bits | | +| `StorageType.UInt64` / `.Int64` | `typeof(ulong)` / `typeof(long)` | 64 bits | | +| `StorageType.NUInt` / `.NInt` | `typeof(nuint)` / `typeof(nint)` | 32 or 64 bits | Platform-dependent; see [Native Integer Types](#native-integer-types-nint--nuint) | +| `StorageType.UInt128` / `.Int128` | `typeof(UInt128)` / `typeof(Int128)` | 128 bits | | +| `StorageType.Half` | `typeof(Half)` | 16 bits | IEEE 754 half-precision | +| `StorageType.Single` | `typeof(float)` | 32 bits | IEEE 754 single-precision | +| `StorageType.Double` | `typeof(double)` | 64 bits | IEEE 754 double-precision | +| `StorageType.Decimal` | `typeof(decimal)` | 128 bits | .NET decimal | +| `[BitFields(N)]` | `[BitFields(N)]` | N bits | Arbitrary width, 1 to 16,384 bits | + +Using `typeof(T)` with a type not in this table (for example, `typeof(Guid)`) produces +compiler error **SD0003**, which names the unsupported type and lists all valid alternatives. +The `StorageType` enum avoids this entirely because only valid values appear in IntelliSense. + +## Native Integer Types (nint / nuint) + +`nint` and `nuint` are platform-dependent native integer types: 32 bits wide in a 32-bit process, +64 bits wide in a 64-bit process. They are useful for memory-mapped registers, pointer-sized +bit-packed values, and other platform-width-sensitive structures. + +```csharp +// 32-bit safe: all fields fit within bits 0-31 on any platform +[BitFields(StorageType.NUInt)] +public partial struct PointerTagReg +{ + [BitField(0, 7)] public partial byte Tag { get; set; } // bits 0..=7 + [BitField(8, 11)] public partial byte Command { get; set; } // bits 8..=11 + [BitFlag(28)] public partial bool Enabled { get; set; } // bit 28 + [BitFlag(31)] public partial bool Valid { get; set; } // bit 31 +} + +// 64-bit only: uses high bits above 31 +[BitFields(StorageType.NUInt)] +public partial struct WideNativeReg +{ +#pragma warning disable SD0002 // High bits: only valid on 64-bit + [BitField(0, 7)] public partial byte Status { get; set; } // bits 0..=7 + [BitField(8, 23)] public partial ushort Data { get; set; } // bits 8..=23 + [BitField(24, 55)] public partial uint Address { get; set; } // bits 24..=55 + [BitFlag(56)] public partial bool Valid { get; set; } // bit 56 + [BitFlag(57)] public partial bool Ready { get; set; } // bit 57 +#pragma warning restore SD0002 +} +``` + +The generated struct includes a platform-dependent `SIZE_IN_BYTES` property (`nint.Size`) and +uses platform-branched serialization for byte span operations (`BinaryPrimitives.ReadUInt32LittleEndian` +on 32-bit, `BinaryPrimitives.ReadUInt64LittleEndian` on 64-bit). + +### Compiler Diagnostics for nint/nuint + +Because `nint`/`nuint` can be 32 bits on some platforms, the source generator performs +compile-time validation of all field and flag bit positions. If any field or flag accesses a bit +above bit 31, the generator emits a diagnostic whose severity depends on the project's +`PlatformTarget` setting: + +| Diagnostic | Severity | When Emitted | Meaning | +|------------|----------|--------------|---------| +| **SD0001** | **Error** | `PlatformTarget` is `x86` | The build is restricted to 32-bit. `nint`/`nuint` is always 32 bits, so bits 32+ are unreachable. The struct will corrupt data at runtime. | +| **SD0002** | **Warning** | `PlatformTarget` is `AnyCPU` (default) or unset | The binary may run on either 32-bit or 64-bit. Bits 32+ work on 64-bit but are silently unreachable on 32-bit, causing data loss. | +| *(none)* | | `PlatformTarget` is `x64` or `ARM64` | The build is restricted to 64-bit. `nint`/`nuint` is always 64 bits, so all bit positions are valid. | + +The diagnostic location points to the specific property declaration that exceeds the 32-bit boundary. +For multi-bit fields, the check uses the highest bit of the field (e.g., `[BitField(24, 55)]` checks +bit 55, not bit 24). + +**Resolving SD0001 (Error):** +- Move all fields to bits 0-31. +- Or change the storage type to `ulong`/`long` for a guaranteed 64-bit width. + +**Resolving SD0002 (Warning):** +- Set `x64` in your `.csproj` if the binary only runs on 64-bit. +- Or change the storage type to `ulong`/`long` for a fixed 64-bit width on all platforms. +- Or suppress the warning with `#pragma warning disable SD0002` if you have confirmed the binary + will only run on 64-bit processes. + +Non-native storage types (`byte`, `uint`, `ulong`, etc.) are never affected by these diagnostics. +Their bit widths are fixed regardless of platform. + +## Enum Property Types + +Property types can be enums for self-documenting code with zero runtime overhead: + +```csharp +public enum OpMode : byte { Idle = 0, Run = 1, Sleep = 2, Reset = 3 } + +[BitFields(typeof(byte))] +public partial struct ControlRegister +{ + [BitField(0, 1)] public partial OpMode Mode { get; set; } + [BitFlag(2)] public partial bool Enable { get; set; } +} + +ControlRegister reg = 0; +reg.Mode = OpMode.Run; +OpMode mode = reg.Mode; // OpMode.Run +``` + +Enum fields work at any bit position, including bit 0, and support fluent `With` methods: + +```csharp +[BitFields(typeof(byte))] +public partial struct CommandReg +{ + [BitField(0, 2)] public partial OpMode Command { get; set; } // enum at bit 0 + [BitField(3, 5)] public partial OpMode Status { get; set; } // enum at bit 3 + [BitField(6, 7)] public partial byte Flags { get; set; } +} + +var reg = CommandReg.Zero + .WithCommand(OpMode.Run) + .WithStatus(OpMode.Sleep) + .WithFlags(3); + +reg.Command; // OpMode.Run +reg.Status; // OpMode.Sleep +``` + +## Operators + +`[BitFields]` types are full-featured numeric types: + +```csharp +StatusRegister a = 0x42; +StatusRegister b = 0x18; + +// Arithmetic +var sum = a + b; var diff = a - b; var neg = -a; + +// Bitwise +var or = a | b; var and = a & b; var not = ~a; + +// Shift (returns int for small types, enabling: (bits >> n) & 1) +int bit = (a >> 2) & 1; + +// Comparison and equality +bool lt = a < b; bool eq = a == b; +``` + +For small types (`byte`, `ushort`), shift operators return `int` for intuitive bit extraction. +For larger types (`int`, `uint`, `long`, `ulong`), shift operators return the BitFields type. + +### Shift-and-Mask Pattern + +The `int` return type for small-type shifts enables the classic bit-extraction idiom: + +```csharp +[BitFields(typeof(byte))] +public partial struct MyReg { /* ... */ } + +MyReg bits = 0b0000_1110; + +int lsb = (bits >> 1) & 1; // Gets bit 1: result = 1 +int bit2 = (bits >> 2) & 1; // Gets bit 2: result = 1 +int bit0 = bits & 1; // Gets bit 0: result = 0 + +// Assign the result back (implicit int -> MyReg truncates to storage type) +MyReg extracted = (bits >> 1) & 0x07; +``` + +## Parsing and Formatting + +```csharp +// Parse from multiple formats +StatusRegister dec = StatusRegister.Parse("255"); +StatusRegister hex = StatusRegister.Parse("0xFF"); +StatusRegister bin = StatusRegister.Parse("0b1111_0000"); + +// Format +StatusRegister value = 0xAB; +value.ToString(); // "0xAB" +value.ToString("X2", null); // "AB" + +// Allocation-free +Span buf = stackalloc char[10]; +value.TryFormat(buf, out int written, "X4", null); +``` + +For `Half`, `float`, `double`, and `decimal` storage types, parsing and formatting +use the native numeric formatters instead of integer hex/binary parsing. + +## Static Bit and Mask Properties + +```csharp +// For [BitFlag(0)] Ready -- a value with only that bit set +StatusRegister readyBit = StatusRegister.ReadyBit; // 0x01 + +// For [BitField(2, 4)] Mode -- the mask covering that field +StatusRegister modeMask = StatusRegister.ModeMask; // 0x1C + +// Use for testing and masking +if ((status & StatusRegister.ReadyBit) != 0) { /* ready */ } +var flags = StatusRegister.ReadyBit | StatusRegister.ErrorBit; +``` + +## Fluent With Methods + +Solve the struct-as-property problem with immutable-style updates: + +```csharp +// Direct setters work on local variables +StatusRegister reg = 0; +reg.Ready = true; + +// But not on properties (C# returns a copy) +// obj.Status.Ready = true; // compiles but doesn't work + +// Use With methods instead +obj.Status = obj.Status.WithReady(true).WithMode(5); +``` + +## Undefined and Reserved Bits + +Control how bits not covered by any field are handled: + +```csharp +[BitFields(typeof(ushort), UndefinedBitsMustBe.Zeroes)] +public partial struct CleanHeader +{ + [BitField(0, 3)] public partial byte TypeCode { get; set; } + [BitField(4, 8)] public partial byte Flags { get; set; } + // Bits 9-15: always forced to zero +} + +CleanHeader h = 0xFFFF; +ushort raw = h; // 0x01FF (undefined bits masked off) +``` + +| Mode | Behavior | Use case | +|------|----------|----------| +| `.Any` (default) | Preserved as-is | Hardware registers | +| `.Zeroes` | Masked to zero | Protocol headers, serialization | +| `.Ones` | Set to one | Reserved-high protocols | + +Individual fields can also override with `MustBe`: + +```csharp +[BitFlag(7, MustBe.One)] public partial bool Sync { get; set; } // always 1 +[BitField(1, 3, MustBe.Zero)] public partial byte Reserved { get; set; } // always 0 +``` + +`MustBe` constraints are enforced at every entry point -- construction, implicit conversion, +operators, `With` methods, `Parse`, and `ReadFrom`. This guarantee holds regardless of how the +raw value is produced: + +```csharp +[BitFields(typeof(byte))] +public partial struct SyncedReg +{ + [BitFlag(0)] public partial bool Active { get; set; } + [BitField(1, 2, MustBe.Zero)] public partial byte Reserved { get; set; } + [BitField(3, 6)] public partial byte Data { get; set; } + [BitFlag(7, MustBe.One)] public partial bool Sync { get; set; } +} + +SyncedReg reg = 0x00; +byte raw = reg; // 0x80 (Sync forced to 1) + +reg = 0xFF; +raw = reg; // 0xF9 (Reserved bits 1-2 cleared, Sync stays 1) + +reg.Reserved = 3; // setter ignores the value -- bits stay 0 +reg.Sync = false; // setter ignores the value -- bit stays 1 + +var r = ~reg; // complement goes through constructor -- constraints re-applied +var s = reg | (SyncedReg)0x06; // OR result normalized -- Reserved stays 0 + +// Span round-trip also enforces constraints +byte[] buf = [0xFF]; +var fromSpan = new SyncedReg(new ReadOnlySpan(buf)); +byte spanRaw = fromSpan; // 0xF9 (same normalization) + +// Parse enforces too +var parsed = SyncedReg.Parse("0xFF", null); +byte parsedRaw = parsed; // 0xF9 +``` + +For `MustBe.Zero` flags, the getter always returns `false`. For `MustBe.One` flags, it always +returns `true`. Both can be freely combined with `UndefinedBitsMustBe`: + +```csharp +[BitFields(typeof(byte), UndefinedBitsMustBe.Zeroes)] +public partial struct ProtocolByte +{ + [BitField(0, 2)] public partial byte Flags { get; set; } // normal field + [BitFlag(3, MustBe.One)] public partial bool AlwaysHigh { get; set; } // forced to 1 + // Bits 4-7: undefined, forced to 0 by UndefinedBitsMustBe.Zeroes +} + +ProtocolByte p = 0xFF; +byte raw = p; // 0x0F (bits 4-7 zeroed, bit 3 set, Flags preserved) + +p = 0x00; +raw = p; // 0x08 (only AlwaysHigh forced on) +``` + +Sparse undefined bits (gaps between fields) are handled correctly: + +```csharp +[BitFields(typeof(sbyte), UndefinedBitsMustBe.Zeroes)] +public partial struct SparseReg +{ + // bit 0: UNDEFINED + [BitField(1, 2)] public partial byte LowField { get; set; } + // bit 3: UNDEFINED + [BitField(4, 6)] public partial byte HighField { get; set; } + // bit 7: UNDEFINED +} + +SparseReg reg = unchecked((sbyte)-1); // try to set all bits +sbyte raw = reg; // 0x76 (only defined bits survive) +``` + +## Nested Structs + +BitFields structs can be nested inside classes or other structs. Containing types must be `partial`: + +```csharp +public partial class HardwareController +{ + [BitFields(typeof(ushort))] + public partial struct StatusRegister + { + [BitFlag(0)] public partial bool Ready { get; set; } + [BitField(8, 15)] public partial byte ErrorCode { get; set; } + } + + private StatusRegister _status; + public bool IsReady => _status.Ready; +} +``` + +## Interface Implementations + +Every `[BitFields]` type implements: + +| Interface | Purpose | +|-----------|---------| +| `IComparable`, `IComparable` | Sorting and comparison | +| `IEquatable` | Value equality | +| `IFormattable`, `ISpanFormattable` | Format string support | +| `IParsable`, `ISpanParsable` | String and span parsing | + +--- + +## Constructors + +`[BitFieldsView]` generates three constructors: + +```csharp +var view = new IPv4HeaderView(buffer.AsMemory()); // from Memory +var view = new IPv4HeaderView(packetBytes); // from byte[] +var view = new IPv4HeaderView(frameBytes, 14); // from byte[] with offset +``` + +All constructors validate that the buffer contains at least `SizeInBytes` bytes. + +## Zero-Copy Semantics + +The view holds a reference to the original buffer. All property access goes through that reference: + +```csharp +byte[] buffer = new byte[20]; +var view = new IPv4HeaderView(buffer); + +view.Version = 4; +Console.WriteLine(buffer[0]); // 0x40 -- buffer modified directly + +buffer[0] = 0x60; +Console.WriteLine(view.Version); // 6 -- view reads live data +``` + +Two views over the same buffer see each other's writes: + +```csharp +var v1 = new IPv4HeaderView(buffer); +var v2 = new IPv4HeaderView(buffer); +v1.Version = 4; +Console.WriteLine(v2.Version); // 4 +``` + +## Generated Members + +For each `[BitFieldsView]` struct, the generator produces: + +| Member | Description | +|--------|-------------| +| `_data` | Private `Memory` field | +| Constructors | `Memory`, `byte[]`, `byte[] + offset` | +| `Data` property | Exposes the underlying `Memory` | +| `SizeInBytes` constant | Minimum buffer size required | +| Property accessors | Inline `BinaryPrimitives` reads/writes with `AggressiveInlining` | + +## Record Struct Equality + +Two views are equal if they reference the same segment of the same array: + +```csharp +var v1 = new IPv4HeaderView(buffer); +var v2 = new IPv4HeaderView(buffer); +Console.WriteLine(v1 == v2); // True + +var v3 = new IPv4HeaderView(new byte[20]); +Console.WriteLine(v1 == v3); // False -- different arrays +``` + +--- + +## BitFields Inside BitFields + +BitFields types can be used as property types within other BitFields, enabling reusable sub-structures: + +```csharp +[BitFields(StorageType.Byte)] +public partial struct StatusFlags +{ + [BitFlag(0)] public partial bool Ready { get; set; } + [BitFlag(1)] public partial bool Error { get; set; } + [BitField(4, 7)] public partial byte Priority { get; set; } +} + +[BitFields(StorageType.UInt16)] +public partial struct ProtocolHeader +{ + [BitField(0, 7)] public partial StatusFlags Status { get; set; } // embedded! + [BitField(8, 15)] public partial byte Length { get; set; } +} + +ProtocolHeader header = 0; +header.Status = new StatusFlags { Ready = true, Priority = 5 }; +bool ready = header.Status.Ready; // true +``` + +## BitFields Inside BitFieldsView + +`[BitFields]` types work as property types inside `[BitFieldsView]`. The implicit conversions +handle packing and unpacking automatically: + +```csharp +[BitFields(StorageType.Byte)] +public partial struct StatusFlags +{ + [BitFlag(0)] public partial bool Active { get; set; } + [BitFlag(1)] public partial bool Valid { get; set; } + [BitField(4, 7)] public partial byte Code { get; set; } +} + +[BitFieldsView] +public partial record struct PacketView +{ + [BitField(0, 7)] public partial StatusFlags Flags { get; set; } + [BitField(8, 15)] public partial byte Payload { get; set; } +} + +byte[] buffer = new byte[2]; +var view = new PacketView(buffer); +view.Flags = new StatusFlags { Active = true, Code = 5 }; +Console.WriteLine(view.Flags.Active); // True +Console.WriteLine(view.Flags.Code); // 5 +``` + +## Sub-View Nesting + +`[BitFieldsView]` types can be nested inside other `[BitFieldsView]` types. The inner view +operates on the **same underlying buffer** at the specified offset -- zero-copy all the way down. + +### Byte-Aligned Nesting + +When the start bit is a multiple of 8, the inner view is sliced at a byte boundary: + +```csharp +[BitFieldsView] +public partial record struct InnerView +{ + [BitField(0, 7)] public partial byte Value { get; set; } +} + +[BitFieldsView] +public partial record struct OuterView +{ + [BitField(0, 7)] public partial byte Header { get; set; } + [BitField(16, 23)] public partial InnerView Inner { get; set; } // byte 2 +} + +byte[] buffer = new byte[4]; +var outer = new OuterView(buffer); +var inner = outer.Inner; // view over same buffer at byte 2 +inner.Value = 0x42; // writes directly to buffer[2] +``` + +### Non-Byte-Aligned Nesting + +When the start bit is not byte-aligned, the inner view receives a bit offset: + +```csharp +[BitFieldsView] +public partial record struct OuterView +{ + [BitField(0, 3)] public partial byte LowNibble { get; set; } + [BitField(4, 11)] public partial InnerView Inner { get; set; } // byte 0, bit 4 +} + +var inner = outer.Inner; // view at byte 0 with 4-bit offset +inner.Value = 0xFF; // writes bits 4-11 of the buffer +``` + +The general case `[BitField(20, 27)]` places the inner view at byte 2, bit 4. + +### Write-Through + +Writes through nested views go directly to the underlying buffer: + +```csharp +var outer = new OuterView(buffer); +outer.Header = 0x99; +var inner = outer.Inner; +inner.Value = 0xAB; + +outer.Header; // still 0x99 -- different bytes +outer.Inner.Value; // 0xAB -- reads the same memory inner wrote to +``` + +## Mixed-Endian Nesting + +Each nested `[BitFieldsView]` independently controls its own byte order. +For individual fields that differ from the struct default, use endian-aware types +(`UInt32Be`, `UInt16Le`, etc.) as a per-field override. + +```csharp +// Embedded network capture header -- big-endian +[BitFieldsView(ByteOrder.BigEndian, BitOrder.BitZeroIsMsb)] +public partial record struct CaptureHeaderView +{ + [BitField(0, 15)] public partial ushort Protocol { get; set; } + [BitField(16, 31)] public partial ushort Length { get; set; } + [BitField(32, 63)] public partial uint SequenceNum { get; set; } +} + +// x86 binary file blob -- little-endian, with one BE field and a nested BE sub-view +[BitFieldsView] +public partial record struct FileBlobView +{ + [BitField(0, 31)] public partial uint Magic { get; set; } // LE + [BitField(32, 63)] public partial uint Timestamp { get; set; } // LE + [BitField(64, 95)] public partial UInt32Be CapturedSrcIp { get; set; } // per-field BE override + [BitField(96, 111)] public partial ushort RecordCount { get; set; } // LE + [BitField(112, 175)] public partial CaptureHeaderView Capture { get; set; } // nested BE sub-view +} + +// Outer transport -- big-endian, wrapping the LE blob +[BitFieldsView(ByteOrder.BigEndian, BitOrder.BitZeroIsMsb)] +public partial record struct TransportView +{ + [BitField(0, 15)] public partial ushort MessageType { get; set; } + [BitField(16, 47)] public partial uint PayloadLength { get; set; } + [BitField(48, 63)] public partial ushort Checksum { get; set; } + [BitField(64, 239)] public partial FileBlobView Blob { get; set; } // nested LE +} +``` + +Each layer reads/writes with its own byte order. The `UInt32Be` property in the LE struct +stores the IP address in network order without needing a separate sub-view. All writes go +to the same underlying buffer. + +--- + +## Hardware Registers + +### 8-bit VIA Register + +```csharp +[BitFields(StorageType.Byte)] +public partial struct ViaRegB +{ + [BitField(0, 2)] public partial byte SoundVolume { get; set; } + [BitFlag(3)] public partial bool SoundBuffer { get; set; } + [BitFlag(4)] public partial bool OverlayRom { get; set; } + [BitFlag(5)] public partial bool HeadSelect { get; set; } + [BitFlag(6)] public partial bool VideoPage { get; set; } + [BitFlag(7)] public partial bool SccAccess { get; set; } +} +``` + +### 16-bit Keyboard Register + +```csharp +[BitFields(StorageType.UInt16)] +public partial struct KeyboardReg0 +{ + [BitField(0, 6)] public partial byte SecondKeyCode { get; set; } + [BitFlag(7)] public partial bool SecondKeyUp { get; set; } + [BitField(8, 14)] public partial byte FirstKeyCode { get; set; } + [BitFlag(15)] public partial bool FirstKeyUp { get; set; } +} +``` + +### 64-bit Status Register + +```csharp +[BitFields(StorageType.UInt64)] +public partial struct StatusReg64 +{ + [BitField(0, 7)] public partial byte Status { get; set; } + [BitField(8, 23)] public partial ushort DataWord { get; set; } + [BitField(24, 55)] public partial uint Address { get; set; } + [BitFlag(56)] public partial bool Enable { get; set; } + [BitFlag(57)] public partial bool Ready { get; set; } + [BitFlag(58)] public partial bool Error { get; set; } +} +``` + +## Numeric Decomposition Types + +The library ships four pre-defined BitFields structs that decompose .NET numeric types into +their constituent bit fields. Just `using Stardust.Utilities;` and start using them -- no +struct definitions required. + +| Type | Storage | Fields | Use case | +|------|---------|--------|----------| +| `IEEE754Half` | `Half` (16-bit) | Sign, BiasedExponent (5-bit, bias 15), Mantissa (10-bit) | Half-precision analysis | +| `IEEE754Single` | `float` (32-bit) | Sign, BiasedExponent (8-bit, bias 127), Mantissa (23-bit) | Single-precision analysis | +| `IEEE754Double` | `double` (64-bit) | Sign, BiasedExponent (11-bit, bias 1023), Mantissa (52-bit) | Double-precision analysis | +| `DecimalBitFields` | `decimal` (128-bit) | Sign, Scale (0-28), Coefficient (96-bit) | Decimal inspection | + +All four types include implicit conversions to/from their storage type, full operator support, +and classification properties. + +### IEEE754Double + +``` +IEEE 754 Double-Precision (64-bit) + 6 5 4 3 2 1 0 + 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +|Sign| BiasedExponent | Mantissa | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ + + Mantissa: 52-bit significand (fractional part); implicit leading 1 not stored + BiasedExponent: 11-bit biased exponent (bias 1023); subtract 1023 for true power of 2 + Sign: Sign bit: 1 = negative, 0 = positive +``` + +```csharp +using Stardust.Utilities; + +// Inspect any double -- implicit conversion from double +IEEE754Double pi = Math.PI; +pi.Sign; // false +pi.BiasedExponent; // 1024 (raw stored value, includes +1023 bias) +pi.Exponent; // 1 (true mathematical power: 2^1, since 2 <= pi < 4) +pi.Mantissa; // 0x921FB54442D18 + +// Construct from bit fields +IEEE754Double val = default; +val.Sign = false; +val.BiasedExponent = 1024; +val.Mantissa = 0x921FB54442D18; +double result = val; // == Math.PI + +// Negate by flipping the sign bit +IEEE754Double x = 42.0; +x.Sign = !x.Sign; // x == -42.0 + +// Full arithmetic works through generated operators +IEEE754Double a = 1.0, sqrt5 = Math.Sqrt(5.0), two = 2.0; +IEEE754Double phi = (a + sqrt5) / two; // golden ratio + +// Classification properties +pi.IsNormal; // true +pi.IsNaN; // false +pi.IsInfinity; // false +pi.IsDenormalized; // false +pi.IsZero; // false + +// Classify special values +IEEE754Double nan = double.NaN; +nan.IsNaN; // true + +IEEE754Double inf = double.PositiveInfinity; +inf.IsInfinity; // true + +IEEE754Double tiny = double.Epsilon; +tiny.IsDenormalized; // true +``` + +### IEEE754Single + +``` +IEEE 754 Single-Precision (32-bit) + 3 2 1 0 + 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +|Sign| BiasedExponent | Mantissa | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ + + Mantissa: 23-bit significand (fractional part); implicit leading 1 not stored + BiasedExponent: 8-bit biased exponent (bias 127); subtract 127 for true power of 2 + Sign: Sign bit: 1 = negative, 0 = positive +``` + +```csharp +IEEE754Single f = 1.5f; +f.Sign; // false +f.BiasedExponent; // 127 (raw stored value) +f.Exponent; // 0 (true power: 1.5 is in [1, 2), so 2^0) +f.Mantissa; // 0x400000 (bit 22 set = 0.5) + +// Build from parts +var built = IEEE754Single.Zero + .WithSign(false) + .WithBiasedExponent(127) + .WithMantissa(0x400000u); +((float)built); // 1.5f + +// Classification +IEEE754Single eps = float.Epsilon; +eps.IsDenormalized; // true +eps.IsNormal; // false +``` + +### IEEE754Half + +``` +IEEE 754 Half-Precision (16-bit) + 1 0 + 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +|Sign| BiasedExponent | Mantissa | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ + + Mantissa: 10-bit significand (fractional part); implicit leading 1 not stored + BiasedExponent: 5-bit biased exponent (bias 15); subtract 15 for true power of 2 + Sign: Sign bit: 1 = negative, 0 = positive +``` + +```csharp +IEEE754Half h = (Half)1.5; +h.Sign; // false +h.BiasedExponent; // 15 (raw stored value) +h.Exponent; // 0 (true power: 1.5 is in [1, 2), so 2^0) +h.Mantissa; // 0x200 (bit 9 set = 0.5) +h.IsNormal; // true + +// Constants for reference +IEEE754Half.EXPONENT_BIAS; // 15 +IEEE754Half.MAX_BIASED_EXPONENT; // 0x1F (31) +``` + +### DecimalBitFields + +``` +.NET Decimal (128-bit) + 3 2 1 0 + 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +| Coefficient | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +| Coefficient | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +| Coefficient | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ +|Sign| Undefined | Scale | Undefined | ++----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ + + Coefficient: 96-bit unsigned integer coefficient (value before scaling) + Scale: Scale factor (0-28); value = Coefficient / 10^Scale + Sign: Sign bit: 1 = negative, 0 = positive + + U/Undefined = bits not defined in the struct +``` + +```csharp +DecimalBitFields price = 19.99m; +price.Sign; // false +price.Scale; // 2 (divided by 10^2) +price.Coefficient; // 1999 + +// Full decimal arithmetic +DecimalBitFields a = 10.5m, b = 3m; +decimal sum = a + b; // 13.5m +decimal prod = a * b; // 31.5m + +// Inspect sign +DecimalBitFields neg = -42m; +neg.Sign; // true +neg.Coefficient; // 42 +``` + +### Constants + +Each IEEE 754 type provides bias and max-exponent constants: + +| Type | `EXPONENT_BIAS` | `MAX_BIASED_EXPONENT` | `MIN_EXPONENT` | `MAX_EXPONENT` | +|------|-----------------|----------------|----------------|---------------------| +| `IEEE754Half` | 15 | 31 (0x1F) | -14 | 15 | +| `IEEE754Single` | 127 | 255 (0xFF) | -126 | 127 | +| `IEEE754Double` | 1023 | 2047 (0x7FF) | -1022 | 1023 | +| `DecimalBitFields` | -- | `MAX_SCALE` = 28 | -- | -- | + +`MIN_EXPONENT` and `MAX_EXPONENT` define the normal range for the `WithExponent(int)` method +and the `Exponent` setter (the true mathematical exponent, after bias removal). Values outside +this range are masked by the underlying `WithBiasedExponent`/`BiasedExponent` setter and may +produce non-normal encodings (zero, denormalized, infinity, or NaN). + +### Classification Properties (IEEE 754 types) + +All three IEEE 754 types provide the same classification properties: + +| Property | Condition | Description | +|----------|-----------|-------------| +| `IsNormal` | 0 < biasedExponent < max | Ordinary floating-point value | +| `IsDenormalized` | biasedExponent = 0, mantissa != 0 | Subnormal (very small) value | +| `IsZero` | biasedExponent = 0, mantissa = 0 | Positive or negative zero | +| `IsInfinity` | biasedExponent = max, mantissa = 0 | Positive or negative infinity | +| `IsNaN` | biasedExponent = max, mantissa != 0 | Not a Number | +| `Exponent` | Normal values only | True mathematical exponent (biased minus bias), or `null`. Setter applies bias automatically; assigning `null` sets `BiasedExponent` to 0, and `BiasedExponent` == 0 returns null for `Exponent` | + +### WithExponent (Fluent True-Exponent Setter) + +All three IEEE 754 types provide a `WithExponent(int)` fluent method that sets the +`BiasedExponent` from a true mathematical exponent (the bias is added automatically). +Out-of-range values are masked by the underlying `WithBiasedExponent` method, consistent +with all other generated `With...` methods. + +The `Exponent` property also provides a setter: assigning an `int` applies the bias and +sets `BiasedExponent`; assigning `null` sets `BiasedExponent` to 0. + +```csharp +// Build 2^3 = 8.0 from a true exponent +var d = IEEE754Double.Zero.WithExponent(3).WithMantissa(0); +double value = d; // 8.0 + +// Round-trip: read Exponent, rebuild with WithExponent +IEEE754Double pi = Math.PI; +int exp = pi.Exponent!.Value; // 1 +var rebuilt = IEEE754Double.Zero + .WithExponent(exp) + .WithMantissa(pi.Mantissa); +double result = rebuilt; // == Math.PI + +// Set Exponent directly via the setter +IEEE754Double d2 = 1.0; +d2.Exponent = 3; // BiasedExponent = 3 + 1023 = 1026 +d2.Exponent = null; // BiasedExponent = 0 + +// Out-of-range values are masked (no exception) +var h = IEEE754Half.Zero.WithExponent(16); // biased value masked to 5-bit field +``` + +## Network Protocol Headers (RFC) + +### Value-Type Approach (BitFields Composition) + +For protocols parsed word-by-word, embed reusable flag structs inside larger word structs: + +```csharp +// 3-bit IPv4 flags +[BitFields(typeof(byte), UndefinedBitsMustBe.Zeroes)] +public partial struct IPv4Flags +{ + [BitFlag(0)] public partial bool MoreFragments { get; set; } + [BitFlag(1)] public partial bool DontFragment { get; set; } + [BitFlag(2, MustBe.Zero)] public partial bool Reserved { get; set; } // Must be 0 even though defined +} + +// 9-bit TCP control flags +[BitFields(typeof(ushort), UndefinedBitsMustBe.Zeroes)] +public partial struct TcpFlags +{ + [BitFlag(0)] public partial bool FIN { get; set; } + [BitFlag(1)] public partial bool SYN { get; set; } + [BitFlag(2)] public partial bool RST { get; set; } + [BitFlag(3)] public partial bool PSH { get; set; } + [BitFlag(4)] public partial bool ACK { get; set; } + [BitFlag(5)] public partial bool URG { get; set; } +} + +// Embed flags into 32-bit header words +[BitFields(typeof(uint))] +public partial struct IPv4FragmentWord +{ + [BitField(16, 31)] public partial ushort Identification { get; set; } + [BitField(13, 15)] public partial IPv4Flags Flags { get; set; } // composed! + [BitField(0, 12)] public partial ushort FragmentOffset { get; set; } +} + +[BitFields(typeof(uint))] +public partial struct TcpControlWord +{ + [BitField(28, 31)] public partial byte DataOffset { get; set; } + [BitField(16, 24)] public partial TcpFlags Flags { get; set; } // composed! + [BitField(0, 15)] public partial ushort WindowSize { get; set; } +} + +// Fluent construction +var tcp = TcpControlWord.Zero + .WithDataOffset(5) + .WithFlags(TcpFlags.Zero.WithSYN(true).WithACK(true)) + .WithWindowSize(65535); + +tcp.Flags.SYN; // true +tcp.Flags.ACK; // true + +// TCP three-way handshake using static Bit properties +var syn = TcpFlags.SYNBit; +var synAck = TcpFlags.SYNBit | TcpFlags.ACKBit; +var ack = TcpFlags.ACKBit; +``` + +### Buffer-View Approach (BitFieldsView) + +For zero-copy parsing of complete headers over byte buffers, use `[BitFieldsView]` with +`ByteOrder.NetworkEndian` and `BitOrder.BitZeroIsMsb` so that bit positions match RFC diagrams directly. +Plain `ushort` and `uint` properties are all you need -- the struct-level `ByteOrder` handles +serialization. + +Reusable protocol header files are available in the test project at `Test/Protocols/`. + +### IPv4 Header (RFC 791) + +See `Test/Protocols/IPv4HeaderView.cs` for a copy-pasteable implementation. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +|Version| IHL |Type of Service| Total Length | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Identification |Flags| Fragment Offset | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Time to Live | Protocol | Header Checksum | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Source Address | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Destination Address | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +```csharp +[BitFieldsView(ByteOrder.NetworkEndian, BitOrder.BitZeroIsMsb)] +public partial record struct IPv4HeaderView +{ + [BitField(0, 3)] public partial byte Version { get; set; } + [BitField(4, 7)] public partial byte Ihl { get; set; } + [BitField(8, 13)] public partial byte Dscp { get; set; } + [BitField(14, 15)] public partial byte Ecn { get; set; } + [BitField(16, 31)] public partial ushort TotalLength { get; set; } + [BitField(32, 47)] public partial ushort Identification { get; set; } + [BitFlag(48)] public partial bool ReservedFlag { get; set; } + [BitFlag(49)] public partial bool DontFragment { get; set; } + [BitFlag(50)] public partial bool MoreFragments { get; set; } + [BitField(51, 63)] public partial ushort FragmentOffset { get; set; } + [BitField(64, 71)] public partial byte TimeToLive { get; set; } + [BitField(72, 79)] public partial byte Protocol { get; set; } + [BitField(80, 95)] public partial ushort HeaderChecksum { get; set; } + [BitField(96, 127)] public partial uint SourceAddress { get; set; } + [BitField(128, 159)] public partial uint DestinationAddress { get; set; } + + public int HeaderLengthBytes => Ihl * 4; +} +``` + +### TCP Header (RFC 793) + +See `Test/Protocols/TcpHeaderView.cs` for a copy-pasteable implementation. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Source Port | Destination Port | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Sequence Number | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Acknowledgment Number | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Data | |N|C|E|U|A|P|R|S|F| | +| Offset| Rsvd |S|W|C|R|C|S|S|Y|I| Window Size | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Checksum | Urgent Pointer | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +```csharp +[BitFieldsView(ByteOrder.NetworkEndian, BitOrder.BitZeroIsMsb)] +public partial record struct TcpHeaderView +{ + [BitField(0, 15)] public partial ushort SourcePort { get; set; } + [BitField(16, 31)] public partial ushort DestinationPort { get; set; } + [BitField(32, 63)] public partial uint SequenceNumber { get; set; } + [BitField(64, 95)] public partial uint AcknowledgmentNumber { get; set; } + [BitField(96, 99)] public partial byte DataOffset { get; set; } + [BitField(100, 102)] public partial byte Reserved { get; set; } + [BitFlag(103)] public partial bool NS { get; set; } + [BitFlag(104)] public partial bool CWR { get; set; } + [BitFlag(105)] public partial bool ECE { get; set; } + [BitFlag(106)] public partial bool URG { get; set; } + [BitFlag(107)] public partial bool ACK { get; set; } + [BitFlag(108)] public partial bool PSH { get; set; } + [BitFlag(109)] public partial bool RST { get; set; } + [BitFlag(110)] public partial bool SYN { get; set; } + [BitFlag(111)] public partial bool FIN { get; set; } + [BitField(112, 127)] public partial ushort WindowSize { get; set; } + [BitField(128, 143)] public partial ushort Checksum { get; set; } + [BitField(144, 159)] public partial ushort UrgentPointer { get; set; } + + public int HeaderLengthBytes => DataOffset * 4; +} +``` + +### UDP Header (RFC 768) + +See `Test/Protocols/UdpHeaderView.cs` for a copy-pasteable implementation. + +```csharp +[BitFieldsView(ByteOrder.NetworkEndian, BitOrder.BitZeroIsMsb)] +public partial record struct UdpHeaderView +{ + [BitField(0, 15)] public partial ushort SourcePort { get; set; } + [BitField(16, 31)] public partial ushort DestinationPort { get; set; } + [BitField(32, 47)] public partial ushort Length { get; set; } + [BitField(48, 63)] public partial ushort Checksum { get; set; } +} +``` + +### IPv6 Header (RFC 2460) + +See `Test/Protocols/IPv6HeaderView.cs` for a copy-pasteable implementation. + +```csharp +[BitFieldsView(ByteOrder.NetworkEndian, BitOrder.BitZeroIsMsb)] +public partial record struct IPv6HeaderView +{ + [BitField(0, 3)] public partial byte Version { get; set; } + [BitField(4, 11)] public partial byte TrafficClass { get; set; } + [BitField(12, 31)] public partial uint FlowLabel { get; set; } + [BitField(32, 47)] public partial ushort PayloadLength { get; set; } + [BitField(48, 55)] public partial byte NextHeader { get; set; } + [BitField(56, 63)] public partial byte HopLimit { get; set; } +} +``` + +## Parsing a Captured Network Packet + +Parse a raw Ethernet/IPv4/TCP packet using the protocol views above: + +```csharp +byte[] frame = File.ReadAllBytes("captured_frame.bin"); + +// Skip 14-byte Ethernet header to reach the IPv4 header +var ip = new IPv4HeaderView(frame, 14); + +Console.WriteLine(ip.Version); // 4 +Console.WriteLine(ip.Ihl); // 5 (20-byte header) +Console.WriteLine(ip.TotalLength); // total IP packet length +Console.WriteLine(ip.TimeToLive); // e.g. 64 +Console.WriteLine(ip.Protocol); // 6 = TCP + +// Parse the TCP header immediately after the IPv4 header +int tcpOffset = 14 + ip.HeaderLengthBytes; +var tcp = new TcpHeaderView(frame, tcpOffset); + +Console.WriteLine(tcp.SourcePort); // e.g. 443 +Console.WriteLine(tcp.DestinationPort); // e.g. 54321 +Console.WriteLine(tcp.SYN); // true for SYN packets +Console.WriteLine(tcp.ACK); // true for ACK packets + +// The application payload starts after both headers +int payloadOffset = tcpOffset + tcp.HeaderLengthBytes; +var payload = frame.AsSpan(payloadOffset); +``` + +All reads go directly to the original `frame` buffer. No copies, no allocations. + +## Mixed-Endian Capture File + +A pcap-style capture file on Windows (LE) containing network packets (BE): + +```csharp +// Pcap global header -- little-endian (x86 native) +[BitFieldsView] +public partial record struct PcapGlobalHeader +{ + [BitField(0, 31)] public partial uint MagicNumber { get; set; } // 0xA1B2C3D4 + [BitField(32, 47)] public partial ushort VersionMajor { get; set; } + [BitField(48, 63)] public partial ushort VersionMinor { get; set; } + [BitField(128, 159)] public partial uint SnapLen { get; set; } + [BitField(160, 191)] public partial uint LinkType { get; set; } // 1 = Ethernet +} + +// Pcap per-packet header -- little-endian +[BitFieldsView] +public partial record struct PcapPacketHeader +{ + [BitField(0, 31)] public partial uint TimestampSec { get; set; } + [BitField(32, 63)] public partial uint TimestampUsec { get; set; } + [BitField(64, 95)] public partial uint IncludedLen { get; set; } + [BitField(96, 127)] public partial uint OriginalLen { get; set; } +} + +// Parse a pcap file +byte[] pcap = File.ReadAllBytes("capture.pcap"); + +var global = new PcapGlobalHeader(pcap); +Console.WriteLine(global.MagicNumber == 0xA1B2C3D4); // True (LE native) +Console.WriteLine(global.VersionMajor); // 2 +Console.WriteLine(global.VersionMinor); // 4 + +// First packet header starts at byte 24 (after global header) +var pktHdr = new PcapPacketHeader(pcap, 24); +int packetLen = (int)pktHdr.IncludedLen; + +// The packet data (Ethernet frame) starts at byte 40 +// Parse it with big-endian views -- they handle their own byte order +var ip = new IPv4HeaderView(pcap, 40 + 14); // skip Ethernet header +Console.WriteLine(ip.Version); // 4 -- BE field in LE file +Console.WriteLine(ip.SourceAddress); // parsed as big-endian + +int tcpOff = 40 + 14 + ip.HeaderLengthBytes; +var tcp = new TcpHeaderView(pcap, tcpOff); +Console.WriteLine(tcp.SourcePort); // BE, correct +Console.WriteLine(tcp.SYN); +``` + +The pcap headers (LE) and network headers (BE) each declare their own byte order. +No manual byte-swapping. No endian-aware types needed for the common case. +`UInt32Be` / `UInt16Le` per-field overrides are only needed for the uncommon case +where a single struct mixes endianness at the individual field level. + +--- + +## RFC Diagram Generator + +`BitFieldDiagram` generates RFC 2360-style ASCII bit field diagrams from any `[BitFields]` or +`[BitFieldsView]` struct. It reads the generated `Fields` metadata property and produces a +text diagram with bit-position headers, byte offsets, and auto-sized cells. + +There are three ways to use it: + +| Approach | Best for | +|----------|----------| +| **Instance API** (`new BitFieldDiagram(...)`) | Reusable, configurable diagrams -- UI bindings, multi-struct lists, changing options at runtime | +| **Static type-based API** (`BitFieldDiagram.Render(typeof(...))`) | Quick one-shot rendering from a `Type` | +| **Static field-based API** (`BitFieldDiagram.Render(fields)`) | Low-level control when you already have `ReadOnlySpan` | + +### Instance API + +Create a `BitFieldDiagram` object, configure it, add one or more structs, and render. + +**Creating a diagram** + +```csharp +using Stardust.Utilities; + +// Empty diagram -- add structs later +var diagram = new BitFieldDiagram(); + +// Single struct +var diagram = new BitFieldDiagram(typeof(IPv4HeaderView), description: "IPv4 Header"); + +// Multiple structs in one diagram +var diagram = new BitFieldDiagram( + [typeof(M68020DataRegisters), typeof(M68020SR), typeof(M68020CCR)], + description: "68020 Register Set"); +``` + +**Setting options** + +All options are mutable properties, so you can change them at any time before rendering: + +```csharp +var diagram = new BitFieldDiagram(typeof(IPv4HeaderView)); +diagram.BitsPerRow = 16; +diagram.IncludeDescriptions = true; +diagram.ShowByteOffset = true; +diagram.CommentPrefix = "// "; +diagram.Description = "IPv4 Header"; +``` + +Options can also be set via constructor parameters: + +```csharp +var diagram = new BitFieldDiagram( + typeof(TcpHeaderView), + description: "TCP Header", + commentPrefix: "/// ", + bitsPerRow: 32, + includeDescriptions: true, + showByteOffset: true); +``` + +| Property | Default | Description | +|----------|---------|-------------| +| `BitsPerRow` | 32 | Number of bits per row. Common values: 8, 16, 32, 64. | +| `IncludeDescriptions` | true (constructor) | Appends a legend with field descriptions below the diagram. | +| `ShowByteOffset` | false (constructor) | Shows hex byte offset (e.g., `0x00`) at the left of each content row. | +| `CommentPrefix` | null | When non-null, prepended to every output line (e.g., `"// "`, `"/// "`). | +| `Description` | null | Caption shown above the diagram. When `DescriptionResourceType` is set, this is used as a resource key. | +| `DescriptionResourceType` | null | Optional `Type` with a `ResourceManager` property for localized descriptions. | + +**Adding structs** + +Use `AddStruct` to add `[BitFields]` or `[BitFieldsView]` types incrementally. It returns a +`Result` so you can check for errors: + +```csharp +var diagram = new BitFieldDiagram(); +diagram.AddStruct(typeof(IPv4HeaderView)); +diagram.AddStruct(typeof(TcpHeaderView)); + +// Error handling +Result result = diagram.AddStruct(typeof(string)); // not a BitFields type +if (result.IsFailure) + Console.WriteLine(result.Error); // "Struct 'String' is not a valid [BitFields] or [BitFieldsView] type." +``` + +The `Structs` property exposes the current list of types: + +```csharp +var diagram = new BitFieldDiagram(typeof(IPv4HeaderView)); +Console.WriteLine(diagram.Structs.Count); // 1 +``` + +**Rendering** + +```csharp +var diagram = new BitFieldDiagram(typeof(IPv4HeaderView), description: "IPv4 Header"); +diagram.BitsPerRow = 32; +diagram.IncludeDescriptions = true; + +// Render as a single string +Result stringResult = diagram.RenderToString(); +if (stringResult.IsSuccess) + Console.WriteLine(stringResult.Value); + +// Render as a list of lines +Result, string> linesResult = diagram.Render(); +if (linesResult.IsSuccess) + foreach (string line in linesResult.Value) + Console.WriteLine(line); +``` + +Both `Render()` and `RenderToString()` return `Result` types. They return an error when no +structs have been added. + +**Real-world example (Blazor UI)** + +The instance API is ideal for UI scenarios where options change at runtime: + +```csharp +// Create reusable diagram objects at initialization +BitFieldDiagram[] sources = +[ + new(typeof(IPv4HeaderView), "IPv4 Header"), + new(typeof(TcpHeaderView), "TCP Header"), + new([typeof(M68020DataRegisters), typeof(M68020SR), typeof(M68020CCR)], + "68020 Register Set"), +]; + +// Update options from UI controls and re-render +var diagram = sources[selectedIndex]; +diagram.BitsPerRow = bitsPerRow; +diagram.IncludeDescriptions = showDescriptions; +diagram.ShowByteOffset = showByteOffset; +diagram.CommentPrefix = commentPrefix; +string text = diagram.RenderToString().Value; +``` + +### Static Type-Based API + +For quick one-shot diagrams, pass a `Type` directly to the static methods: + +```csharp +// Render a single type +List lines = BitFieldDiagram.Render(typeof(IPv4HeaderView), bitsPerRow: 32); +string diagram = BitFieldDiagram.RenderToString(typeof(IPv4HeaderView), bitsPerRow: 32); + +// Render multiple types as a unified diagram with consistent cell widths +List lines = BitFieldDiagram.RenderList( + bitFieldsTypes: [typeof(M68020DataRegisters), typeof(M68020SR)], + bitsPerRow: 32, + includeDescriptions: true); + +string diagram = BitFieldDiagram.RenderListToString( + bitFieldsTypes: [typeof(M68020DataRegisters), typeof(M68020SR)], + bitsPerRow: 32, + includeDescriptions: true); +``` + +### Static Field-Based API + +When you already have a `ReadOnlySpan` (from the generated `Fields` property), +use the field-based overloads for direct control: + +```csharp +// Render as a list of lines +List lines = BitFieldDiagram.Render(IPv4HeaderView.Fields); + +// Render as a single string +string diagram = BitFieldDiagram.RenderToString(IPv4HeaderView.Fields); +``` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `bitsPerRow` | 32 | Number of bits per row. Common values: 8, 16, 32, 64. | +| `includeDescriptions` | false | Appends a legend with `Description` text for each field. | +| `showByteOffset` | true | Shows hex byte offset (e.g., `0x00`) at the left of each content row. | +| `minCellWidth` | 0 (auto) | Minimum cell width in characters per bit column. When 0, computed automatically. Used internally by `RenderList` for consistent scale. | +| `commentPrefix` | null | When non-null, prepended to every output line. | + +```csharp +// 8 bits per row for small registers +string diagram = BitFieldDiagram.RenderToString(StatusRegister.Fields, bitsPerRow: 8); + +// 64-bit wide display +string diagram = BitFieldDiagram.RenderToString(TcpHeaderView.Fields, bitsPerRow: 64); + +// Include field descriptions +string diagram = BitFieldDiagram.RenderToString(StatusRegister.Fields, includeDescriptions: true); + +// Hide byte offsets for compact output +string diagram = BitFieldDiagram.RenderToString(StatusRegister.Fields, showByteOffset: false); + +// Add a comment prefix for embedding in source code +string diagram = BitFieldDiagram.RenderToString(StatusRegister.Fields, commentPrefix: "// "); +``` + +Use `ComputeMinCellWidth` to pre-compute the shared width if you need it for custom layout logic. + +### Features + +- **Auto-sized cells** -- Cell width adjusts automatically so all field names fit without truncation. +- **Byte offsets** -- Each content row shows the hex byte offset (e.g., `0x00`, `0x04`) on the left. +- **Bit-position headers** -- Tens and ones digit rows in standard RFC format, with digits centered + over cell dashes. +- **Undefined bits** -- Gaps between defined fields are labeled `Undefined` (or `U` if the span is + too narrow). A legend is appended when undefined bits are present. +- **Struct-sized rows** -- The last row ends at the struct's last defined bit rather than padding + to `bitsPerRow`. +- **Single separators** -- One `+-+-+` line between rows, matching RFC 791 style. + +### Example Output + +IPv4 header at 32 bits per row: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0x00 |Version| Ihl | Undefined | TotalLength | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0x04 | Undefined | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0x08 | Undefined | Protocol | Undefined | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0x0C | SourceAddress | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0x10 | DestinationAddress | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + U/Undefined = bits not defined in the struct + Version (bits 0-3): IP protocol version (always 4 for IPv4) + Ihl (bits 4-7): Internet Header Length in 32-bit words (min 5 = 20 bytes) + TotalLength (bits 16-31): Total packet length in bytes, including header and payload + Protocol (bits 72-79): Upper-layer protocol number (6=TCP, 17=UDP, 1=ICMP) + SourceAddress (bits 96-127): 32-bit source IPv4 address + DestinationAddress (bits 128-159): 32-bit destination IPv4 address +``` + +CPU status register with descriptions: + +```csharp +var diagram = new BitFieldDiagram(typeof(StatusRegister), bitsPerRow: 16, includeDescriptions: true); +string output = diagram.RenderToString().Value; +``` + +### Demo Application + +**[Try the interactive web demo](https://dhadner.github.io/Stardust.Utilities/)** -- includes an RFC Diagram tab with struct picker, bits/row selector, description and byte offset toggles, and copy to clipboard. + +The source code includes two demo apps: +- `Demo/BitFields.DemoWeb` -- Blazor WebAssembly app (runs in any browser, no install) +- `Demo/BitFields.DemoApp` -- WPF desktop app (Windows) + +--- + +## Performance + +Benchmarks show generated code performs within **1%** of hand-coded bit manipulation. + +**Property Accessors vs Raw Bit Manipulation** (500M iterations, .NET 10): + +| Operation | Raw Bit Ops | Generated Properties | Difference | +|-----------|-------------|---------------------|------------| +| Boolean GET | 271 ms | 263 ms | ~0% (noise) | +| Boolean SET | 506 ms | 494 ms | ~0% (noise) | +| Field GET (shift+mask) | 124 ms | 123 ms | ~0% (noise) | + +All masks and shifts are compile-time constants. `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +eliminates all property call overhead. No heap allocations, no reflection, no boxing. + +## Generated Code Listing + +### BitFields Generated Code + +For this struct: + +```csharp +[BitFields(typeof(byte))] +public partial struct StatusRegister +{ + [BitFlag(0)] public partial bool Ready { get; set; } + [BitFlag(1)] public partial bool Error { get; set; } + [BitField(2, 4)] public partial byte Mode { get; set; } +} +``` + +The generator creates (abbreviated -- the full output also includes parsing, formatting, +`IComparable`, `IEquatable`, span serialization, and a JSON converter): + +```csharp +[JsonConverter(typeof(StatusRegisterJsonConverter))] +public partial struct StatusRegister : IComparable, IComparable, IEquatable, + IFormattable, ISpanFormattable, IParsable, ISpanParsable +{ + private byte Value; + + public const int SizeInBytes = 1; + public static StatusRegister Zero => default; + + public StatusRegister(byte value) { Value = value; } + + // ── BitFlag properties ────────────────────────────────────── + public partial bool Ready + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (Value & 0x01) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Value = value ? (byte)(Value | 0x01) : (byte)(Value & 0xFE); + } + + // ── BitField properties ───────────────────────────────────── + // Note: value is cast to the storage type before shifting to prevent + // widening promotion from corrupting adjacent bits. + public partial byte Mode + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (byte)((Value >> 2) & 0x07); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Value = (byte)((Value & 0xE3) | ((((byte)value) << 2) & 0x1C)); + } + + // ── Static Bit and Mask properties ────────────────────────── + public static StatusRegister ReadyBit => new((byte)0x01); + public static StatusRegister ErrorBit => new((byte)0x02); + public static StatusRegister ModeMask => new((byte)0x1C); + + // ── Metadata ──────────────────────────────────────────────── + public static string? StructDescription => null; + public static Type? StructDescriptionResourceType => null; + public static ReadOnlySpan Fields => new BitFieldInfo[] + { + new("Ready", 0, 1, "bool", true, ByteOrder.LittleEndian, BitOrder.BitZeroIsLsb, StructTotalBits: 8, ...), + new("Error", 1, 1, "bool", true, ByteOrder.LittleEndian, BitOrder.BitZeroIsLsb, StructTotalBits: 8, ...), + new("Mode", 2, 3, "byte", false, ByteOrder.LittleEndian, BitOrder.BitZeroIsLsb, StructTotalBits: 8, ...), + }; + + // ── Fluent With methods ───────────────────────────────────── + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public StatusRegister WithReady(bool value) => + new(value ? (byte)(Value | 0x01) : (byte)(Value & 0xFE)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public StatusRegister WithMode(byte value) => + new((byte)((Value & 0xE3) | (((byte)value << 2) & 0x1C))); + + // ── Operators (all AggressiveInlining) ────────────────────── + public static StatusRegister operator ~(StatusRegister a) => new((byte)~a.Value); + public static StatusRegister operator |(StatusRegister a, StatusRegister b) => new((byte)(a.Value | b.Value)); + public static StatusRegister operator &(StatusRegister a, StatusRegister b) => new((byte)(a.Value & b.Value)); + public static StatusRegister operator ^(StatusRegister a, StatusRegister b) => new((byte)(a.Value ^ b.Value)); + public static StatusRegister operator +(StatusRegister a, StatusRegister b) => new(unchecked((byte)(a.Value + b.Value))); + public static StatusRegister operator -(StatusRegister a, StatusRegister b) => new(unchecked((byte)(a.Value - b.Value))); + public static StatusRegister operator -(StatusRegister a) => new(unchecked((byte)(0 - a.Value))); + // ... plus +, -, *, /, % with storage-type operand on either side + + // Small types return int so (bits >> n) & 1 works without casting + public static int operator <<(StatusRegister a, int b) => a.Value << b; + public static int operator >>(StatusRegister a, int b) => a.Value >> b; + public static int operator >>>(StatusRegister a, int b) => a.Value >>> b; + + public static bool operator <(StatusRegister a, StatusRegister b) => a.Value < b.Value; + public static bool operator >(StatusRegister a, StatusRegister b) => a.Value > b.Value; + public static bool operator <=(StatusRegister a, StatusRegister b) => a.Value <= b.Value; + public static bool operator >=(StatusRegister a, StatusRegister b) => a.Value >= b.Value; + public static bool operator ==(StatusRegister a, StatusRegister b) => a.Value == b.Value; + public static bool operator !=(StatusRegister a, StatusRegister b) => a.Value != b.Value; + + // ── Conversions ───────────────────────────────────────────── + public static implicit operator byte(StatusRegister value) => value.Value; + public static implicit operator StatusRegister(byte value) => new(value); + public static implicit operator StatusRegister(int value) => new(unchecked((byte)value)); + + // ── Span serialization ────────────────────────────────────── + public StatusRegister(ReadOnlySpan bytes) { /* validates length, reads LE */ } + public static StatusRegister ReadFrom(ReadOnlySpan bytes) => new(bytes); + public void WriteTo(Span destination) { /* validates length, writes LE */ } + public bool TryWriteTo(Span destination, out int bytesWritten) { /* ... */ } + public byte[] ToByteArray() { /* ... */ } + + // ── Equality, hashing, formatting ─────────────────────────── + public override bool Equals(object? obj) => obj is StatusRegister other && Value == other.Value; + public override int GetHashCode() => Value.GetHashCode(); + public override string ToString() => $"0x{Value:X}"; + + // ── Interface implementations (IComparable, IEquatable, IParsable, etc.) ── + // ── JSON converter (reads/writes as string) ── +} +``` + +### BitFieldsView Generated Code + +For this view struct: + +```csharp +[BitFieldsView(ByteOrder.BigEndian, BitOrder.BitZeroIsMsb)] +public partial record struct ByteFlagsView +{ + [BitFlag(0)] public partial bool MsbFlag { get; set; } + [BitFlag(7)] public partial bool LsbFlag { get; set; } + [BitField(1, 4)] public partial byte Middle { get; set; } +} +``` + +The generator creates: + +```csharp +public partial record struct ByteFlagsView +{ + private readonly Memory _data; + private readonly byte _bitOffset; + + public const int SizeInBytes = 1; + public const int BitWidth = 8; + + // ── Constructors ──────────────────────────────────────────── + public ByteFlagsView(Memory data) { /* validates length */ _data = data; _bitOffset = 0; } + public ByteFlagsView(byte[] data) : this(data.AsMemory()) { } + public ByteFlagsView(byte[] data, int offset) : this(data.AsMemory(offset)) { } + internal ByteFlagsView(Memory data, int bitOffset) { /* for nested views */ } + + public Memory Data => _data; + + // ── Property accessors (read/write directly to buffer) ────── + // Fast path when bitOffset == 0, fallback path for sub-byte nesting. + public partial byte Middle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var s = _data.Span; + if (_bitOffset == 0) + return (byte)((s[0] >> 3) & 0x0F); + // Fallback: BinaryPrimitives read with bit offset calculation + int ep = 1 + _bitOffset; + int bi = ep >> 3; + int endInWindow = (ep + 3) - bi * 8; + int sh = 16 - 1 - endInWindow; + return (byte)((BinaryPrimitives.ReadUInt16BigEndian(s.Slice(bi)) >> sh) & 0x000F); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + var s = _data.Span; + if (_bitOffset == 0) + { + s[0] = (byte)((s[0] & 0x87) | (((byte)value << 3) & 0x78)); + } + else + { + // Fallback: read-modify-write via BinaryPrimitives + int ep = 1 + _bitOffset; + int bi = ep >> 3; + int endInWindow = (ep + 3) - bi * 8; + int sh = 16 - 1 - endInWindow; + var slice = s.Slice(bi); + ushort raw = BinaryPrimitives.ReadUInt16BigEndian(slice); + ushort m = (ushort)(0x000F << sh); + raw = (ushort)((raw & (ushort)~m) | (((ushort)value << sh) & m)); + BinaryPrimitives.WriteUInt16BigEndian(slice, raw); + } + } + } + + public partial bool MsbFlag + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var s = _data.Span; + if (_bitOffset == 0) return (s[0] & 0x80) != 0; + int ep = 0 + _bitOffset; + return (s[ep >> 3] & (1 << (7 - (ep & 7)))) != 0; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + var s = _data.Span; + if (_bitOffset == 0) + { + s[0] = value ? (byte)(s[0] | 0x80) : (byte)(s[0] & 0x7F); + return; + } + int ep = 0 + _bitOffset; + int bi = ep >> 3; + int m = 1 << (7 - (ep & 7)); + s[bi] = value ? (byte)(s[bi] | m) : (byte)(s[bi] & ~m); + } + } + + // ── Metadata ──────────────────────────────────────────────── + public static ReadOnlySpan Fields => new BitFieldInfo[] { /* ... */ }; +} +``` diff --git a/BITSTREAM.md b/BITSTREAM.md deleted file mode 100644 index ba9b65f..0000000 --- a/BITSTREAM.md +++ /dev/null @@ -1,244 +0,0 @@ -# BitStream - -A stream class for reading and writing individual bits. Extends `System.IO.Stream` for compatibility with standard stream operations. - -## Overview - -`BitStream` allows you to work at the bit level, which is useful for: -- Parsing binary protocols with non-byte-aligned fields -- Implementing compression algorithms -- Working with bit-packed data structures -- Serial communication protocols - -## Quick Start - -```csharp -using Stardust.Utilities.Bits; - -// Create a stream -var stream = new BitStream(); - -// Write individual bits -stream.Write(true); // 1 -stream.Write(false); // 0 -stream.Write(true); // 1 - -// Write a byte (8 bits) -stream.WriteByte(0xAB); - -// Seek back to start -stream.Seek(0, SeekOrigin.Begin); - -// Read bits back -bool bit1 = stream.Read(); // true -bool bit2 = stream.Read(); // false -bool bit3 = stream.Read(); // true - -// Skip to byte boundary and read byte -stream.Seek(3, SeekOrigin.Begin); // Position after first 3 bits -int value = stream.ReadByte(); // Returns 0xAB (if aligned) -``` - -## Creating a BitStream - -```csharp -// Default capacity (2048 bits / 256 bytes) -var stream = new BitStream(); - -// Specific capacity in bits -var stream = new BitStream(1024); // 1024 bits capacity -``` - -## Writing Bits - -### Single Bits - -```csharp -// Write returns 1 if successful, 0 if at capacity -int written = stream.Write(true); // Write a 1 bit -written = stream.Write(false); // Write a 0 bit -``` - -### Bytes - -```csharp -// Write a single byte (8 bits) -stream.WriteByte(0xFF); - -// Write from byte array -byte[] data = { 0x01, 0x02, 0x03 }; -stream.Write(data, offset: 0, count: 3); // Writes 24 bits -``` - -## Reading Bits - -### Single Bits - -```csharp -// Read one bit, throws if past end -bool bit = stream.Read(); - -// Safe read - returns -1 if at end -int result = stream.Read(out bool bit); -if (result == 1) -{ - // bit contains the value -} -``` - -### Bytes - -```csharp -// Read a byte (8 bits), returns -1 if not enough bits -int value = stream.ReadByte(); -if (value >= 0) -{ - byte b = (byte)value; -} - -// Read into buffer -byte[] buffer = new byte[10]; -int bytesRead = stream.Read(buffer, offset: 0, count: 10); -``` - -## Positioning - -### Properties - -```csharp -long pos = stream.Position; // Current position in bits -long len = stream.Length; // Used length in bits -long cap = stream.Capacity; // Total capacity in bits -``` - -### Seeking - -```csharp -// Seek from beginning -stream.Seek(0, SeekOrigin.Begin); - -// Seek relative to current position -stream.Seek(8, SeekOrigin.Current); // Forward 8 bits -stream.Seek(-4, SeekOrigin.Current); // Back 4 bits - -// Seek from end -stream.Seek(-1, SeekOrigin.End); // Last bit -``` - -### Setting Length - -```csharp -// Extend or truncate the stream -stream.SetLength(100); // 100 bits - -// Truncate from beginning (removes first N bits) -stream.Truncate(8, SeekOrigin.Begin); // Remove first 8 bits - -// Truncate from end (removes last N bits) -stream.Truncate(8, SeekOrigin.End); // Remove last 8 bits -``` - -## Capacity Management - -The stream automatically grows when needed, doubling capacity each time: - -```csharp -// Manual capacity control -stream.Capacity = 4096; // Set capacity to 4096 bits - -// Access underlying buffer -byte[] buffer = stream.GetBuffer(); -``` - -## Example: Parsing a Bit-Packed Protocol - -```csharp -// Protocol: 3-bit type, 5-bit length, N bytes of data -public (int type, byte[] data) ParsePacket(BitStream stream) -{ - // Read 3-bit type - int type = 0; - for (int i = 0; i < 3; i++) - { - if (stream.Read()) - type |= (1 << i); - } - - // Read 5-bit length - int length = 0; - for (int i = 0; i < 5; i++) - { - if (stream.Read()) - length |= (1 << i); - } - - // Read data bytes - byte[] data = new byte[length]; - stream.Read(data, 0, length); - - return (type, data); -} -``` - -## Example: Building a Bit-Packed Message - -```csharp -public byte[] BuildMessage(int type, byte[] data) -{ - var stream = new BitStream(); - - // Write 3-bit type - for (int i = 0; i < 3; i++) - { - stream.Write((type & (1 << i)) != 0); - } - - // Write 5-bit length - int length = data.Length; - for (int i = 0; i < 5; i++) - { - stream.Write((length & (1 << i)) != 0); - } - - // Write data bytes - stream.Write(data, 0, data.Length); - - // Get result (may have trailing bits in last byte) - return stream.GetBuffer()[..(int)((stream.Length + 7) / 8)]; -} -``` - -## Stream Compatibility - -`BitStream` extends `System.IO.Stream`, so it works with APIs expecting streams: - -```csharp -// Note: Byte-level operations may not align with bit boundaries -public override bool CanRead => true; -public override bool CanSeek => true; -public override bool CanWrite => true; -``` - -However, be aware that: -- `Position` and `Length` are in **bits**, not bytes -- `ReadByte()` requires at least 8 bits remaining -- Mixing bit and byte operations requires careful position management - -## API Reference - -| Member | Description | -|--------|-------------| -| `Write(bool)` | Write a single bit | -| `Write(byte[], int, int)` | Write bytes (offset and count in bytes) | -| `WriteByte(byte)` | Write 8 bits | -| `Read()` | Read a single bit (throws if past end) | -| `Read(out bool)` | Safe read (returns -1 if at end) | -| `Read(byte[], int, int)` | Read bytes into buffer | -| `ReadByte()` | Read 8 bits as int (-1 if not enough) | -| `Seek(long, SeekOrigin)` | Move position (in bits) | -| `SetLength(long)` | Set length (in bits) | -| `Truncate(int, SeekOrigin)` | Remove bits from beginning or end | -| `GetBuffer()` | Access underlying byte array | -| `Position` | Current position in bits | -| `Length` | Used length in bits | -| `Capacity` | Total capacity in bits | diff --git a/BitFieldAttribute.cs b/BitFieldAttribute.cs index 82f98cf..b35ca3b 100644 --- a/BitFieldAttribute.cs +++ b/BitFieldAttribute.cs @@ -44,18 +44,56 @@ public sealed class BitFieldAttribute : Attribute /// public int EndBit { get; } + /// + /// Normally set to and has no effect. + /// When set to or , it overrides the behavior of the field's + /// bits on write and during conversion of the underlying BitFields struct to/from other types. + /// + public MustBe ValueOverride { get; } = MustBe.Any; + + /// + /// An optional human-readable description of this field. + /// When is null, this is a literal string. + /// When is set, this is a resource key + /// resolved at runtime via the resource type's ResourceManager. + /// + /// + /// + /// // Literal description (no localization) + /// [BitField(0, 3, Description = "IP header version")] + /// public partial byte Version { get; set; } + /// + /// // Localized description via .resx resource file + /// [BitField(0, 3, Description = nameof(Strings.IpVersion), + /// DescriptionResourceType = typeof(Strings))] + /// public partial byte Version { get; set; } + /// + /// + public string? Description { get; set; } + + /// + /// When set, is treated as a resource key and resolved + /// at runtime via this type's ResourceManager property. + /// The type must have a static ResourceManager property (as generated by .resx files). + /// + public Type? DescriptionResourceType { get; set; } + /// /// Creates a new bit field attribute with Rust-style inclusive bit range. /// /// The starting bit position (0-based, inclusive). /// The ending bit position (0-based, inclusive). Must be >= startBit. + /// Optional override for how bits are handled for this field. Defaults to . + /// If set to , the bits will be masked to zero on write. If set to + /// , the bits will be set to one on write. /// Thrown when endBit is less than startBit. - public BitFieldAttribute(int startBit, int endBit) + public BitFieldAttribute(int startBit, int endBit, MustBe mustBe = MustBe.Any) { if (endBit < startBit) throw new ArgumentException($"endBit ({endBit}) must be >= startBit ({startBit})", nameof(endBit)); - + StartBit = startBit; EndBit = endBit; + ValueOverride = mustBe; } } diff --git a/BitFieldDiagram.cs b/BitFieldDiagram.cs new file mode 100644 index 0000000..0daeebb --- /dev/null +++ b/BitFieldDiagram.cs @@ -0,0 +1,867 @@ +using System.Globalization; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices.Marshalling; +using System.Text; + +namespace Stardust.Utilities; + +using static Result; + +/// +/// Generates RFC 2360-style ASCII diagrams from metadata. +/// Works with any [BitFields] or [BitFieldsView] struct that exposes a +/// static Fields property. +/// +/// +/// +/// // Generate diagram from any BitFields/BitFieldsView struct: +/// List<string> lines = BitFieldDiagram.Render(IPv4HeaderView.Fields); +/// +/// // With descriptions legend: +/// List<string> lines = BitFieldDiagram.Render(CpuStatusRegister.Fields, includeDescriptions: true); +/// +/// // Custom width (8 or 16 bits per row): +/// List<string> lines = BitFieldDiagram.Render(CpuStatusRegister.Fields, bitsPerRow: 8); +/// +/// // Render multiple structs as a unified list with consistent scale: +/// var sections = new DiagramSection[] +/// { +/// new("Data Registers", M68020DataRegisters.Fields.ToArray()), +/// new("SR", M68020SR.Fields.ToArray()), +/// }; +/// string diagram = BitFieldDiagram.RenderListToString(sections); +/// +/// + +/// +/// Describes a labeled section for multi-struct diagram rendering via +/// and . +/// +/// Section heading displayed above the diagram (empty string for no heading). +/// The field metadata array (from the generated Fields property). +[Obsolete("Use the Type-based RenderList/RenderListToString overloads instead. Set Description on [BitFields]/[BitFieldsView] attributes to provide section labels.")] +public readonly record struct DiagramSection(string Label, BitFieldInfo[] Fields); + +public class BitFieldDiagram +{ + /// + /// Create a new diagram instance with an empty list of structs (to be added later). + /// + /// Defaults to null + /// Defaults to null + /// Defaults to 32 + /// Defaults to true + /// Defaults to false + /// Defaults to null + public BitFieldDiagram(string? description = null, string? commentPrefix = null, int bitsPerRow = 32, bool includeDescriptions = true, bool showByteOffset = false, Type? descriptionResourceType = null) + { + BitsPerRow = bitsPerRow; + IncludeDescriptions = includeDescriptions; + ShowByteOffset = showByteOffset; + CommentPrefix = commentPrefix; + Description = description; + DescriptionResourceType = descriptionResourceType; + } + + /// + /// Create a new diagram with a list of structs to render. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public BitFieldDiagram(ReadOnlySpan structs, string? description = null, string? commentPrefix = null, int bitsPerRow = 32, bool includeDescriptions = true, bool showByteOffset = false, Type? descriptionResourceType = null) + : this(description, commentPrefix, bitsPerRow, includeDescriptions, showByteOffset, descriptionResourceType) + { + foreach (var field in structs) + { + AddStruct(field).OnFailure(err => throw new ArgumentException($"Failed to add struct type '{field.Name}': {err}")); + } + } + + /// + /// Create a new diagram with struct to render. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public BitFieldDiagram(Type bitStruct, string? description = null, string? commentPrefix = null, int bitsPerRow = 32, bool includeDescriptions = true, bool showByteOffset = false, Type? descriptionResourceType = null) + : this([bitStruct], description, commentPrefix, bitsPerRow, includeDescriptions, showByteOffset, descriptionResourceType) + { + } + + /// + /// Bits per row. + /// + public virtual int BitsPerRow { get; set; } = 32; + /// + /// True to include descriptions. + /// + public virtual bool IncludeDescriptions { get; set; } = false; + /// + /// True to show byte offset to left of each row. + /// + public virtual bool ShowByteOffset { get; set; } = false; + /// + /// Comment prefix (if any) that will be prepended to the left of each line. + /// + public virtual string? CommentPrefix { get; set; } + /// + /// Description/Caption for the diagram. If is present, + /// this is the key used to retrieve the localized resource string. + /// + public virtual string? Description { get; set; } + /// + /// Optional string resource provider. If present, the is the key used to retrieve the + /// localized resource string. This line (or lines if it contains newlines) is prepended to the list of lines + /// in the diagram, with the optional prepended if present. + /// + public virtual Type? DescriptionResourceType { get; set; } + + + /// + /// Returns the resolved description string. When is set, + /// looks up as a resource key using the type's ResourceManager. + /// Otherwise returns as a literal string. + /// + /// + /// The culture to use for resource lookup. Defaults to when null. + /// + /// The resolved description, or null if no description was specified. + public virtual string? GetDescription(CultureInfo? culture = null) + { + if (Description is null) + return null; + + if (DescriptionResourceType is null) + return Description; + + var prop = DescriptionResourceType.GetProperty( + "ResourceManager", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + + if (prop?.GetValue(null) is ResourceManager rm) + return rm.GetString(Description, culture ?? CultureInfo.CurrentUICulture) ?? Description; + + return Description; + } + + /// + /// List of [BitFIelds], [BitFieldsView] or a mixture of the two. + /// + public virtual List Structs { get; } = []; + + public virtual Result AddStruct(Type structType) + { + if (structType == null) return Err("structType is null"); + if (!structType.IsBitsType()) return Err($"Struct '{structType.Name}' is not a valid [BitFields] or [BitFieldsView] type."); + + var fieldInfoResult = structType.GetFieldInfo(); + if (fieldInfoResult.IsFailure) return Err(fieldInfoResult.Error); + if (fieldInfoResult.Value.Length == 0) return Err($"Struct '{structType.Name}' has no fieldInfos."); + + // Add the struct type to the list for later rendering + Structs.Add(structType); + return Ok(); + } + + public virtual Result RenderToString() + { + var result = Render(); + if (result.IsFailure) return Result.Err(result.Error); + + return Ok(string.Join(Environment.NewLine, result.Value)); + } + + public virtual Result,string> Render() + { + if (Structs.Count == 0) return Result, string>.Err(FormatLine("(no bitStruct)", CommentPrefix)); + + // Resolve the description (handles resource lookup via DescriptionResourceType) + string? resolvedDescription = GetDescription(); + + // Render all added structs as a unified diagram. + // RenderList emits the description once as a top-level title. + var lines = RenderList([.. Structs], resolvedDescription, BitsPerRow, IncludeDescriptions, ShowByteOffset, CommentPrefix); + + return Ok(lines); + } + + /// + /// Renders an RFC-style ASCII bit field diagram from the given field metadata. + /// Bit order is detected automatically from field metadata: MSB-first fields (RFC convention) + /// show bit 0 on the left; LSB-first fields (hardware convention) show bit 0 on the right. + /// When adjacent fields have different bit orders, a new section with fresh headers is emitted. + /// + /// The field metadata list (from the generated Fields property). + /// Optional description. Ignored if is false. + /// Number of bits per diagram row. Defaults to 32. Common values: 8, 16, 32. + /// When true, appends a legend with field descriptions below the diagram. + /// When true, shows hex byte offset (e.g., 0x00) at the left of each content row. + /// Minimum cell width (characters per bit column). When 0, computed automatically. + /// Used internally by to enforce consistent scale across sections. + /// When non-null, prepended to every output line (e.g., "// " or "/// "). + /// A list of strings, one per output line. + public static List Render(ReadOnlySpan fields, string? description = null, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, int minCellWidth = 0, string? commentPrefix = null) + => RenderCore(fields, description, bitsPerRow, includeDescriptions, showByteOffset, minCellWidth, commentPrefix, emitStructDescription: true); + + /// Core rendering logic with control over StructDescription emission. + private static List RenderCore(ReadOnlySpan fields, string? description, int bitsPerRow, bool includeDescriptions, bool showByteOffset, int minCellWidth, string? commentPrefix, bool emitStructDescription) + { + if (fields.Length == 0) + { + return [FormatLine("(no fields)", commentPrefix)]; + } + + if (bitsPerRow < 1) bitsPerRow = 32; + + // Determine total bit span + int maxBit = 0; + foreach (var f in fields) + { + if (f.EndBit > maxBit) maxBit = f.EndBit; + } + + // Use the struct's declared total bits when available (so trailing undefined bits are shown) + if (fields.Length > 0 && fields[0].StructTotalBits > 0) + maxBit = Math.Max(maxBit, fields[0].StructTotalBits - 1); + int totalRows = (maxBit / bitsPerRow) + 1; + + // Build layered cell maps: layer 0 holds non-overlapping fields (first-writer-wins), + // subsequent layers hold fields displaced by overlaps. + var layers = BuildLayers(fields, maxBit + 1); + var cellMap = layers[0]; + + // Pre-scan all spans across all layers to compute minimum cell width + // that fits every named field label at its actual (per-layer) span width. + int cellWidth = Math.Max(2, minCellWidth); + bool hasUndefined = false; + for (int li = 0; li < layers.Count; li++) + { + var layerMap = layers[li]; + for (int row = 0; row < totalRows; row++) + { + int rowStart = row * bitsPerRow; + int col = 0; + while (col < bitsPerRow) + { + int bp = rowStart + col; + if (bp > maxBit) break; + var field = layerMap[bp]; + int spanStart = col; + while (col < bitsPerRow) + { + int bp2 = rowStart + col; + if (bp2 > maxBit) break; + if (layerMap[bp2] != field) break; + if (field != null && bp2 > field.EndBit) break; + col++; + } + int spanBits = col - spanStart; + if (field != null) + { + // Need: spanBits * cellWidth - 1 >= name.Length + int needed = (field.Name.Length + spanBits) / spanBits; + cellWidth = Math.Max(cellWidth, needed); + } + else if (li == 0) + { + hasUndefined = true; + } + } + } + } + + var lines = new List(); + + // Emit struct-level title as a header when provided. + if (includeDescriptions && description != null) + { + lines.Add(FormatLine(description, commentPrefix)); + } + + // Emit struct-level description as a header when descriptions are enabled + // and the caller hasn't already handled section labeling (emitStructDescription). + // Split on embedded newlines so every visual line is a separate entry + // (required for comment-prefix to be applied to each line). + if (emitStructDescription && includeDescriptions && fields.Length > 0 && fields[0].StructDescription is { } structDesc) + { + foreach (var descLine in structDesc.Split(["\r\n", "\n", "\r"], StringSplitOptions.None)) + lines.Add(descLine); + } + + int gutterWidth = showByteOffset ? 7 : 0; + string gutterBlank = new(' ', gutterWidth); + int lineWidth = 1 + bitsPerRow * cellWidth; + + BitOrder? prevBitOrder = null; + for (int row = 0; row < totalRows; row++) + { + int rowStartBit = row * bitsPerRow; + int rowEndBit = Math.Min(rowStartBit + bitsPerRow - 1, maxBit); + int rowCols = rowEndBit - rowStartBit + 1; + int byteOffset = rowStartBit / 8; + string offsetLabel = showByteOffset + ? $"0x{byteOffset:X2}".PadRight(gutterWidth) + : ""; + + // Detect bit order for this row from the first defined field + BitOrder rowOrder = prevBitOrder ?? BitOrder.BitZeroIsLsb; + for (int b = rowStartBit; b <= rowEndBit; b++) + { + var bitFieldInfo = cellMap[b]; + if (bitFieldInfo != null) + { + rowOrder = bitFieldInfo.BitOrder; + break; + } + } + bool reversed = (rowOrder == BitOrder.BitZeroIsLsb); + bool sectionChange = (prevBitOrder == null || rowOrder != prevBitOrder); + + // Emit bit-position header rows when bit order changes (or first row) + if (sectionChange) + { + if (prevBitOrder != null) + lines.Add(""); + + int headerWidth = gutterWidth + 1 + rowCols * cellWidth; + var tensChars = new char[headerWidth]; + var onesChars = new char[headerWidth]; + Array.Fill(tensChars, ' '); + Array.Fill(onesChars, ' '); + for (int col = 0; col < rowCols; col++) + { + int bitNum = reversed ? (rowCols - 1 - col) : col; + int center = gutterWidth + col * cellWidth + cellWidth / 2; + if (center < onesChars.Length) + { + onesChars[center] = (char)('0' + (bitNum % 10)); + if (bitNum % 10 == 0) + tensChars[center] = (char)('0' + (bitNum / 10)); + } + } + // Only show the tens-digit line when at least one bit number >= 10 + int maxBitNum = reversed ? (rowCols - 1) : rowCols - 1; + if (maxBitNum >= 10) + lines.Add(new string(tensChars).TrimEnd()); + lines.Add(new string(onesChars).TrimEnd()); + + // Top separator for new section + lines.Add(gutterBlank + BuildSeparator(rowCols, cellWidth)); + } + + // Field content row with byte offset label + var contentLine = new StringBuilder(gutterWidth + lineWidth); + contentLine.Append(offsetLabel); + contentLine.Append('|'); + int col2 = 0; + while (col2 < rowCols) + { + int bitPos = reversed + ? rowStartBit + rowCols - 1 - col2 + : rowStartBit + col2; + var field = cellMap[bitPos]; + int spanStart = col2; + while (col2 < rowCols) + { + int bp = reversed + ? rowStartBit + rowCols - 1 - col2 + : rowStartBit + col2; + if (cellMap[bp] != field) break; + if (field != null && (reversed ? bp < field.StartBit : bp > field.EndBit)) break; + col2++; + } + int spanLen = col2 - spanStart; + int spanCharWidth = spanLen * cellWidth - 1; + + string label; + if (field != null) + { + label = field.Name; + } + else + { + label = spanCharWidth >= 9 ? "Undefined" : "U"; + } + if (label.Length > spanCharWidth) + label = label[..spanCharWidth]; + + int totalPad = spanCharWidth - label.Length; + int leftPad = totalPad / 2; + int rightPad = totalPad - leftPad; + contentLine.Append(' ', leftPad); + contentLine.Append(label); + contentLine.Append(' ', rightPad); + contentLine.Append('|'); + } + lines.Add(contentLine.ToString()); + + // Overlay layers (for overlapping fields shown as stacked alternate rows) + for (int li = 1; li < layers.Count; li++) + { + var overlayMap = layers[li]; + + // Check if this overlay layer has any content in this row + bool hasOverlayContent = false; + for (int b = rowStartBit; b <= rowEndBit; b++) + { + if (b < overlayMap.Length && overlayMap[b] != null) + { + hasOverlayContent = true; + break; + } + } + if (!hasOverlayContent) continue; + + // Hybrid separator: dashed where overlay has content, solid elsewhere + lines.Add(gutterBlank + BuildHybridSeparator(rowCols, cellWidth, overlayMap, rowStartBit, reversed, maxBit)); + + // Overlay content row + var overlayLine = new StringBuilder(gutterWidth + lineWidth); + overlayLine.Append(gutterBlank); + overlayLine.Append('|'); + int oc = 0; + while (oc < rowCols) + { + int bitPos = reversed + ? rowStartBit + rowCols - 1 - oc + : rowStartBit + oc; + var field = (bitPos <= maxBit && bitPos < overlayMap.Length) ? overlayMap[bitPos] : null; + int spanStart = oc; + while (oc < rowCols) + { + int bp = reversed + ? rowStartBit + rowCols - 1 - oc + : rowStartBit + oc; + var current = (bp <= maxBit && bp < overlayMap.Length) ? overlayMap[bp] : null; + if (current != field) break; + if (field != null && (reversed ? bp < field.StartBit : bp > field.EndBit)) break; + oc++; + } + int spanLen = oc - spanStart; + int spanCharWidth = spanLen * cellWidth - 1; + + string label = field != null ? field.Name : ""; + if (label.Length > spanCharWidth) + label = label[..spanCharWidth]; + + int totalPad = spanCharWidth - label.Length; + int leftPad = totalPad / 2; + int rightPad = totalPad - leftPad; + overlayLine.Append(' ', leftPad); + overlayLine.Append(label); + overlayLine.Append(' ', rightPad); + overlayLine.Append('|'); + } + lines.Add(overlayLine.ToString()); + } + + // Bottom separator + lines.Add(gutterBlank + BuildSeparator(rowCols, cellWidth)); + + prevBitOrder = rowOrder; + } + + // Descriptions legend + if (includeDescriptions || hasUndefined) + { + bool hasAny = false; + if (includeDescriptions) + { + foreach (var f in fields) + { + var desc = f.GetDescription(); + string? mustBe = f.FieldMustBe switch + { + MustBe.Zero => "must be 0", + MustBe.One => "must be 1", + _ => null + }; + if (desc != null || mustBe != null) + { + if (!hasAny) + { + lines.Add(""); + hasAny = true; + } + string text = (desc, mustBe) switch + { + (not null, not null) => $"{desc} ({mustBe})", + (not null, null) => desc, + (null, not null) => mustBe, + _ => "" + }; + // Split on embedded newlines so each display line is a separate entry + string[] descLines = text.Split(["\r\n", "\n", "\r"], StringSplitOptions.None); + lines.Add($" {f.Name}: {descLines[0]}"); + for (int d = 1; d < descLines.Length; d++) + lines.Add($" {descLines[d]}"); + } + } + } + if (hasUndefined) + { + lines.Add(""); + UndefinedBitsMustBe undefinedMode = fields.Length > 0 ? fields[0].StructUndefinedMustBe : UndefinedBitsMustBe.Any; + string undefinedLegend = undefinedMode switch + { + UndefinedBitsMustBe.Zeroes => " U/Undefined = bits not defined in the struct (must be 0)", + UndefinedBitsMustBe.Ones => " U/Undefined = bits not defined in the struct (must be 1)", + _ => " U/Undefined = bits not defined in the struct" + }; + lines.Add(undefinedLegend); + } + + } + + if (commentPrefix != null) + { + for (int i = 0; i < lines.Count; i++) + lines[i] = FormatLine(lines[i], commentPrefix); + } + + return lines; + } + + /// + /// Renders the diagram as a single string with newlines. + /// + public static string RenderToString(ReadOnlySpan fields, string? title = null, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, int minCellWidth = 0, string? commentPrefix = null) + { + var lines = Render(fields, title, bitsPerRow, includeDescriptions, showByteOffset, minCellWidth, commentPrefix); + return string.Join(Environment.NewLine, lines); + } + + /// + /// Computes the minimum cell width (characters per bit column) needed to fit all field names + /// for the given fields at the specified bits-per-row. Useful for pre-computing a shared width + /// across multiple structs, though handles this automatically. + /// + public static int ComputeMinCellWidth(ReadOnlySpan fields, int bitsPerRow = 32) + { + if (fields.Length == 0 || bitsPerRow < 1) return 2; + + int maxBit = 0; + foreach (var f in fields) + { + if (f.EndBit > maxBit) maxBit = f.EndBit; + } + if (fields[0].StructTotalBits > 0) + maxBit = Math.Max(maxBit, fields[0].StructTotalBits - 1); + int totalRows = (maxBit / bitsPerRow) + 1; + + var layers = BuildLayers(fields, maxBit + 1); + + int cellWidth = 2; + foreach (var layerMap in layers) + { + for (int row = 0; row < totalRows; row++) + { + int rowStart = row * bitsPerRow; + int col = 0; + while (col < bitsPerRow) + { + int bp = rowStart + col; + if (bp > maxBit) break; + var field = layerMap[bp]; + int spanStart = col; + while (col < bitsPerRow) + { + int bp2 = rowStart + col; + if (bp2 > maxBit) break; + if (layerMap[bp2] != field) break; + if (field != null && bp2 > field.EndBit) break; + col++; + } + int spanBits = col - spanStart; + if (field != null) + { + int needed = (field.Name.Length + spanBits) / spanBits; + cellWidth = Math.Max(cellWidth, needed); + } + } + } + } + return cellWidth; + } + + /// + /// Renders an RFC-style ASCII bit field diagram for the specified [BitFields] or [BitFieldsView] type. + /// The type must have a static Fields property returning ReadOnlySpan<BitFieldInfo>. + /// + /// The struct type decorated with [BitFields] or [BitFieldsView]. + /// Number of bits per diagram row. + /// When true, appends a legend with field descriptions below the diagram. + /// When true, shows hex byte offset at the left of each content row. + /// When non-null, prepended to every output line. + /// A list of strings, one per output line. + public static List Render(Type bitFieldsType, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, string? commentPrefix = null) + { + var fieldsResult = bitFieldsType.GetFieldInfo(); + var fields = fieldsResult.IsSuccess ? fieldsResult.Value : []; + // StructDescription is already available in fields[0].StructDescription and + // emitted by the fields-based Render when includeDescriptions is true. + return Render(fields, null, bitsPerRow, includeDescriptions, showByteOffset, 0, commentPrefix); + } + + /// + /// Renders the diagram for the specified type as a single string with newlines. + /// + public static string RenderToString(Type bitFieldsType, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, string? commentPrefix = null) + { + var lines = Render(bitFieldsType, bitsPerRow, includeDescriptions, showByteOffset, commentPrefix); + return string.Join(Environment.NewLine, lines); + } + + /// + /// Renders multiple [BitFields] or [BitFieldsView] types as a unified diagram + /// with consistent cell widths. Each type's StructDescription (or simple type name when + /// no description is set) is shown as a section heading. + /// + /// + /// + /// Number of bits per diagram row. + /// When true, appends field description legends. + /// When true, shows hex byte offsets at the left. + /// When non-null, prepended to every output line. + /// A list of strings, one per output line. + public static List RenderList(ReadOnlySpan bitFieldsTypes, string? description = null, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, string? commentPrefix = null) + { + if (bitFieldsTypes.Length == 0) return [FormatLine("(no types)", commentPrefix)]; + if (bitsPerRow < 1) bitsPerRow = 32; + + var allFields = new BitFieldInfo[bitFieldsTypes.Length][]; + int sharedCellWidth = 2; + for (int i = 0; i < bitFieldsTypes.Length; i++) + { + var result = bitFieldsTypes[i].GetFieldInfo(); + allFields[i] = result.IsSuccess ? result.Value : []; + int w = ComputeMinCellWidth(allFields[i], bitsPerRow); + sharedCellWidth = Math.Max(sharedCellWidth, w); + } + + var lines = new List(); + if (description != null) + { + lines.Add(FormatLine(description, commentPrefix)); + } + for (int i = 0; i < allFields.Length; i++) + { + var fields = allFields[i]; + + if (lines.Count > 0) lines.Add(FormatLine("", commentPrefix)); + + if (includeDescriptions) + { + string? structDesc = fields.Length > 0 ? fields[0].StructDescription : null; + + if (description == null) + { + // No top-level title: emit type name as section heading. + // StructDescription (if any) follows on the next line. + lines.Add(FormatLine(bitFieldsTypes[i].Name, commentPrefix)); + if (structDesc != null) + { + foreach (var descLine in structDesc.Split(["\r\n", "\n", "\r"], StringSplitOptions.None)) + lines.Add(FormatLine(descLine, commentPrefix)); + } + } + else if (structDesc != null && structDesc != description) + { + // Top-level title exists and StructDescription differs: emit as section heading. + foreach (var descLine in structDesc.Split(["\r\n", "\n", "\r"], StringSplitOptions.None)) + lines.Add(FormatLine(descLine, commentPrefix)); + } + // else: structDesc matches top-level description or is null → skip to avoid duplication + } + lines.AddRange(RenderCore(fields, null, bitsPerRow, includeDescriptions, showByteOffset, sharedCellWidth, commentPrefix, emitStructDescription: false)); + } + return lines; + } + + /// + /// Renders multiple types as a single string with consistent cell widths. + /// + public static string RenderListToString(ReadOnlySpan bitFieldsTypes, string? title = null, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, string? commentPrefix = null) + { + var lines = RenderList(bitFieldsTypes, title, bitsPerRow, includeDescriptions, showByteOffset, commentPrefix); + return string.Join(Environment.NewLine, lines); + } + + /// + /// Renders multiple struct sections as a unified diagram list with consistent cell widths. + /// The widest field name across all sections determines the scale for the entire output. + /// + /// The labeled sections to render sequentially. + /// Number of bits per diagram row. + /// When true, appends field description legends. + /// When true, shows hex byte offsets at the left. + /// When non-null, prepended to every output line (e.g., "// " or "/// "). + /// A list of strings, one per output line. + [Obsolete("Use RenderList(bitsPerRow, includeDescriptions, showByteOffset, commentPrefix, params Type[]) instead. Set Description on [BitFields]/[BitFieldsView] attributes to provide section labels.")] + public static List RenderList(ReadOnlySpan sections, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, string? commentPrefix = null) + { + if (sections.Length == 0) return [FormatLine("(no sections)", commentPrefix)]; + if (bitsPerRow < 1) bitsPerRow = 32; + + // Compute shared cell width across all sections + int sharedCellWidth = 2; + foreach (var section in sections) + { + int w = ComputeMinCellWidth(section.Fields, bitsPerRow); + sharedCellWidth = Math.Max(sharedCellWidth, w); + } + + var lines = new List(); + foreach (var section in sections) + { + if (section.Label.Length > 0) + { + if (lines.Count > 0) lines.Add(FormatLine("", commentPrefix)); + lines.Add(FormatLine(section.Label, commentPrefix)); + } + lines.AddRange(Render(section.Fields, null, bitsPerRow, includeDescriptions, showByteOffset, sharedCellWidth, commentPrefix)); + } + return lines; + } + + /// + /// Renders multiple struct sections as a single string with consistent cell widths. + /// + [Obsolete("Use RenderListToString(bitsPerRow, includeDescriptions, showByteOffset, commentPrefix, params Type[]) instead.")] + public static string RenderListToString(ReadOnlySpan sections, int bitsPerRow = 32, bool includeDescriptions = false, bool showByteOffset = true, string? commentPrefix = null) + { + var lines = RenderList(sections, bitsPerRow, includeDescriptions, showByteOffset, commentPrefix); + return string.Join(Environment.NewLine, lines); + } + + /// + /// Retrieves the Fields metadata from a [BitFields] or [BitFieldsView] type. + /// + /// A struct type decorated with [BitFields] or [BitFieldsView]. + /// A successful result containing the field metadata array, or an error string on failure. + [Obsolete("Use the extension method type.GetFieldInfo() from Stardust.Utilities.Extensions instead.")] + public static Result GetFieldInfo(Type bitFieldsType) => + bitFieldsType.GetFieldInfo(); + + /// + /// Formats the specified string by prepending an optional prefix to each line. + /// + /// Each line in the input string is processed individually. If the prefix parameter is null, + /// lines are returned without any prefix. + /// The string to be formatted. May contain multiple lines separated by newline characters. + /// An optional prefix to prepend to each line. If null, no prefix is added. + /// A formatted string in which each line is prefixed by the specified prefix, if provided. + private static string FormatLine(string line, string? prefix) + { + string[] lines = line.Split(Environment.NewLine); + var sb = new StringBuilder(); + bool first = true; + foreach (var ln in lines) + { + if (first) + { + first = false; + } + else + { + sb.AppendLine(ln); + } + if (prefix != null) + { + sb.Append(prefix); + } + sb.Append(ln); + } + return sb.ToString(); + } + + private static string BuildSeparator(int bitsPerRow, int cellWidth) + { + var sb = new StringBuilder(1 + bitsPerRow * cellWidth); + sb.Append('+'); + for (int i = 0; i < bitsPerRow; i++) + { + sb.Append('-', cellWidth - 1); + sb.Append('+'); + } + return sb.ToString(); + } + + /// + /// Builds a separator where bit positions covered by the overlay layer use dashed lines + /// (alternating dash-space) and uncovered positions use solid dashes. + /// + private static string BuildHybridSeparator(int cols, int cellWidth, BitFieldInfo?[] overlayMap, int rowStartBit, bool reversed, int maxBit) + { + var sb = new StringBuilder(1 + cols * cellWidth); + sb.Append('+'); + for (int col = 0; col < cols; col++) + { + int bitPos = reversed + ? rowStartBit + cols - 1 - col + : rowStartBit + col; + bool isDashed = bitPos <= maxBit && bitPos < overlayMap.Length && overlayMap[bitPos] != null; + if (isDashed) + { + for (int c = 0; c < cellWidth - 1; c++) + sb.Append(c % 2 == 0 ? '-' : ' '); + } + else + { + sb.Append('-', cellWidth - 1); + } + sb.Append('+'); + } + return sb.ToString(); + } + + /// + /// Assigns each field to the lowest available layer where none of its bits are already claimed. + /// Layer 0 holds non-overlapping fields (first-writer-wins); displaced fields go to higher layers. + /// + private static List BuildLayers(ReadOnlySpan fields, int mapSize) + { + var layers = new List { new BitFieldInfo?[mapSize] }; + foreach (var f in fields) + { + int targetLayer = -1; + for (int li = 0; li < layers.Count; li++) + { + bool canFit = true; + for (int b = f.StartBit; b <= f.EndBit && b < mapSize; b++) + { + if (layers[li][b] != null) + { + canFit = false; + break; + } + } + if (canFit) + { + targetLayer = li; + break; + } + } + if (targetLayer < 0) + { + targetLayer = layers.Count; + layers.Add(new BitFieldInfo?[mapSize]); + } + for (int b = f.StartBit; b <= f.EndBit && b < mapSize; b++) + layers[targetLayer][b] = f; + } + return layers; + } +} diff --git a/BitFieldInfo.cs b/BitFieldInfo.cs new file mode 100644 index 0000000..384daaf --- /dev/null +++ b/BitFieldInfo.cs @@ -0,0 +1,214 @@ +using System.Globalization; +using System.Resources; + +namespace Stardust.Utilities; +using static Result; + +/// +/// Describes a single field or flag within a bitfield struct, providing its name, +/// bit position, width, type, effective endianness, and optional description at runtime. +/// +/// The property name of the field. +/// The starting bit position (inclusive, 0-based, as declared by the user). +/// The number of bits in the field (1 for flags). +/// The fully qualified CLR type name of the property (e.g., "byte", "bool", "ushort"). +/// True if this is a single-bit flag; false for a field. +/// The effective byte order for this field (struct-level default or per-field override). +/// The effective bit ordering for this field (struct-level). +/// +/// An optional description string or resource key. +/// When is null, this is a literal string. +/// When set, this is a resource key resolved by . +/// +/// +/// When set, is treated as a resource key and resolved +/// via this type's static ResourceManager property. +/// +/// +/// The total number of bits in the containing struct (e.g., 16 for typeof(ushort), +/// 256 for [BitFields(256)]). Used by diagram renderers to show undefined trailing bits. +/// +/// +/// Per-field MustBe constraint: Any = no constraint, Zero = must be zero, Ones = must be all ones. +/// +/// +/// Struct-level UndefinedBitsMustBe: Any = any, Zeroes = zeroes, Ones = ones. +/// +/// +/// An optional description of the containing struct, from the [BitFields] or [BitFieldsView] +/// attribute's Description property. Used as a section label in multi-struct diagram rendering. +/// +public sealed record BitFieldInfo( + string Name, + int StartBit, + int BitLength, + string PropertyType, + bool IsFlag, + ByteOrder ByteOrder = ByteOrder.LittleEndian, + BitOrder BitOrder = BitOrder.BitZeroIsLsb, + string? Description = null, + Type? DescriptionResourceType = null, + int StructTotalBits = 0, + MustBe FieldMustBe = MustBe.Any, + UndefinedBitsMustBe StructUndefinedMustBe = UndefinedBitsMustBe.Any, + string? StructDescription = null) +{ + public static Result Create(Type type, string? field = null, bool inherit = true) + { + string name; + int startBit; + int bitLength; + string propertyType; + bool isFlag; + ByteOrder byteOrder = ByteOrder.LittleEndian; + BitOrder bitOrder = BitOrder.BitZeroIsLsb; + string? description = null; + Type? descriptionResourceType = null; + int structTotalBits; + MustBe fieldMustBe = MustBe.Any; + UndefinedBitsMustBe structUndefinedMustBe = UndefinedBitsMustBe.Any; + string? structDescription = null; + + if (type == null) + { + return Result.Err("Type cannot be null."); + } + + if (!type.IsBitsType()) + { + return Result.Err($"Type '{type.FullName}' is not a valid bitfield struct type."); + } + var fieldInfo = field == null ? null : type.GetProperty(field); + if (field != null && fieldInfo == null) + { + return Result.Err($"Field '{field}' not found in type '{type.FullName}'."); + } + + // At this point, we have a valid type and (if specified) a valid field. + // We can proceed to extract all the relevant information. + + // Description is the same for either struct or field + var descRes = type.GetBitsDescription(field, inherit); + if (descRes.IsFailure) return Result.Err(descRes.Error); + description = descRes.Value.description; + descriptionResourceType = descRes.Value.descriptionResourceType; + if (field == null) + { + // For struct-level description, also set structDescription for convenience + structDescription = description; + } + else + { + // For field-level description, get struct-level description separately for convenience + var fldDescRes = type.GetBitsDescription(null, inherit); + if (fldDescRes.IsFailure) return Result.Err(fldDescRes.Error); + structDescription = fldDescRes.Value.description; + } + var byteOrderRes = type.GetBitAndByteOrder(inherit); + + // Undefined bits defined at the struct level. + type.GetUndefinedBitsMustBe(inherit).Match( + onSuccess: mustBe => structUndefinedMustBe = mustBe, + onFailure: _ => structUndefinedMustBe = UndefinedBitsMustBe.Any + ); + var bitLengthRes = type.GetBitLength(field, inherit); + if (bitLengthRes.IsFailure) return Result.Err(bitLengthRes.Error); + bitLength = bitLengthRes.Value; + + if (field != null) + { + var structTotalBitsRes = type.GetBitLength(null, inherit); + if (structTotalBitsRes.IsFailure) return Result.Err(structTotalBitsRes.Error); + structTotalBits = structTotalBitsRes.Value; + } + else + { + structTotalBits = bitLength; + } + + if (field != null) + { + name = field; + propertyType = fieldInfo!.PropertyType.FullName ?? fieldInfo.PropertyType.Name; + + var seBitsRes = type.GetStartAndEndBits(field, inherit); + if (seBitsRes.IsFailure) return Result.Err(seBitsRes.Error); + + startBit = seBitsRes.Value.startBit; + bitLength = seBitsRes.Value.endBit - startBit + 1; + type.GetFieldValueOverride(field, inherit).OnSuccess(mustBe => fieldMustBe = mustBe); + var fieldAttr = type.GetAttribute(field, inherit); + if (fieldAttr != null) + { + isFlag = false; + } + else + { + var flagAttr = type.GetAttribute(field, inherit); + if (flagAttr != null) + { + isFlag = true; + } + else + { + return Result.Err($"Field '{field}' in type '{type.FullName}' is missing both [BitField] and [BitFlag] attributes."); + } + } + structDescription = description; + } + else + { + // Dealing with the struct itself + name = type.Name; + propertyType = type.FullName ?? type.Name; + startBit = 0; + isFlag = false; + } + + return Ok(new BitFieldInfo( + Name: name, + StartBit: startBit, + BitLength: bitLength, + PropertyType: propertyType, + IsFlag: isFlag, + ByteOrder: byteOrder, + BitOrder: bitOrder, + Description: description, + DescriptionResourceType: descriptionResourceType, + StructTotalBits: structTotalBits, + FieldMustBe: fieldMustBe, + StructUndefinedMustBe: structUndefinedMustBe, + StructDescription: structDescription + )); + } + + /// The ending bit position (inclusive, 0-based, as declared by the user). + public int EndBit => StartBit + BitLength - 1; + + /// + /// Returns the resolved description string. When is set, + /// looks up as a resource key using the type's ResourceManager. + /// Otherwise returns as a literal string. + /// + /// + /// The culture to use for resource lookup. Defaults to when null. + /// + /// The resolved description, or null if no description was specified. + public string? GetDescription(CultureInfo? culture = null) + { + if (Description is null) + return null; + + if (DescriptionResourceType is null) + return Description; + + var prop = DescriptionResourceType.GetProperty( + "ResourceManager", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + + if (prop?.GetValue(null) is ResourceManager rm) + return rm.GetString(Description, culture ?? CultureInfo.CurrentUICulture) ?? Description; + + return Description; + } +} diff --git a/BitFieldsAttribute.cs b/BitFieldsAttribute.cs index ff2b796..4facb57 100644 --- a/BitFieldsAttribute.cs +++ b/BitFieldsAttribute.cs @@ -13,14 +13,33 @@ namespace Stardust.Utilities; /// [BitFlag(7)] public partial bool Flag { get; set; } /// } /// +/// // Equivalent: use the StorageType enum for compile-time safety +/// [BitFields(StorageType.Byte)] +/// public partial struct MyRegister2 { ... } +/// /// // Usage with implicit conversions /// MyRegister reg = 0xFF; // From byte /// byte raw = reg; // To byte /// var reg2 = new MyRegister(0x55); // Constructor +/// +/// // With undefined bits handling: +/// [BitFields(typeof(ushort), UndefinedBitsMustBe.Zeroes)] +/// public partial struct ProtocolHeader { ... } +/// +/// // Arbitrary-size bitfields (multi-word backing store): +/// [BitFields(200)] // 200 bits backed by 4 x ulong +/// public partial struct WideRegister { ... } +/// +/// // MSB-first bit numbering (for datasheets that number from the MSB): +/// [BitFields(typeof(byte), bitOrder: BitOrder.BitZeroIsMsb)] +/// public partial struct MsbRegister +/// { +/// [BitField(0, 3)] public partial byte HighNibble { get; set; } // top 4 bits +/// } /// /// The generator creates: /// -/// A private Value field of the specified storage type +/// A private Value field of the specified storage type (or multiple ulong fields for arbitrary sizes) /// A constructor taking the storage type /// Property implementations with inline bit manipulation /// Implicit conversion operators to/from the storage type @@ -30,16 +49,122 @@ namespace Stardust.Utilities; public sealed class BitFieldsAttribute : Attribute { /// - /// The storage type (byte, ushort, uint, ulong, sbyte, short, int, or long). + /// The storage type (byte, ushort, uint, ulong, nint, nuint, Half, float, double, decimal, UInt128, Int128, sbyte, short, int, or long). + /// Null when using the bit-count constructor for arbitrary-size bitfields. + /// + public Type? StorageType { get; } + + /// + /// The total number of bits in the bitfield. Set when using the bit-count constructor. + /// Zero when using the storage type constructor (bit count is inferred from the type). + /// + public int BitCount { get; } + + /// + /// Specifies how undefined bits (bits not covered by any field or flag) are handled. + /// Default is which preserves raw data. + /// + public UndefinedBitsMustBe UndefinedBits { get; } + + /// + /// The byte order used for serialization (ReadFrom/WriteTo/ToByteArray). + /// Default is (x86 native). + /// Use for network protocols or big-endian hardware. /// - public Type StorageType { get; } + /// + /// This controls the byte order of ReadFrom, WriteTo, TryWriteTo, and ToByteArray. + /// When a [BitFields] struct is nested inside a [BitFieldsView], the struct's + /// ByteOrder overrides the view's default byte order for that field. + /// + public ByteOrder ByteOrder { get; } + + /// + /// The bit numbering convention used for field positions. + /// Default is (hardware register convention: bit 0 = least significant bit). + /// + public BitOrder BitOrder { get; } + + /// + /// An optional description of the struct, used as a section label in + /// multi-struct diagrams. + /// + public string? Description { get; set; } + + /// + /// An optional resource type for the Description property, allowing localization of struct descriptions in BitFieldDiagram. + /// + public Type? DescriptionResourceType { get; set; } /// /// Creates a BitFields attribute with the specified storage type. /// - /// The storage type (byte, ushort, uint, ulong, sbyte, short, int, or long). - public BitFieldsAttribute(Type storageType) + /// The storage type (byte, ushort, uint, ulong, nint, nuint, Half, float, double, decimal, UInt128, Int128, sbyte, short, int, or long). + /// Specifies how undefined bits are handled. Defaults to . + /// Bit numbering convention. Defaults to . + /// Byte order for serialization. Defaults to . + public BitFieldsAttribute(Type storageType, UndefinedBitsMustBe undefinedBits = UndefinedBitsMustBe.Any, BitOrder bitOrder = BitOrder.BitZeroIsLsb, ByteOrder byteOrder = ByteOrder.LittleEndian) { StorageType = storageType; + UndefinedBits = undefinedBits; + BitOrder = bitOrder; + ByteOrder = byteOrder; + } + + /// + /// Creates a BitFields attribute with the specified storage type using the enum. + /// This overload provides compile-time safety and discoverability for the supported storage types. + /// + /// The storage type as an enum value. + /// Specifies how undefined bits are handled. Defaults to . + /// Bit numbering convention. Defaults to . + /// Byte order for serialization. Defaults to . + public BitFieldsAttribute(StorageType storageType, UndefinedBitsMustBe undefinedBits = UndefinedBitsMustBe.Any, BitOrder bitOrder = BitOrder.BitZeroIsLsb, ByteOrder byteOrder = ByteOrder.LittleEndian) + { + StorageType = MapToType(storageType); + UndefinedBits = undefinedBits; + BitOrder = bitOrder; + ByteOrder = byteOrder; } + + /// + /// Creates a BitFields attribute with an arbitrary bit count. + /// The backing store is generated as multiple ulong fields, rounded up to the next 64-bit boundary. + /// Maximum supported size is 16,384 bits (2,048 bytes). + /// + /// The number of bits in the bitfield (1 to 16,384). + /// Specifies how undefined bits are handled. Defaults to . + /// Bit numbering convention. Defaults to . + /// Byte order for serialization. Defaults to . + public BitFieldsAttribute(int bitCount, UndefinedBitsMustBe undefinedBits = UndefinedBitsMustBe.Any, BitOrder bitOrder = BitOrder.BitZeroIsLsb, ByteOrder byteOrder = ByteOrder.LittleEndian) + { + BitCount = bitCount; + UndefinedBits = undefinedBits; + BitOrder = bitOrder; + ByteOrder = byteOrder; + } + + /// + /// Maps a enum value to the corresponding . + /// + private static Type? MapToType(StorageType storageType) => storageType switch + { + Utilities.StorageType.Byte => typeof(byte), + Utilities.StorageType.SByte => typeof(sbyte), + Utilities.StorageType.Int16 => typeof(short), + Utilities.StorageType.UInt16 => typeof(ushort), + Utilities.StorageType.Int32 => typeof(int), + Utilities.StorageType.UInt32 => typeof(uint), + Utilities.StorageType.Int64 => typeof(long), + Utilities.StorageType.UInt64 => typeof(ulong), + Utilities.StorageType.NInt => typeof(nint), + Utilities.StorageType.NUInt => typeof(nuint), + Utilities.StorageType.Half => typeof(Half), + Utilities.StorageType.Single => typeof(float), + Utilities.StorageType.Double => typeof(double), + Utilities.StorageType.Decimal => typeof(decimal), + Utilities.StorageType.Int128 => typeof(Int128), + Utilities.StorageType.UInt128 => typeof(UInt128), + _ => null, + }; } + diff --git a/BitFieldsViewAttribute.cs b/BitFieldsViewAttribute.cs new file mode 100644 index 0000000..a5a5ea2 --- /dev/null +++ b/BitFieldsViewAttribute.cs @@ -0,0 +1,81 @@ +namespace Stardust.Utilities; + +/// +/// Marks a partial record struct as a zero-copy view over a Memory<byte> buffer, +/// enabling source generation for bitfield properties that read and write directly into the underlying memory. +/// +/// +/// +/// Unlike which generates value-type structs with inline storage, +/// [BitFieldsView] generates a record struct that wraps a Memory<byte> reference. +/// This enables zero-copy access to protocol headers, file formats, and other binary data of arbitrary size. +/// +/// +/// The generated struct includes: +/// +/// A Memory<byte> field referencing the external buffer +/// Constructors accepting Memory<byte> and byte[] +/// Property implementations that read/write directly through the span +/// A SizeInBytes constant for the minimum required buffer size +/// +/// +/// +/// +/// // Default: little-endian, LSB-first (matches [BitFields] convention) +/// [BitFieldsView] +/// public partial record struct RegisterView +/// { +/// [BitField(0, 7)] public partial byte LowByte { get; set; } +/// [BitField(8, 15)] public partial byte HighByte { get; set; } +/// } +/// +/// // Network protocol: big-endian, MSB-first (RFC convention) +/// [BitFieldsView(ByteOrder.BigEndian, BitOrder.BitZeroIsMsb)] +/// public partial record struct IPv6Header +/// { +/// [BitField(0, 3)] public partial byte Version { get; set; } +/// [BitField(4, 11)] public partial byte TrafficClass { get; set; } +/// [BitField(12, 31)] public partial uint FlowLabel { get; set; } +/// } +/// +/// +/// +[AttributeUsage(AttributeTargets.Struct)] +public sealed class BitFieldsViewAttribute : Attribute +{ + /// + /// The byte order used for multi-byte field access. + /// Default is (native byte order), matching . + /// Use for network protocols. + /// + public ByteOrder ByteOrder { get; } + + /// + /// The bit numbering convention used for field positions. + /// Default is (bit 0 = least significant), matching . + /// Use for RFC/network protocol conventions. + /// + public BitOrder BitOrder { get; } + + /// + /// An optional description of the struct, used as a section label in + /// multi-struct diagrams. + /// + public string? Description { get; set; } + + /// + /// An optional resource type for the Description property, allowing localization of struct descriptions in BitFieldDiagram. + /// + public Type? DescriptionResourceType { get; set; } + + /// + /// Creates a BitFieldsView attribute with the specified byte order and bit order. + /// + /// Byte order for multi-byte field access. Defaults to . + /// Bit numbering convention. Defaults to . + public BitFieldsViewAttribute(ByteOrder byteOrder = ByteOrder.LittleEndian, BitOrder bitOrder = BitOrder.BitZeroIsLsb) + { + ByteOrder = byteOrder; + BitOrder = bitOrder; + } +} diff --git a/BitFlagAttribute.cs b/BitFlagAttribute.cs index 931815d..30257c0 100644 --- a/BitFlagAttribute.cs +++ b/BitFlagAttribute.cs @@ -28,12 +28,36 @@ public sealed class BitFlagAttribute : Attribute /// public int Bit { get; } + /// + /// Normally set to and has no effect. + /// When set to or , it overrides the + /// flag's value. On write and during conversion of the underlying BitFields struct to/from other + /// types, the flag's bit will be forced to zero or one respectively. + /// + public MustBe ValueOverride { get; } + + /// + /// An optional human-readable description of this flag. + /// When is null, this is a literal string. + /// When is set, this is a resource key + /// resolved at runtime via the resource type's ResourceManager. + /// + public string? Description { get; set; } + + /// + /// When set, is treated as a resource key and resolved + /// at runtime via this type's ResourceManager property. + /// The type must have a static ResourceManager property (as generated by .resx files). + /// + public Type? DescriptionResourceType { get; set; } + /// /// Creates a new bit flag attribute. /// /// The bit position (0-based). - public BitFlagAttribute(int bit) + public BitFlagAttribute(int bit, MustBe mustBe = MustBe.Any) { Bit = bit; + ValueOverride = mustBe; } } diff --git a/BitOrder.cs b/BitOrder.cs new file mode 100644 index 0000000..2e061cd --- /dev/null +++ b/BitOrder.cs @@ -0,0 +1,36 @@ +namespace Stardust.Utilities; + +/// +/// Specifies how bit positions are numbered within a +/// or a . +/// +public enum BitOrder +{ + /// + /// MSB-first (RFC/network convention). Bit 0 is the most significant bit of byte 0. + /// This matches how protocol fields are described in RFCs and network specifications. + /// Example (IPv6 header): [BitField(0, 3)] = Version (top nibble of byte 0). + /// + BitZeroIsMsb = 0, + + /// + /// RFC (network) order. Bit 0 is the most significant bit of byte 0. + /// This matches how protocol fields are described in RFCs and network specifications. + /// Example (IPv6 header): [BitField(0, 3)] = Version (top nibble of byte 0). + /// + RfcNetworkOrder = BitZeroIsMsb, + + /// + /// LSB-first (hardware/register convention). Bit 0 is the least significant bit of byte 0. + /// This matches the convention used by for hardware registers. + /// Example: [BitField(0, 3)] = bottom nibble of byte 0. + /// + BitZeroIsLsb = 1, + + /// + /// LSB-first (hardware/register convention). Bit 0 is the least significant bit of byte 0. + /// This matches the convention used by for hardware registers. + /// Example: [BitField(0, 3)] = bottom nibble of byte 0. + /// + HwRegisterOrder = BitZeroIsLsb, +} diff --git a/BitStream.cs b/BitStream.cs deleted file mode 100644 index 1d73393..0000000 --- a/BitStream.cs +++ /dev/null @@ -1,520 +0,0 @@ -using System; -using System.IO; - -namespace Stardust.Utilities -{ - /// - /// BitStream class. Allows reading and writing individual bits as well as - /// byte, ushort, and uint. - /// - public class BitStream : Stream - { - private byte[] _bits; - private long _position = -1; - private long _capacity; - private long _length = 0; - - /// - /// Create instance of class with - /// default capacity of 2k bits. - /// - public BitStream() - { - _bits = new byte[256]; - _capacity = _bits.Length * 8; - } - - /// - /// Create instance of class with - /// capacity of bits. - /// - /// - public BitStream(long capacity) - { - _bits = new byte[capacity / 8 + (capacity % 8 == 0 ? 0 : 1)]; - _capacity = capacity; - } - - /// - /// Capacity in bits. - /// - public long Capacity - { - get => _capacity; - set - { - SetCapacity(value); - } - } - - /// - /// Set capacity in bits. - /// - /// must be > 0 - private void SetCapacity(long capacity) - { - if (capacity <= 0) - { - throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); - } - if (capacity == Capacity) - { - return; - } - - // Copy bytes. There are some corner cases where - // the new capacity could result in the same number - // of bytes, but those are not worth the effort to - // save a copy operation since the default behavior - // when writing to the stream is to double capacity - // when increasing it. See EnsureCapacity. - byte[] newBits = new byte[capacity / 8 + (capacity % 8 == 0 ? 0 : 1)]; - int byteCount = Math.Min(_bits.Length, newBits.Length); - Span fromSpan = new Span(_bits, 0, byteCount); - Span toSpan = new Span(newBits, 0, byteCount); - fromSpan.CopyTo(toSpan); - _bits = newBits; - - _capacity = capacity; - if (_length > capacity) - { - _length = capacity; - } - if (_position > _length - 1) - { - // _position can be -1 if no bits. - _position = _length - 1; - } - } - - /// - /// True if can read from the stream. - /// - public override bool CanRead => true; - - /// - /// True if can seek in the stream. - /// - public override bool CanSeek => true; - - /// - /// True if can write to the stream. - /// - public override bool CanWrite => true; - - /// - /// Length of the stream in bits (used). - /// - public override long Length => _length; - - /// - /// Position in the stream in bits. - /// - public override long Position - { - get => _position; - set - { - if (value < 0) - { - // Set to -1 if no bits. - if (_length == 0) - { - _position = -1; - } - else - { - // Limit to 0 if there are bits - _position = 0; - } - } - else - { - if (value > _length) - { - _position = _length; - } - else - { - _position = value; - } - } - } - } - - /// - /// Ensure enough capacity. - /// - /// - /// - private void EnsureCapacity(long value) - { - if (value <= _capacity) - { - return; - } - if (value > (long)Array.MaxLength * 8) - { - throw new ArgumentException("Capacity overflow - too large", nameof(value)); - } - if (value < _capacity * 2) - { - // Grow by factor of 2 each time - value = _capacity * 2; - } - - // Limit to maximum array size when doubling. - // value is guaranteed to be <= Array.MaxLength * 8. - Capacity = Math.Min(value, (long)Array.MaxLength * 8); - } - - /// - /// Has no effect since the stream is in memory without - /// backing store. - /// - public override void Close() - { - // Empty method to prevent calling Dispose, etc. - } - - /// - /// Has no effect since the stream is in memory without - /// backing store. - /// - public override void Flush() - { - } - - /// - /// Read a bit at the current and increment the - /// Position. - /// - /// - public bool Read() - { - if (Position <= _capacity - 1) - { - bool bit = ReadBit(_bits, (int)Position); - _position++; - return bit; - } - throw new ArgumentException($"Position out of range: {Position}"); - } - - /// - /// Read a bit, return 1 if bit read, -1 if not. - /// Safe even at end of stream. - /// - /// - /// 1 if bit returned, else 0 - public int Read(out bool bit) - { - if (Position <= _capacity - 1) - { - bit = ReadBit(_bits, (int)Position); - _position++; - return 1; - } - bit = false; - return -1; - } - - /// - /// Read bits from the stream into the passed-in buffer starting - /// from . Advances Position by the number of - /// bits read (i.e., count * 8 bits). - /// - /// output buffer - /// starting offset in the output buffer - /// number of bytes to read - /// number of bytes or -1 if end of stream reached - public override int Read(byte[] buffer, int offset, int count) - { - if (_position > _length - 1) - { - return -1; - } - int outPos = offset * 8; - int bitCount = count * 8; - int byteCount = 0; - for (int i = 0; i < bitCount; i++) - { - if (_position > _length - 1) - { - break; - } - bool bit = ReadBit(_bits, _position++); - WriteBit(buffer, outPos++, bit); - if (i % 8 == 0) - { - byteCount++; - } - } - return byteCount; - } - - /// - /// Return the byte starting at the current position and - /// increment position by 8. - /// - /// -1 if not enough bits to fill a byte, otherwise byte value - public override int ReadByte() - { - int result = -1; - if (Position >= 0 && Length >= Position + 8) - { - byte value = 0; - for (int bit = 0; bit < 8; bit++) - { - if (Read()) - { - value |= (byte)(1 << bit); - } - } - result = value; - } - return result; - } - - /// - /// Copy bytes from one buffer to another buffer. - /// - /// - /// - /// - /// - /// - /// - private void CopyBytes(byte[] fromBuffer, int fromOffset, - byte[] toBuffer, int toOffset, int byteCount) - { - // Check bounds very carefully first. - if (fromBuffer.Length < fromOffset + byteCount || - fromOffset < 0 || - toBuffer.Length < toOffset + byteCount || - toOffset < 0 || - byteCount < 0) - { - throw new ArgumentException("CopyBytes: Arguments out of range"); - } - - // Copy bytes - Span fromSpan = new Span(fromBuffer, fromOffset, byteCount); - Span toSpan = new Span(toBuffer, toOffset, byteCount); - fromSpan.CopyTo(toSpan); - } - - /// - /// Copy bits. - /// - /// - /// - /// - /// - /// - private void CopyBits(byte[] fromBuffer, int fromBitPosition, - byte[] toBuffer, int toBitPosition, int bitCount) - { - if (bitCount < 0) - { - throw new ArgumentException($"{nameof(bitCount)} < 0"); - } - if (fromBitPosition < 0 || toBitPosition < 0) - { - throw new ArgumentException($"{nameof(fromBitPosition)} and/or {nameof(toBitPosition)} out of range"); - } - - if (fromBitPosition % 8 == 0 && toBitPosition % 8 == 0) - { - int byteCount = bitCount / 8; - int fromOffset = fromBitPosition / 8; - int toOffset = toBitPosition / 8; - - // Just copy the bytes - CopyBytes(fromBuffer, fromOffset, toBuffer, toOffset, byteCount); - - int remainderBits = bitCount % 8; - if (remainderBits > 0) - { - // Finish up the last partial byte. - for (int i = 0; i < remainderBits; i++) - { - bool bit = ReadBit(fromBuffer, fromOffset * 8 + i); - WriteBit(toBuffer, toOffset * 8 + i, bit); - } - } - } - else - { - int fromByteCount = (fromBitPosition + bitCount) / 8 + ((fromBitPosition + bitCount) % 8) != 0 ? 1 : 0; - int toByteCount = (toBitPosition + bitCount) / 8 + ((toBitPosition + bitCount) % 8) != 0 ? 1 : 0; - - if (fromBuffer.Length < fromByteCount || - toBuffer.Length < toByteCount) - { - throw new ArgumentException($"CopyBits: Too many bits for the size of the array(s)"); - } - - // Do it the slow way - for (int i = 0; i < bitCount; i++) - { - bool bit = ReadBit(fromBuffer, fromBitPosition + i); - WriteBit(toBuffer, toBitPosition + i, bit); - } - } - } - - /// - /// Allow public access to the bit buffer. - /// - /// - public byte[] GetBuffer() - { - return _bits; - } - - /// - /// Truncate the stream by bits starting at - /// the . Does not change the Capacity. - /// Position is set to the same bit as it started out unless - /// that would push it off the end, in which case it is set to the - /// last bit. Length is reduced by . - /// - /// - /// Begin and End are supported, but Current is not supported and - /// throws an exception. - public void Truncate(int bitCount, SeekOrigin origin = SeekOrigin.Begin) - { - if (_position < 0) - { - return; - } - int length = (int)Length - bitCount; - BitStream temp = new BitStream(_capacity); - byte[] toBuffer = temp.GetBuffer(); - switch (origin) - { - case SeekOrigin.Begin: - CopyBits(_bits, bitCount, toBuffer, 0, length); - _bits = toBuffer; - _position = Math.Max(0, _position - bitCount); - break; - case SeekOrigin.Current: - throw new NotSupportedException("SeekOrigin.Current not supported"); - case SeekOrigin.End: - // No action required - _length and _position handled below - break; - default: - break; - } - _length = length; - _position = Math.Min(_position, _length - 1); - } - - /// - /// Move the Position to the specified location. - /// - /// - /// - /// - public override long Seek(long offset, SeekOrigin origin) - { - switch (origin) - { - case SeekOrigin.Begin: - Position = offset; - break; - case SeekOrigin.Current: - Position = Position + offset; - break; - case SeekOrigin.End: - Position = Length - 1 + offset; - break; - default: - break; - }; - return Position; - } - - /// - /// Set the length in bits. - /// - /// - public override void SetLength(long value) - { - EnsureCapacity(value); - _length = value; - if (_position > _length - 1) - { - _position = Math.Max(0, _length - 1); - } - } - - /// - /// Write one bit if there is room and return 1, else return 0 - /// if at the maximum length of the stream. - /// - /// - /// Number of bits written (0 or 1) - public int Write(bool bit) - { - long nextPos = Position + 1; - EnsureCapacity(nextPos + 1); - - WriteBit(_bits, (int)nextPos, bit); - _length++; - Position = nextPos; - return 1; - } - - /// - /// Write (offset in bytes, count in bytes) - /// - /// - /// - /// - public override void Write(byte[] buffer, int offset, int count) - { - for (int i = 0; i < count * 8; i++) - { - int bitPosition = offset * 8 + i; - bool bit = ReadBit(buffer, bitPosition); - Write(bit); - } - } - - /// - /// Read a bit. - /// - /// - /// - /// - private bool ReadBit(byte[] buffer, long bitPosition) - { - byte value = buffer[bitPosition / 8]; - return (value & (1 << (int)(bitPosition % 8))) != 0 ? true : false; - } - - /// - /// Write a bit. - /// - /// - /// - /// - private void WriteBit(byte[] buffer, int bitPosition, bool bit) - { - int byteOffset = bitPosition / 8; - int bitNumber = bitPosition % 8; - byte value = buffer[byteOffset]; - if (bit) - { - value = (byte)(value | (1 << bitNumber)); - } - else - { - value = (byte)(value & ~(1 << bitNumber)); - } - buffer[byteOffset] = value; - } - } -} diff --git a/Build-Combined-NuGetPackages.ps1 b/Build-Combined-NuGetPackages.ps1 index f937b75..2484582 100644 --- a/Build-Combined-NuGetPackages.ps1 +++ b/Build-Combined-NuGetPackages.ps1 @@ -37,7 +37,7 @@ if ($Help) { Write-Host " .\Build-Combined-NuGetPackages.ps1 [options]" Write-Host "" Write-Host "ARGUMENTS:" -ForegroundColor Yellow - Write-Host " Version number (required, e.g., '0.9.0')" + Write-Host " Version number (default: read from Directory.Build.props)" Write-Host "" Write-Host "OPTIONS:" -ForegroundColor Yellow Write-Host " -SkipTests Skip running unit tests" @@ -45,25 +45,36 @@ if ($Help) { Write-Host " -Help, -h, -? Show this help message" Write-Host "" Write-Host "EXAMPLES:" -ForegroundColor Yellow - Write-Host " .\Build-Combined-NuGetPackages.ps1 0.9.0" - Write-Host " .\Build-Combined-NuGetPackages.ps1 0.9.0 -SkipTests" + Write-Host " .\Build-Combined-NuGetPackages.ps1 # Use version from Directory.Build.props" + Write-Host " .\Build-Combined-NuGetPackages.ps1 -SkipTests # Skip tests, auto version" + Write-Host " .\Build-Combined-NuGetPackages.ps1 0.9.0 # Override version" + Write-Host " .\Build-Combined-NuGetPackages.ps1 0.9.0 -SkipTests # Override version, skip tests" Write-Host " .\Build-Combined-NuGetPackages.ps1 1.0.0-beta1 -SkipTests" Write-Host "" Write-Host "OUTPUT:" -ForegroundColor Yellow Write-Host " - Packages saved to: ./nupkg/" Write-Host " - Packages published to: ~/.nuget/local-packages/" Write-Host " - NuGet cache cleared for stardust.utilities and stardust.generators" + Write-Host " - Demo app bin/obj cleared to prevent stale builds" Write-Host "" exit 0 } # Version is required when not showing help if (-not $Version) { - Write-Host "ERROR: Version is required." -ForegroundColor Red - Write-Host "Usage: .\Build-Combined-NuGetPackages.ps1 [options]" -ForegroundColor Yellow - Write-Host "Example: .\Build-Combined-NuGetPackages.ps1 0.9.0" -ForegroundColor Gray - Write-Host "Run with -Help for more information." -ForegroundColor Gray - exit 1 + # Read default version from Directory.Build.props + $propsFile = "$ScriptDir\Directory.Build.props" + if (Test-Path $propsFile) { + [xml]$props = Get-Content $propsFile + $Version = $props.Project.PropertyGroup.Version + } + if (-not $Version) { + Write-Host "ERROR: Version is required (not found in Directory.Build.props)." -ForegroundColor Red + Write-Host "Usage: .\Build-Combined-NuGetPackages.ps1 [options]" -ForegroundColor Yellow + Write-Host "Run with -Help for more information." -ForegroundColor Gray + exit 1 + } + Write-Host "Using version $Version from Directory.Build.props" -ForegroundColor Gray } Write-Host "========================================" -ForegroundColor Cyan @@ -88,8 +99,7 @@ $foldersToClean = @( "$ScriptDir\Generators\bin", "$ScriptDir\Generators\obj", "$ScriptDir\Test\bin", - "$ScriptDir\Test\obj", - "$ScriptDir\Test\Generated" + "$ScriptDir\Test\obj" ) foreach ($folder in $foldersToClean) { @@ -222,6 +232,20 @@ foreach ($cacheDir in $cacheDirs) { } } +# Clear bin/obj of consuming projects so they pick up the fresh package on next build +$consumerDirs = @( + "$ScriptDir\Demo\BitFields.DemoApp\bin", + "$ScriptDir\Demo\BitFields.DemoApp\obj", + "$ScriptDir\Demo\BitFields.DemoWeb\bin", + "$ScriptDir\Demo\BitFields.DemoWeb\obj" +) +foreach ($dir in $consumerDirs) { + if (Test-Path $dir) { + Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue + Write-Host " Cleared consumer build: $dir" -ForegroundColor Gray + } +} + # Summary Write-Host "" Write-Host "========================================" -ForegroundColor Cyan diff --git a/ByteOrder.cs b/ByteOrder.cs new file mode 100644 index 0000000..9257f65 --- /dev/null +++ b/ByteOrder.cs @@ -0,0 +1,27 @@ +namespace Stardust.Utilities; + +/// +/// Specifies the byte order (endianness) for multi-byte field access in a . +/// +public enum ByteOrder +{ + /// + /// Big-endian (network byte order). Most significant byte first. + /// This is the standard for network protocols (TCP/IP, DNS, HTTP/2, etc.) + /// and some legacy and third-party processors and .NET distributions. + /// + BigEndian = 0, + + /// + /// Network byte order. Synonym for . + /// Provided for readability when defining protocol headers. + /// + NetworkEndian = BigEndian, + + /// + /// Little-endian (native byte order on x86/ARM). Least significant byte first. + /// This matches the native memory layout on most modern processors and every + /// Microsoft .NET distribution. + /// + LittleEndian = 1 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fd9a2ad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,102 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.6] - 2026-03-20 +### Added +- **`nint`/`nuint` storage type support** for `[BitFields]`. Generates compiler error (SD0001) when fields exceed bit 31 on x86 builds, and compiler warning (SD0002) on AnyCPU builds where 32-bit execution would silently lose data. +- **`StorageType` enum** -- new constructor overload `[BitFields(StorageType.Byte)]` (etc.) for compile-time validation of storage type. Generates compiler error (SD0003) for unsupported types. The existing `[BitFields(typeof(T))]` and `[BitFields(int)]` constructors remain supported. +- **Pre-defined numeric decomposition types**: `IEEE754Half`, `IEEE754Single`, `IEEE754Double`, and `DecimalBitFields` for inspecting IEEE 754 and .NET decimal bit layouts. Includes implicit conversions to/from their storage type, classification properties (`IsNormal`, `IsNaN`, `IsInfinity`, `IsDenormalized`, `IsZero`), true `Exponent` property (with getter and setter), `WithExponent(int)` fluent setter, and full arithmetic operator support. +- **Named SCREAMING_SNAKE_CASE mask constants** in generated code (e.g., `MANTISSA_MASK`, `MANTISSA_SHIFTED_MASK`, `MANTISSA_INVERTED_MASK`, `MANTISSA_START_BIT`) replacing inline hex literals for improved readability and reviewability. + +### Changed +- **`MustBe`/`UndefinedBitsMustBe` enforcement across all operations** -- `MustBe.Zero` and `MustBe.One` constraints on `[BitField]`/`[BitFlag]` are now enforced in setters, `With...` methods, constructors, parsing, and implicit conversions. Previously these constraints were only masked on read. + +### Backwards Compatibility +- All existing `[BitFields]` and `[BitFieldsView]` APIs are backwards compatible with 0.9.5. The `StorageType` enum constructor, `nint`/`nuint` support, numeric decomposition types, and named mask constants are entirely new additions. +- `MustBe`/`UndefinedBitsMustBe` enforcement is now applied consistently in setters, `With...` methods, constructors, and parsing. Code that previously wrote invalid values to `MustBe.Zero`/`MustBe.One` fields will now have those writes silently corrected. This is a behavioral change but aligns with the documented contract. + +## [0.9.5] - 2026-03-05 +### Added +- Added `Description` to `[BitFields]` and `[BitFieldsView]` structs, used for diagram section descriptions and demo app tooltips. The +purpose of this is to simplify the API and deprecate `DiagramSection` that adds unnecessary complexity to the API. + +### Fixed +- Fixed compile error in generated `With{Name}` methods when the property type is a byte-backed enum and the field starts at bit 0 (shift == 0). The generated code now casts the value to the storage type before applying the mask, matching the pattern already used by the setter and the shift != 0 branch. + +### Changed +- Deprecated `DiagramSection` feature, replaced with the `Description` field for `[BitFields]` and `[BitFieldsView]` structs. Will be removed in a future version. +- Diagrams now handle overlapping bit fields by rendering them in the order they are declared, with later fields potentially overwriting earlier ones in the diagram. This allows for intentional overlapping fields while still rendering all declared fields. + +### Backwards Compatibility +- All APIs are backwards compatible with 0.9.4. The `Description` parameter on `[BitFields]` and `[BitFieldsView]` is optional +and does not affect existing functionality. +- The `DiagramSection` type and related `RenderList`/`RenderListToString` methods are still present but marked as deprecated, +with no breaking changes. + +## [0.9.4] - 2026-02-09 +### Added +- **`BitFieldDiagram` RFC diagram generator** -- generates RFC 2360-style ASCII bit field diagrams from `[BitFields]` and `[BitFieldsView]` struct metadata via `BitFieldDiagram.Render()` and `RenderToString()`. Features auto-sized cells to fit field names, byte offset labels, bit-position headers (tens/ones digits), undefined bit marking (`Undefined` or `U` with legend), configurable bits-per-row (8, 16, 32, 64), optional field description legend, and struct-sized last rows. +- **`RenderList` / `RenderListToString`** -- render multiple struct sections as a unified diagram with consistent cell widths via `DiagramSection`. The widest field name across all sections determines the scale. `ComputeMinCellWidth` exposed for custom layout logic. +- **`showByteOffset` parameter** -- controls hex byte offset labels on diagram content rows (default: true). +- **`Description` parameter** on `[BitField]` and `[BitFlag]` attributes for field-level documentation, used by diagram descriptions legend and demo app tooltips. +- **`[BitFieldsView]` source generator** -- zero-copy `record struct` views over `Memory` buffers with per-field bit manipulation, supporting both big-endian/MSB-first (network protocols) and little-endian/LSB-first (hardware registers) conventions. Includes nested sub-view composition and per-field endianness override via `[BitFields]` ByteOrder detection. +- **`BitOrder` enum** (`BitZeroIsMsb`, `BitZeroIsLsb`) for controlling bit numbering in `[BitFields]` and `[BitFieldsView]`. +- **`ByteOrder` enum** (`BigEndian`/`NetworkEndian`, `LittleEndian`) for controlling byte order in `[BitFields]` serialization and `[BitFieldsView]` multi-byte field access. +- **`bitOrder` and `byteOrder` optional parameters** on `[BitFields]` attribute constructors. Defaults (`BitZeroIsLsb`, `LittleEndian`) preserve backwards compatibility. +- **Little-endian endian-aware types**: `UInt16Le`, `UInt32Le`, `UInt64Le`, `Int16Le`, `Int32Le`, `Int64Le` with `TypeConverter` support, complementing the existing big-endian types. +- **`[BitFieldsView]` per-field endianness override**: using endian-aware property types (e.g., `UInt32Be` in a LE view) or embedding a `[BitFields]` struct whose declared `ByteOrder` differs from the view's default. +- **Canonical protocol header examples**: `IPv4HeaderView`, `IPv6FullHeaderView`, `UdpHeaderView`, `TcpHeaderView` in the test suite, demonstrating real-world network packet parsing with nested sub-views. +- Added support for signed properties in a `[BitFields]` struct. +- Added `UndefinedBitsMustBe` enum with `Any`, `Zeroes`, and `Ones` values for controlling undefined bit behavior in `[BitFields]`. +- Added `MustBe` enum with `Any`, `Zero`, `One`, and `Ones` values for per-field/flag bit control. +- Added `ValueOverride` parameter (`MustBe` enum type) to `[BitField]` and `[BitFlag]` attributes for per-field/flag bit override. +- Added support for sparse undefined bits (gaps between defined fields). +- Added `[BitFields]` struct composition (using one `[BitFields]` type as a property type in another), with testing and documentation. +- Added `Half` (16-bit float) and `decimal` storage type support for `[BitFields]`. +- Added fuzz testing for parsers. No errors found. +- Performance testing now runs on local dev machine. Still disabled during CI builds. +- Builds are now deterministic. +- **Blazor WebAssembly demo app** (`Demo/BitFields.DemoWeb`) -- interactive browser-based playground for BitFields, PE headers, network packets, CPU registers, and RFC diagrams. Deployable to GitHub Pages. +- **`PeParser`** shared utility -- demonstrates `Result.Then()` chaining for multi-step PE header validation pipeline. +- **BitFieldDiagram test suite** -- 29 tests covering `Render`, `RenderToString`, `RenderList`, `RenderListToString`, `ComputeMinCellWidth`, bit order handling, undefined bits, descriptions, and separator structure. + +### Backwards Compatibility +All APIs are backwards compatible with 0.9.3. New parameters on `[BitFields]`, `[BitField]`, and `[BitFlag]` attribute constructors use optional defaults that preserve existing behavior. `[BitFieldsView]`, `BitOrder`, `ByteOrder`, and the little-endian endian-aware types are entirely new additions. The `StorageType` property on `BitFieldsAttribute` changed from `Type` to `Type?` to support the new bit-count constructor overload. + +## [0.9.3] - 2026-02-05 +### Added +- Added support for .NET 7 and .NET 8 in addition to .NET 10. +- No feature changes. + +## [0.9.2] - 2026-02-04 (First NuGet Release) +### Added +- Added several NuGet project properties, icon, links in preparation for release. +- Added CHANGELOG.md, SECURITY.md, CODE_OF_CONDUCT.md. +- Added GitHub templates for issues and pull requests. + +### Removed +- Removed unused BitStream feature - not useful enough yet. +- Removed a few unnecessary Extensions features that can be accomplished easily in .NET already. + +## [Unreleased] + +## [0.9.1] - 2026-02-01 +### Added +- Migrated from app-specific in-house library to NuGet package for better reuse. + +## [0.9.0] - 2026-01-28 +### Added +- Migrated from mature in-house library to NuGet package for better reuse. +- Added support for C#-style `_` digit separators in `Parse` and `TryParse` methods for `[BitField]` types. +- Added support for binary format parsing (e.g., `0b1101`) for `[BitField]` types. + +### Changed +- `[BitFields]` types now implement `ISpanFormattable` for allocation-free string formatting. +- `[BitFields]` types now implement `ISpanParsable` for allocation-free string parsing. + +## [0.0.1] - 2023-04-07 +### Added +- Initial internal release to private GitHub repo. \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..177b2be --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,134 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement via +[GitHub Issues](https://github.com/dhadner/Stardust.Utilities/issues) (for +non-sensitive matters) or through GitHub's private reporting features. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8abcabf..b9b5ff8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,6 +143,7 @@ Stardust.Utilities/ │ └── BitFieldsGenerator.cs ├── Test/ # Unit tests ├── build/ # MSBuild props/targets for NuGet +├── PRIVACY.md # Privacy statement └── nupkg/ # Local NuGet output (gitignored) ``` diff --git a/DEVELOPER.md b/DEVELOPER.md index d810125..ffbbe1a 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -2,19 +2,57 @@ This guide explains how to modify the source generators and update consuming projects. +## Table of Contents + +- [Quick Reference: Build Workflow](#quick-reference-build-workflow) +- [Getting Started](#getting-started) + - [Opening the Project](#opening-the-project) + - [Project Structure](#project-structure) +- [Submodule Workflow](#submodule-workflow) + - [How the submodule is structured](#how-the-submodule-is-structured) + - [Committing changes](#committing-changes) + - [Detached HEAD pitfall](#detached-head-pitfall) + - [CI is independent](#ci-is-independent) + - [Pulling upstream changes](#pulling-upstream-changes) +- [Building and Packaging](#building-and-packaging) + - [What Requires What Workflow?](#what-requires-what-workflow) + - [Build Scripts](#build-scripts) + - [Package Reference Scenarios](#package-reference-scenarios) + - [Updating the Source Generator](#updating-the-source-generator) + - [NuGet Package Architecture](#nuget-package-architecture) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Performance Tests](#performance-tests) + - [Fuzz Tests](#fuzz-tests) +- [CI and Multi-Targeting](#ci-and-multi-targeting) + - [Multi-Targeting and SolutionPath](#multi-targeting-and-solutionpath) + - [CI Workflow Design](#ci-workflow-design) +- [Features](#features) + - [Rust-Style Bit Ranges](#rust-style-bit-ranges) + - [Nested Struct Support](#nested-struct-support) + - [Automatic IntelliSense](#automatic-intellisense) + - [Full Operator Support](#full-operator-support) +- [Demo Apps](#demo-apps) + - [DemoWeb (Blazor WebAssembly)](#demoweb-blazor-webassembly) + - [DemoApp (WPF)](#demoapp-wpf) +- [Troubleshooting](#troubleshooting) +- [Version History](#version-history) +- [API Simplification Trade Study (v0.3.0)](#api-simplification-trade-study-v030) +- [Releasing a Version](#releasing-a-version) + ## Quick Reference: Build Workflow -**To build a new version (e.g., 0.9.0):** +**To build a new version (e.g., 0.9.6):** ```powershell # Navigate to the Stardust.Utilities directory cd Stardust.Utilities # Build both NuGet packages (automatically publishes to local feed) -.\Build-Combined-NuGetPackages.ps1 0.9.0 +.\Build-Combined-NuGetPackages.ps1 0.9.6 # Or skip tests for faster iteration during development -.\Build-Combined-NuGetPackages.ps1 0.9.0 -SkipTests +.\Build-Combined-NuGetPackages.ps1 0.9.6 -SkipTests ``` **What happens automatically:** @@ -30,7 +68,9 @@ cd Stardust.Utilities > **Note:** Version is specified at build time and is NOT stored in .csproj files. This keeps source control clean and avoids accidental version mismatches. -## Opening the Project +## Getting Started + +### Opening the Project Open `Stardust.Utilities.slnx` in Visual Studio or VS Code: @@ -44,7 +84,7 @@ code . This solution uses the new XML-based solution format (`.slnx`) which is cleaner and easier to merge than the legacy `.sln` format. -## Project Structure +### Project Structure ``` Stardust.Utilities/ @@ -52,47 +92,121 @@ Stardust.Utilities/ ├── Stardust.Utilities.csproj # Main library (types, attributes) ├── Generators/ │ ├── Stardust.Generators.csproj # Source generator project -│ └── BitFieldsGenerator.cs # [BitFields] generator +│ ├── BitFieldsGenerator.cs # [BitFields] generator +│ ├── BitFieldsViewGenerator.cs # [BitFieldsView] generator +│ └── *.cs # Additional generator source files ├── Test/ │ └── Stardust.Utilities.Tests.csproj +│ └── *.cs # Test cases and supporting source files ├── build/ │ ├── Stardust.Utilities.props # Auto-enables IntelliSense for consumers │ └── Stardust.Utilities.targets # Auto-excludes generated files from compilation ├── nupkg/ # Local NuGet packages output +├── *.cs # Stardust.Utilities source files ├── Build-Generator-NuGetPackage.ps1 # Builds generator package (for local development) ├── Build-Combined-NuGetPackages.ps1 # Builds the distributable package +├── CODE_OF_CONDUCT.md # Code of conduct +├── CONTRIBUTING.md # Contribution guidelines +├── DEVELOPER.md # This file +├── ENDIAN.md # Endianness documentation (Int32Be, UInt16Be, etc.) +├── EXTENSIONS.md # Detailed documentation on the Extensions class methods +├── PRIVACY.md # Privacy statement (no telemetry, no data collection) ├── README.md # User documentation -└── DEVELOPER.md # This file +├── RESULT.md # Result documentation -- used extensively throughout Stardust.Utilities +└── SECURITY.md # Security documentation and how to report vulnerabilities ``` -## NuGet Package Architecture +## Submodule Workflow -### Why Two Packages Exist +Stardust.Utilities is designed to work both as a **standalone repository** and as a +**Git submodule** inside a parent solution (e.g., MySolution). This dual-use pattern requires +some awareness when committing and when modifying CI or build configuration. -The **`Stardust.Generators`** package is **not intended for public distribution**. It exists solely -to support local development scenarios. The source generator is embedded directly within the `Stardust.Utilities` -package for distribution. +### How the submodule is structured -The separate `Stardust.Generators` package is needed because: -- When debugging `Stardust.Utilities` via `ProjectReference`, the embedded analyzer doesn't load -- The standalone generator package includes MSBuild `.props`/`.targets` files that automatically configure consumer projects -- This enables IntelliSense for generated code without manual project configuration +``` +MySolution/ ← parent repo (branch: adb) +├── MySolution.sln ← parent solution (references Stardust.Utilities projects) +├── Stardust.Utilities/ ← Git submodule (branch: main or dev) +│ ├── Stardust.Utilities.slnx ← standalone solution +│ ├── .github/workflows/ci.yml ← CI runs against THIS repo independently +│ └── ... +└── ... +``` -**For end users:** Only reference `Stardust.Utilities` — the generator is included automatically. +The parent solution (`MySolution.sln`) includes `Stardust.Utilities.csproj`, +`Stardust.Generators.csproj`, and `Stardust.Utilities.Tests.csproj` directly. +MSBuild sets `SolutionPath` when building through a solution, which the test project +uses to control multi-targeting (see [Multi-Targeting and SolutionPath](#multi-targeting-and-solutionpath) below). -### Stardust.Utilities Package Contents +### Committing changes -The `Stardust.Utilities` NuGet package includes: +When Stardust.Utilities is checked out as a submodule, it has its **own independent Git +history**. Changes must be committed and pushed from within the submodule directory: -| Path | Description | -|------|-------------| -| `lib/net10.0/Stardust.Utilities.dll` | Main library with attributes and types | -| `analyzers/dotnet/cs/Stardust.Generators.dll` | Source generator (embedded, runs at compile time) | -| `build/Stardust.Utilities.props` | Auto-enables `EmitCompilerGeneratedFiles` for IntelliSense | -| `build/Stardust.Utilities.targets` | Auto-excludes generated files from duplicate compilation | -| `README.md` | Package documentation | +```powershell +# 1. Navigate into the submodule +cd Stardust.Utilities -## What Requires What Workflow? +# 2. Verify you are on the correct branch (submodules can detach HEAD) +git branch --show-current +# If empty (detached HEAD), re-attach: +git checkout main # or dev + +# 3. Stage, commit, and push as normal +git add -A +git commit -m "Your commit message" +git push origin main + +# 4. Return to parent repo and update the submodule reference +cd .. +git add Stardust.Utilities +git commit -m "Update Stardust.Utilities submodule pointer" +git push +``` + +> **Important:** Always commit and push inside the submodule **first**, then update the +> submodule pointer in the parent repo. If you push the parent first, it will reference a +> commit that doesn't exist on the remote and other clones will fail. + +### Detached HEAD pitfall + +Git submodules check out a specific **commit**, not a branch. After cloning the parent +repo or switching branches, the submodule may be in a "detached HEAD" state: + +```powershell +cd Stardust.Utilities +git status +# "HEAD detached at abc1234" + +# Fix: checkout the branch you want to work on +git checkout dev +``` + +### CI is independent + +The `.github/workflows/ci.yml` inside `Stardust.Utilities/` triggers on pushes to the +**Stardust.Utilities** GitHub repository. It does **not** run when the parent repo is +pushed. This means: + +- Stardust.Utilities CI validates the library in isolation (all TFMs, all test categories + except Performance) +- The parent repo's CI (if any) is responsible for integration testing + +### Pulling upstream changes + +```powershell +# From the parent repo root +git submodule update --remote Stardust.Utilities + +# Or from within the submodule +cd Stardust.Utilities +git pull origin main +``` + +## Building and Packaging + +### What Requires What Workflow? | What You Changed | How to See Changes | |------------------|-------------------| @@ -100,9 +214,9 @@ The `Stardust.Utilities` NuGet package includes: | **Stardust.Generators** (source generator code) | Run `Build-Generator-NuGetPackage.ps1` or `Build-Combined-NuGetPackages.ps1` | | **build/*.props or build/*.targets** | Run `Build-Combined-NuGetPackages.ps1` | -## Build Scripts +### Build Scripts -### Build-Combined-NuGetPackages.ps1 +#### Build-Combined-NuGetPackages.ps1 **This is the primary build script.** It builds both NuGet packages and automatically publishes to the local feed: - `Stardust.Utilities.x.y.z.nupkg` - **The distributable package** (includes generator as embedded analyzer + utility types) @@ -112,21 +226,21 @@ The `Stardust.Utilities` NuGet package includes: # Show help .\Build-Combined-NuGetPackages.ps1 -Help -# Build version 0.9.0 (runs tests, publishes to local feed) -.\Build-Combined-NuGetPackages.ps1 0.9.0 +# Build version 0.9.6 (runs tests, publishes to local feed) +.\Build-Combined-NuGetPackages.ps1 0.9.6 # Skip tests for faster iteration -.\Build-Combined-NuGetPackages.ps1 0.9.0 -SkipTests +.\Build-Combined-NuGetPackages.ps1 0.9.6 -SkipTests # Use Debug configuration -.\Build-Combined-NuGetPackages.ps1 0.9.0 -Configuration Debug +.\Build-Combined-NuGetPackages.ps1 0.9.6 -Configuration Debug ``` **Parameters:** | Parameter | Required | Description | |-----------|----------|-------------| -| `` | Yes | Version number (e.g., `0.9.0`, `1.0.0-beta1`) | +| `` | Yes | Version number (e.g., `0.9.6`, `1.0.0-beta1`) | | `-SkipTests` | No | Skip running unit tests | | `-Configuration` | No | `Debug` or `Release` (default: Release) | | `-Help` | No | Show help message | @@ -144,12 +258,20 @@ The `Stardust.Utilities` NuGet package includes: - `./nupkg/` - Package files - `~/.nuget/local-packages/` - Local NuGet feed (packages appear in NuGet Package Manager) -### Build-Generator-NuGetPackage.ps1 +> **Local feed setup for contributors:** The build script copies packages to `~/.nuget/local-packages/`. +> If this is your first time building, you may need to register the folder as a NuGet source: +> ```powershell +> dotnet nuget add source "$env:USERPROFILE\.nuget\local-packages" --name local-packages +> ``` +> You can verify it was added with `dotnet nuget list source`. The local feed appears in +> Visual Studio's NuGet Package Manager under **Package source > local-packages**. + +#### Build-Generator-NuGetPackage.ps1 Builds only the `Stardust.Generators.x.y.z.nupkg` standalone generator package **for local development**. -> **Note:** This package is not published to NuGet.org. It exists only to support debugging scenarios where -`Stardust.Utilities` is referenced via `ProjectReference`. +> **Note:** This package is not published to NuGet.org. It exists only to support debugging scenarios where +> `Stardust.Utilities` is referenced via `ProjectReference`. **Use this when:** - Debugging `Stardust.Utilities` via ProjectReference in Visual Studio @@ -157,40 +279,277 @@ Builds only the `Stardust.Generators.x.y.z.nupkg` standalone generator package * ```powershell # Specify a version -.\Build-Generator-NuGetPackage.ps1 -Version "0.9.0" +.\Build-Generator-NuGetPackage.ps1 -Version "0.9.6" ``` -## Package Reference Scenarios +### Package Reference Scenarios | Scenario | What to Reference | |----------|-------------------| | **Normal usage** (consuming the library) | `Stardust.Utilities` NuGet package only | -| **Debugging Stardust.Utilities locally** | ProjectReference to `Stardust.Utilities.csproj` + PackageReference to -`Stardust.Generators` (local only) | +| **Debugging Stardust.Utilities locally** | ProjectReference to `Stardust.Utilities.csproj` + PackageReference to `Stardust.Generators` (local only) | -> **Important:** Only `Stardust.Utilities` is published to NuGet.org. The `Stardust.Generators` package is for local -development only and should never be distributed separately. +> **Important:** Only `Stardust.Utilities` is published to NuGet.org. The `Stardust.Generators` package is for local +> development only and should never be distributed separately. -## Updating the Source Generator +### Updating the Source Generator -### Step 1: Make Your Changes +**Step 1:** Edit the generator in `Generators/BitFieldsGenerator.cs`. -Edit the generator in `Generators/BitFieldsGenerator.cs`. +> **Enum casting rule:** When generating code that applies bitwise operators (`&`, `|`, `^`) +> to a `value` parameter whose type comes from `field.PropertyType`, always cast `value` to +> the storage type first (e.g., `({info.StorageType})value`). C# does not allow `enum & int` +> directly. The setter and shift != 0 paths already follow this pattern; the shift == 0 path +> in `GenerateWithBitFieldMethod` was fixed in v0.9.5 to match. See the test +> `GeneratedBitFields_WithEnumAtBitZero` for the regression test. -### Step 2: Rebuild the Package +**Step 2:** Rebuild the package: ```powershell # Rebuild both packages (recommended) -.\Build-Combined-NuGetPackages.ps1 0.9.0 -SkipTests +.\Build-Combined-NuGetPackages.ps1 0.9.6 -SkipTests ``` -### Step 3: Test Locally - -If testing with a consuming project: +**Step 3:** Test locally with a consuming project: 1. Update the `PackageReference` version in the consuming project 2. Clear NuGet cache: `dotnet nuget locals global-packages --clear` 3. Restore and rebuild +### NuGet Package Architecture + +#### Why Two Packages Exist + +The **`Stardust.Generators`** package is **not intended for public distribution**. It exists solely +to support local development scenarios. The source generator is embedded directly within the `Stardust.Utilities` +package for distribution. + +The separate `Stardust.Generators` package is needed because: +- When debugging `Stardust.Utilities` via `ProjectReference`, the embedded analyzer doesn't load +- The standalone generator package includes MSBuild `.props`/`.targets` files that automatically configure consumer projects +- This enables IntelliSense for generated code without manual project configuration + +**For end users:** Only reference `Stardust.Utilities` — the generator is included automatically. + +#### Stardust.Utilities Package Contents + +The `Stardust.Utilities` NuGet package includes: + +| Path | Description | +|------|-------------| +| `lib/net10.0/Stardust.Utilities.dll` | Main library with attributes and types | +| `analyzers/dotnet/cs/Stardust.Generators.dll` | Source generator (embedded, runs at compile time) | +| `build/Stardust.Utilities.props` | Auto-enables `EmitCompilerGeneratedFiles` for IntelliSense | +| `build/Stardust.Utilities.targets` | Auto-excludes generated files from duplicate compilation | +| `README.md` | Package documentation | + +## Testing + +### Unit Tests + +Run all unit tests with: + +```powershell +cd Stardust.Utilities +dotnet test -c Release +``` + +This runs tests on all target frameworks (.NET 8, 9, and 10). + +To run tests on a specific framework: + +```powershell +dotnet test -c Release --framework net10.0 +``` + +### Performance Tests + +Performance tests compare the generated BitField code against hand-coded bit manipulation to verify +there is no performance overhead from using the source generator. + +**Performance tests are excluded from CI** using two independent layers: +1. **CI workflow filter:** `--filter "Category!=Performance"` prevents them from running at all +2. **In-code guard:** `Assert.SkipWhen(CiEnvironmentDetector.IsRunningInCi, ...)` skips them if + the filter is ever removed or tests are invoked without the filter + +This dual-layer approach ensures performance tests never cause CI failures, even if the workflow +is modified or tests are run through a different pipeline. + +**Locally in Visual Studio:** Performance tests are visible in Test Explorer (tagged with +`[Trait("Category", "Performance")]`). They run when explicitly selected and produce timing +results with pass/fail status indicators. They also run when "Run All Tests" is invoked, since +the CI guard does not apply locally. + +There are six performance tests: +- **`BitFlag_Get_Performance`**: Tests single-bit read performance +- **`BitFlag_Set_Performance`**: Tests single-bit write performance +- **`BitField_Get_Performance`**: Tests multi-bit field read performance +- **`BitField_Set_Performance`**: Tests multi-bit field write performance +- **`Mixed_ReadWrite_Performance`**: Quick sanity check (single run, ~500ms), outputs results but doesn't fail on variance +- **`FullSuite_Performance_Summary`**: Rigorous statistical analysis (20 runs, ~2 minutes), computes mean, σ, and 95% CI + +#### Running Performance Tests + +**From Visual Studio:** Select individual performance tests in Test Explorer and click Run, +or run all tests (performance tests will execute locally). + +**From the command line:** + +```powershell +cd Stardust.Utilities + +# Run all tests including performance tests +dotnet test Test/Stardust.Utilities.Tests.csproj -c Release + +# Run only performance tests +dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --filter "Category=Performance" + +# Run the comprehensive statistical suite +dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --filter "FullyQualifiedName~FullSuite_Performance_Summary" --framework net10.0 + +# Run all tests EXCEPT performance tests (same as CI) +dotnet test Test/Stardust.Utilities.Tests.csproj -c Release --filter "Category!=Performance" +``` + +#### Understanding Performance Test Output + +The `Mixed_ReadWrite_Performance` test outputs timing results with a status indicator: +- ✓ Performance is within expected range (ratio 0.75-1.25) +- ⚠️ WARNING if generated code is >25% slower (may indicate regression or system load) + +The `FullSuite_Performance_Summary` test runs 20 iterations of each test and produces statistical output: + +``` +BITFIELD PERFORMANCE SUMMARY WITH STATISTICS +Runs: 20, Iterations per run: 100,000,000 + +============================================================ + +| Test | Generated | σ | Hand-coded | σ | Ratio | σ | 95% CI | +|---------------|-----------|----| -----------|----|-------|-------|---------------| +| BitFlag GET | 584 ms | 14 | 568 ms | 15 | 1.029 | 0.035 | 0.960 – 1.098 | +| BitFlag SET | 825 ms | 27 | 821 ms | 22 | 1.006 | 0.031 | 0.945 – 1.067 | +| BitField GET | 402 ms | 36 | 405 ms | 18 | 0.995 | 0.087 | 0.824 – 1.166 | +| BitField SET | 413 ms | 9 | 410 ms | 7 | 1.007 | 0.020 | 0.968 – 1.046 | +| Mixed R/W | 1031 ms | 13 | 1030 ms | 23 | 1.001 | 0.024 | 0.954 – 1.048 | +| **Overall** | | | | | 1.008 | 0.048 | 0.914 – 1.102 | +``` + +- **σ**: Sample standard deviation of the measurements +- **Ratio**: Generated time / Hand-coded time (1.0 = identical performance) +- **95% CI**: 95% Confidence Interval for the mean = mean ± 1.96 × SE, where SE = σ/√n + +**Expected results**: Ratio should be between 0.9 and 1.1 (within 10% of hand-coded performance). + +### Fuzz Tests + +Fuzz tests verify that parsing methods handle malformed, malicious, and edge-case inputs gracefully: + +```powershell +# Run only fuzz tests +dotnet test -c Release --filter "FullyQualifiedName~ParsingFuzzTests" --framework net10.0 +``` + +Fuzz tests cover: +- Null/empty/whitespace inputs +- Overflow and boundary values +- Injection attacks (SQL, XSS, command, path traversal) +- Unicode homoglyphs and invisible characters +- Control characters and embedded nulls +- Random garbage data (1000+ random inputs) + +## CI and Multi-Targeting + +### Multi-Targeting and SolutionPath + +The test project (`Test/Stardust.Utilities.Tests.csproj`) uses an MSBuild condition to +control which target frameworks are built: + +```xml + + + net8.0;net9.0;net10.0 + + + + + net10.0 + +``` + +**Why this exists:** When the test project is included in a parent solution (e.g., +`MySolution.sln`), solution-level NuGet restore writes a single `project.assets.json`. If +the test project multi-targets, the restore for `net8.0`/`net9.0` can overwrite the +assets file needed by other projects in the solution. Limiting to `net10.0` avoids this. + +**Impact on different workflows:** + +| Workflow | `SolutionPath` | Test TFMs | Notes | +|----------|---------------|-----------|-------| +| Visual Studio (any `.sln` or `.slnx`) | Set | `net10.0` only | Developer sees only .NET 10 tests in Test Explorer | +| `dotnet test Test/*.csproj` (CLI) | Unset | `net8.0`, `net9.0`, `net10.0` | Full multi-target coverage | +| GitHub Actions CI | Unset | `net8.0`, `net9.0`, `net10.0` | Projects restored explicitly, not via solution | +| `Build-Combined-NuGetPackages.ps1` | Unset | `net8.0`, `net9.0`, `net10.0` | Script invokes `dotnet test` on `.csproj` directly | + +> **Key rule for CI/build scripts:** Always restore and build individual `.csproj` files, +> never the `.slnx` solution. This keeps `SolutionPath` unset and enables multi-targeting. + +### CI Workflow Design + +The GitHub Actions workflow (`.github/workflows/ci.yml`) is designed to work correctly +regardless of whether Stardust.Utilities is checked out standalone or as a submodule. + +#### Key design decisions + +**1. Explicit project restores (not solution restore)** + +```yaml +# ✅ Correct: restore each project individually +- run: dotnet restore Generators/Stardust.Generators.csproj +- run: dotnet restore Stardust.Utilities.csproj +- run: dotnet restore Test/Stardust.Utilities.Tests.csproj + +# ❌ Wrong: bare "dotnet restore" finds the .slnx and sets SolutionPath, +# causing the test project to restore for net10.0 only +- run: dotnet restore +``` + +When `dotnet restore` runs without arguments, it discovers `Stardust.Utilities.slnx` and +restores through the solution. This sets `SolutionPath`, which triggers the single-target +`net10.0` condition in the test project. Restoring each `.csproj` individually leaves +`SolutionPath` unset, so the test project correctly restores for all three TFMs. + +**2. Performance test exclusion via `--filter`** + +```yaml +- run: dotnet test ... --filter "Category!=Performance" +``` + +Performance tests have `[Trait("Category", "Performance")]` and are excluded from CI runs +because GitHub Actions runners are shared VMs with unpredictable CPU scheduling. The tests +also contain `Assert.SkipWhen(CiEnvironmentDetector.IsRunningInCi, ...)` as a fallback. + +**3. .NET 7 SDK installed but not tested** + +The library targets `net7.0` for consumers who haven't upgraded, but xUnit v3 does not +support .NET 7. The .NET 7 SDK is installed so the library **builds** for `net7.0`, but +there is no `dotnet test --framework net7.0` step. + +**4. Separate build steps** + +The workflow builds Generator → Library → Tests in dependency order with `--no-restore`, +then runs tests per-framework. This matches the project dependency graph and ensures each +step uses the same restored packages. + +#### Making changes to the CI workflow + +If you modify `ci.yml`, verify these invariants: + +- [ ] `dotnet restore` is called on individual `.csproj` files, never bare or on `.slnx` +- [ ] All SDK versions in `setup-dotnet` match the library's `TargetFrameworks` (currently 7, 8, 9, 10) +- [ ] Test steps include `--filter "Category!=Performance"` +- [ ] No `dotnet test --framework net7.0` step (xUnit v3 doesn't support it) +- [ ] Build steps use `-c Release` (required for JIT inlining in performance-sensitive code) + ## Features ### Rust-Style Bit Ranges @@ -293,6 +652,81 @@ var parsed = GeneratedStatusReg8.Parse("0xFF"); > **Note:** Unary `-` for unsigned types is an extension (native `uint`/`ulong` don't support it). > It produces two's complement negation: `-1` on a `byte` yields `255`. +## Demo Apps + +### DemoWeb (Blazor WebAssembly) + +The `Demo/BitFields.DemoWeb` project is a Blazor WebAssembly app that showcases +BitFields, protocol headers, PE viewers, and the RFC diagram generator. + +**Build requirements:** + +- The `wasm-tools` workload is required because the project customizes the + native WASM build. Install it once: + ```powershell + dotnet workload install wasm-tools + ``` +- After installing the workload, **restart Visual Studio** so its build host + picks up the new workload. + +**WASM compatibility settings (csproj):** + +The project disables two WASM features to maximize browser compatibility: + +| Property | Default | DemoWeb | Why | +|----------|---------|---------|-----| +| `WasmEnableSIMD` | `true` | `false` | V8 JIT-less mode crashes on SIMD instructions | +| `WasmEnableExceptionHandling` | `true` | `false` | V8 JIT-less mode crashes on native WASM EH | + +These settings cause `dotnet.native.wasm` to be relinked without SIMD and with +JavaScript-based exception handling. The result is a slightly slower but +universally compatible binary. This is appropriate for a demo app. + +**Edge Enhanced Security Mode (Strict):** + +Edge's Enhanced Security Mode (Strict) disables WebAssembly JIT compilation for +large modules. Even with SIMD and native EH disabled, the 14 MB +`dotnet.native.wasm` crashes the renderer with `STATUS_ILLEGAL_INSTRUCTION`. +Small WASM modules (under ~1 MB) compile fine, so no JavaScript probe can detect +the problem ahead of time. + +The `index.html` boot script uses a three-tier strategy to handle this: + +**Non-Edge browsers** (Chrome, Firefox, Safari): auto-load Blazor immediately. +These browsers are not affected by the JIT-less issue. + +**Edge, first visit**: the script detects Edge via User-Agent (`/Edg\//`) and +shows a welcome page with a "Load Interactive Demo" button, a video walkthrough +link, and README screenshots link. This prevents the renderer crash from being +the first thing a visitor sees. If the user clicks "Load Demo": + +- **Balanced mode** (default): Blazor loads successfully. `Program.cs` sets + `localStorage('blazorBoot') = 'success'`. All subsequent visits auto-load. +- **Strict mode**: the renderer crashes with `STATUS_ILLEGAL_INSTRUCTION`. + The `localStorage` flag stays at `'loading'` (the browser process manages + localStorage, so it survives renderer crashes). The next visit shows a + compatibility panel with the Edge settings fix and fallback content links. + +**Edge, return visit after crash** (`blazorBoot === 'loading'`): shows the +compatibility panel immediately (no crash). The primary fix instructs the user +to add the site to the exception list at +`edge://settings/privacy/security/secureModeSites`, with video/screenshots as +a fallback. A "Try again" button clears the flag and reloads. + +**Any browser, return visit after success** (`blazorBoot === 'success'`): +auto-loads Blazor regardless of User-Agent. + +**GitHub Pages deployment:** + +The `deploy-demo.yml` workflow installs `wasm-tools` and publishes with the +same WASM settings as local builds. Both environments produce identical binaries. + +### DemoApp (WPF) + +The `Demo/BitFields.DemoApp` project is a WPF desktop app (Windows only). It +shares model and utility files with DemoWeb via `` links. +No special build requirements beyond the standard .NET SDK. + ## Troubleshooting ### Generator changes not taking effect @@ -350,15 +784,7 @@ To see what the generator produces: ## Version History -| Version | Changes | -|---------|---------| -| 0.9.0 | Full operator support: arithmetic (+, -, *, /, %), shift (<<, >>, >>>), comparison (<, >, <=, >=), IComparable, IEquatable, IFormattable, ISpanFormattable | -| 0.8.3 | Added parsing support via `IParsable` and `ISpanParsable` interfaces | -| 0.6.0 | **Breaking:** Changed `[BitField(shift, width)]` to Rust-style `[BitField(startBit, endBit)]` | -| 0.5.2 | Auto-enable IntelliSense via .props/.targets files | -| 0.5.0 | Nested struct support, improved indentation in generated code | -| 0.3.0 | **Breaking:** Simplified BitFields API, added signed storage types | -| 0.2.0 | Initial release with BitFields generator | +See [CHANGELOG.md](https://github.com/dhadner/Stardust.Utilities/blob/main/CHANGELOG.md) for detailed version history and release notes. ## API Simplification Trade Study (v0.3.0) @@ -426,3 +852,22 @@ v0.3.0 also added support for signed storage types (`sbyte`, `short`, `int`, `lo Performance testing showed signed types are approximately 22% slower than unsigned equivalents due to the additional casts required to avoid sign extension issues in bitwise operations. For most use cases, this overhead is acceptable. + +## Releasing a Version + +### Release Checklist + +1. Update `Version` in `Directory.Build.props` (single source of truth; demo app + `PackageReference` versions use `$(Version)` automatically) +2. Update `CHANGELOG.md` with the new version, date, and changes +3. Update `PackageReleaseNotes` in `Stardust.Utilities.csproj` with version-specific notes +4. Verify `README.md` installation snippet references the new version +5. Build and test: `.\Build-Combined-NuGetPackages.ps1` +6. Commit all changes and push to `main` +7. Create a Git tag with the `v` prefix: `git tag v0.9.6 && git push origin v0.9.6` +8. Publish to NuGet.org using the command shown by the build script + +### Tag Naming Convention + +All release tags use the `v` prefix (e.g., `v0.9.6`, `v1.0.0`). The initial `0.9.2` tag +predates this convention. New releases must use the `v` prefix for consistency. diff --git a/Demo/BitFields.DemoApp/App.xaml b/Demo/BitFields.DemoApp/App.xaml new file mode 100644 index 0000000..8c727f2 --- /dev/null +++ b/Demo/BitFields.DemoApp/App.xaml @@ -0,0 +1,6 @@ + + diff --git a/Demo/BitFields.DemoApp/App.xaml.cs b/Demo/BitFields.DemoApp/App.xaml.cs new file mode 100644 index 0000000..8564b95 --- /dev/null +++ b/Demo/BitFields.DemoApp/App.xaml.cs @@ -0,0 +1,7 @@ +using System.Windows; + +namespace BitFields.DemoApp; + +public partial class App : Application +{ +} diff --git a/Demo/BitFields.DemoApp/BitFields.DemoApp.csproj b/Demo/BitFields.DemoApp/BitFields.DemoApp.csproj new file mode 100644 index 0000000..bcffca6 --- /dev/null +++ b/Demo/BitFields.DemoApp/BitFields.DemoApp.csproj @@ -0,0 +1,18 @@ + + + WinExe + net10.0-windows + true + enable + enable + + true + Stardust-Utilities-Icon.ico + + + + + + + + diff --git a/Demo/BitFields.DemoApp/MainWindow.xaml b/Demo/BitFields.DemoApp/MainWindow.xaml new file mode 100644 index 0000000..9bd2462 --- /dev/null +++ b/Demo/BitFields.DemoApp/MainWindow.xaml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + +