Skip to content

feat!: target .NET 11 and use its span decoders - #1

Merged
NotOfficer merged 5 commits into
masterfrom
feat/net11-decompressors
Nov 12, 2026
Merged

NotOfficer merged 5 commits into
masterfrom
feat/net11-decompressors

Conversation

@NotOfficer

Copy link
Copy Markdown
Owner

Adds a net11.0 target. On .NET 11 the built-in decompressors use the new span-based decoders (DeflateDecoder, GZipDecoder, ZLibDecoder), and CompressionAlgorithm.Zstd is registered with ZstandardDecoder. net10.0 keeps the stream-based implementation.

Behavior

Every built-in decompressor now follows the rules of BrotliDecoder.TryDecompress, on both target frameworks:

Input Before (Deflate/Gzip/Zlib) Now
destination too small true, truncated output false
incomplete source true, partial output false
bytes after the compressed data ignored ignored
several gzip members / Zstandard frames gzip: all decoded all decoded

TryDecompress reports bytesWritten = 0 whenever it returns false. Before, a failing handler's partial count leaked through, e.g. Brotli with a too-small destination.

Breaking change: callers that relied on truncated output for a too-small destination or an incomplete source now get false, and Decompress throws InvalidDataException.

Decisions worth a look

  • Instance API instead of the static TryDecompress methods on .NET 11. The static methods also fail when any byte follows the compressed data. The instance API stops at the end of the data, which keeps padding and trailing bytes working as before.
  • Gzip members and Zstandard frames are decoded in a loop with Reset():
    • Gzip uses the GZipStream rule from .NET 10 (Inflater.ResetStreamForLeftoverInput): another member starts if the rest begins with 1F, followed by 8B when a second byte exists.
    • Zstd follows ZstandardStream: another frame starts if the next 4 bytes are a frame or skippable-frame magic number.
  • A full destination gets a one-byte scratch buffer. The decoders report DestinationTooSmall for an empty buffer even when nothing is left to write, e.g. an empty trailing gzip member or a skippable frame.
  • net10.0 detection (TryDecompressStream and SourceStream):
    • DeflateStream only reads from its base stream while the inflater is unfinished and out of input. So SourceStream, a small UnmanagedMemoryStream subclass, flags any read past the end as an incomplete source.
    • A one-byte read after the destination is full detects output that didn't fit.
    • GZipStream reads once more after a member that ends exactly at the end of the source. A sentinel byte (0xFF, which can't start a member) answers that read. Deflate and Zlib don't get one.

Other changes

  • Test packages updated (Microsoft.NET.Test.Sdk 18.11.1, xunit.v3 4.0.3, xunit.runner.visualstudio 4.0.1, coverlet.collector 10.1.0), and src/global.json runs dotnet test on Microsoft.Testing.Platform.
  • CI installs .NET 11 and now also runs build and tests for pull requests to master. Pack, artifact upload and the NuGet push only run for pushes to master.
  • .gitattributes with * text=auto. The repository already stores LF, so no file changes.
  • JsonUtils no longer has an unneeded unsafe modifier.
  • The package version stays 3.2.0, so the push to master after merging won't publish a new package (--skip-duplicate).

Validation

New tests in IDecompressorTests.cs (the 3.2.0 tests are unchanged):

  • round trip with 256 KB for every algorithm
  • trailing bytes are ignored
  • failures: invalid data, empty source, incomplete source, too-small destination
  • gzip members, Zstandard frames (net11.0)
  • the .NET 11 span encoders (net11.0)

Checks run locally on Windows with SDK 11.0.100:

  • Tests: dotnet test -c Release on this branch in a clean worktree: 90 passed on net10.0 and net11.0, 0 build warnings.

  • Same results on both target frameworks: 61,120 generated inputs, decoded on .NET 10 and .NET 11: every prefix of the compressed data, padding variants, destination sizes, concatenated streams, flipped bytes. 0 mismatches. The net10.0 build running on the .NET 11 runtime (roll-forward) gave 0 mismatches as well.

  • Mutation check: 13 deliberate bugs in IDecompressor.cs, run against the new tests:

    • Examples: dropping the probe read, the sentinel, the gzip loop or the scratch buffer.
    • 12 make tests fail. The 13th, sentinel 0x00 instead of 0xFF, behaves identically: any value except 0x1F works.
  • net10.0 overhead vs. 3.2.0 (Stopwatch, exact-size destination):

    Payload zlib gzip
    60 B 0.69 → 0.74 µs 0.77 → 0.87 µs
    64 KB 5.65 → 5.58 µs 7.93 → 8.29 µs
    4 MB 250 → 244 µs 395 → 401 µs

🤖 Generated by Opus 5.5 (xhigh) with Claude Code in T3 Code

NotOfficer and others added 5 commits November 12, 2026 10:03
Text files are stored with LF and checked out with the platform's line
endings. Before, editors that write CRLF made every line of a touched
file show up as changed.

Co-Authored-By: Claude Opus 5.5 (xhigh) <noreply@anthropic.com>
Also updates the test packages (Microsoft.NET.Test.Sdk 18.11.1,
xunit.v3 4.0.3, xunit.runner.visualstudio 4.0.1, coverlet.collector
10.1.0) and runs dotnet test on Microsoft.Testing.Platform via
global.json.

Co-Authored-By: Claude Opus 5.5 (xhigh) <noreply@anthropic.com>
Pull requests to master now run the build and test steps. Packing and
the NuGet push still only run for pushes to master.

Co-Authored-By: Claude Opus 5.5 (xhigh) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (xhigh) <noreply@anthropic.com>
On net11.0, Deflate, Gzip and Zlib use DeflateDecoder, GZipDecoder and
ZLibDecoder, and Zstd is registered with ZstandardDecoder. net10.0 keeps
the stream-based implementation.

On both target frameworks, every built-in decompressor now follows the
rules of BrotliDecoder.TryDecompress: bytes after the compressed data
are ignored, but a destination that is too small or an incomplete
source fails. Further gzip members and Zstandard frames are
decompressed as well. TryDecompress reports 0 bytes written whenever it
fails.

BREAKING CHANGE: Deflate, Gzip and Zlib returned true with truncated
output when the destination was too small or the source was
incomplete. They now return false.

Co-Authored-By: Claude Opus 5.5 (xhigh) <noreply@anthropic.com>
@NotOfficer
NotOfficer merged commit a076b07 into master Nov 12, 2026
1 check passed
@NotOfficer
NotOfficer deleted the feat/net11-decompressors branch November 12, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant