Skip to content

Add TryParse*Header entry points that report the true header length; obsolete TryExtract* - #81

Merged
MDA2AV merged 3 commits into
mainfrom
fix/58-consumed-count
Nov 3, 2026
Merged

MDA2AV merged 3 commits into
mainfrom
fix/58-consumed-count

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Fixes #58

Why

TryExtractFullHeader* (and the response and FlexibleParser equivalents) report one less than the bytes they consumed, and the XML doc said the opposite. Every caller compensates with bytesRead + 1, including GenHTTP and our own example.

This takes option 2 from the issue. Fixing the value in place (option 1) would break every one of those callers silently: no compile error and no exception. The first body byte would be read as header, and a Content-Length body would arrive one byte short, which desyncs a keep-alive connection. Renaming the correct methods and marking the old ones [Obsolete] moves that break to compile time, the only place anyone can see it.

What changes

New entry points. Each returns consumed: the length of the header block including its CRLFCRLF, so the body starts at input[consumed]. It is 0 when the method returns false.

Obsolete (reports length − 1, or −1) Replacement (reports the length)
UltraHardenedParser.TryExtractFullHeaderROM(ref ReadOnlyMemory<byte>, …) UltraHardenedParser.TryParseRequestHeader(ref ReadOnlyMemory<byte>, …)
UltraHardenedParser.TryExtractFullHeaderValidated(ref ReadOnlySequence<byte>, …) UltraHardenedParser.TryParseRequestHeader(ref ReadOnlySequence<byte>, …)
UltraHardenedParser.TryExtractFullResponseHeaderROM(…) UltraHardenedParser.TryParseResponseHeader(ref ReadOnlyMemory<byte>, …)
UltraHardenedParser.TryExtractFullResponseHeaderValidated(…) UltraHardenedParser.TryParseResponseHeader(ref ReadOnlySequence<byte>, …)
FlexibleParser.TryExtractFullHeaderReadOnlyMemory(…) FlexibleParser.TryParseRequestHeader(ref ReadOnlyMemory<byte>, …)
FlexibleParser.TryExtractFullHeader(…) FlexibleParser.TryParseRequestHeader(ref ReadOnlySequence<byte>, …)

Migration: switch to the new name and drop the + 1. The parameters are otherwise identical.

  • The methods are renamed in place: same files, same bodies. Only the signature, the docs and the two lines that assign the count change.
  • The old names live in UltraHardenedParser.Obsolete.cs / FlexibleParser.Obsolete.cs as thin wrappers that return exactly the legacy values (consumed - 1, or -1 when incomplete). Their docs now say what they actually return.
  • [Obsolete] is a warning (CS0618), not an error. The message names the difference and the replacement. These members are slated for removal in a later release.
  • Docs move to the new API: README (which also corrects "advance your reader by bytesRead"), SECURITY.md, site/glyph11.html (including the PipeReader sample that advanced by bytesRead + 1), and Examples/Glyph11.Example.
  • Glyph11 version goes to 0.4.0.

Not touched, on purpose:

  • PicoParser, core/ and the diff harness: bindings are being removed in a separate PR.
  • Benchmarks/: the bindings PR (Drop the bindings and the C core: Glyph11 is managed-only #84) rewrites that file, so its two Glyph11 calls stay on the obsolete names here to avoid a conflict. They build with two CS0618 warnings, and once both PRs are merged they need a two-line rename.

GenHTTP builds with TreatWarningsAsErrors, so it needs a migration (new names, no + 1): Kaliumhexacyanoferrat/GenHTTP#972. The draft PR Kaliumhexacyanoferrat/GenHTTP#973 does it. Its full acceptance suite passes on net10.0 and net11.0 against a local pack of this repo with every open PR merged.

Decisions worth reviewing

  • New names rather than overloads. An overload that differs only in what the out int means cannot coexist with the old one: same name, same parameter list. Changing it to out long would make out var ambiguous. New names keep the migration a rename.
  • The test project suppresses CS0618. The existing suites reach the legacy entry points through a handful of helpers (17 call sites), and they keep doing so on purpose: those entry points still ship and must keep their exact behaviour until removed. Migrating the helpers would also collide with sibling PRs that edit the same test files.

Overlap with sibling PRs

#59, #64, #65, #68, #71 and #73 edit the bodies of these same methods, which keep their files and structure here. Expect small textual conflicts around the signature and the bytesReadCount lines. Whichever lands second must also rename any new line it adds that assigns bytesReadCount to consumed, keeping the new meaning (the full length, or 0 when incomplete).

Tests

New tests/Tests/ConsumedCount.Tests.cs, 221 cases. Every new entry point is fed four shapes: contiguous memory, a single-segment sequence, three segments, and one byte per segment.

  • consumed equals the header block length, and what follows it (a body or the next pipelined message) starts exactly there.
  • Incomplete input returns false with consumed == 0.
  • The obsolete entry points return consumed - 1 on the same corpus, and -1 when incomplete.

A mutation check (consumed = total - 1) fails 35 tests. Full suite: 624 passing, 0 failing (net10.0). dotnet build src/Glyph11.sln is clean; the only CS0618 warnings are the two in Benchmarks/ noted above.

SonarCloud

Two findings here are expected, and the code stays as is:

  • S1133 on the six obsolete wrappers: the reminder to remove deprecated code, which is what they are for.
  • S3776 (cognitive complexity) on the three renamed parse methods. Their complexity is unchanged (34, 109 and 76). The rename touches the line the existing issue sits on, so SonarCloud reports it as new here.

TryExtractFullHeader* (and the response and FlexibleParser equivalents)
report one less than the bytes they consumed, while their XML docs said
the opposite. Every caller compensates with "bytesRead + 1", so
correcting the value in place would break them silently: the first body
byte would be read as header and a Content-Length body would arrive one
byte short - a desync on a keep-alive connection, the failure class this
library exists to prevent.

So the correctly-specified methods get new names and the old ones stay,
unchanged in behaviour, marked [Obsolete] with a message naming the
difference. The migration then shows up at compile time, the only place
it can be seen:

  UltraHardenedParser.TryExtractFullHeaderROM / ...Validated
    -> UltraHardenedParser.TryParseRequestHeader
  UltraHardenedParser.TryExtractFullResponseHeaderROM / ...Validated
    -> UltraHardenedParser.TryParseResponseHeader
  FlexibleParser.TryExtractFullHeaderReadOnlyMemory / TryExtractFullHeader
    -> FlexibleParser.TryParseRequestHeader

each with a ReadOnlyMemory<byte> and a ReadOnlySequence<byte> overload.
'consumed' is the length of the header block including its CRLFCRLF, so
the body starts at input[consumed]; it is 0 when the method returns
false. The methods are renamed in place (same files, same bodies); the
legacy names are thin wrappers in *.Obsolete.cs returning consumed - 1,
or -1 when incomplete.

The test project suppresses CS0618 so the existing suites keep pinning
the legacy entry points for as long as they ship; ConsumedCount.Tests.cs
covers the new ones and their parity with the old. README, SECURITY.md,
the docs site and the managed example move to the new API. Glyph11 goes
to 0.4.0.

Fixes #58.
The MemberData sources returned untyped object[] rows (xUnit1042).
Same 221 cases.
# Conflicts:
#	README.md
#	site/glyph11.html
@MDA2AV
MDA2AV merged commit 35eeb5f into main Nov 3, 2026
1 of 2 checks passed
MDA2AV added a commit that referenced this pull request Nov 3, 2026
…89)

The harness called TryExtractFullHeaderROM and TryExtractFullHeaderValidated, obsolete
since #81, and cleared the request before each call, which the parser does itself since
#76. The multi-segment comment now says that only the header block is linearized (#86).
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.

bytesReadCount is one less than the bytes consumed, and the doc says otherwise

1 participant