Skip to content

fix: implement the RFC 9110 media-type grammar strictly - #82

Open
mcollina wants to merge 1 commit into
perf/charcode-scannerfrom
fix/rfc9110-strict
Open

fix: implement the RFC 9110 media-type grammar strictly#82
mcollina wants to merge 1 commit into
perf/charcode-scannerfrom
fix/rfc9110-strict

Conversation

@mcollina

Copy link
Copy Markdown
Member

Stacked on #81. Addresses @blakeembrey's review there: the "quirks" the scanner had carried over from the previous regular expressions were bugs, and last-parameter-wins diverged from the rest of the ecosystem.

Adversarial review against RFC 9110

I went through the scanner clause by clause against RFC 9110 §8.3.1, §5.6.2 (token), §5.6.3 (OWS), §5.6.4 (quoted-string), §5.6.6 (parameters) and §5.5 (field value whitespace). Findings, all fixed here:

# Input #81 RFC 9110 This PR
1 te`xt/ht`ml invalid media type ` is a tchar accepted
2 text/html; a="x<HTAB>y" invalid parameter format HTAB is qdtext accepted
3 text/html; a="x<VT>y" accepted VT is not qdtext rejected
4 text/html; a="\<DEL>" accepted quoted-pair is HTAB / SP / VCHAR / obs-text; DEL is none of those rejected
5 <HTAB>text/html<HTAB>; a=b<HTAB> invalid (HTAB after ; / before the value end) OWS = SP / HTAB accepted
6 \r\ntext/html\r\n,  text/html, text/html\n; a=b, … accepted (String.prototype.trim() set) only OWS may surround a field value (§5.5) rejected
7 text/html;, text/html; ; a=b invalid parameter format parameters = *( OWS ";" OWS [ parameter ] ); Appendix B: "Parameters in media type … can be empty via one or more trailing semicolons" accepted
8 text/html; charset=1; charset=2 charset=2 unspecified charset=1, matching util.MIMEType, WHATWG MIME Sniffing and content-type@2

Things I checked and left as they were, for the record:

  • type/subtype/parameter names are lower-cased; values are untouched (§8.3.1: "Parameter values might or might not be case-sensitive").
  • Empty quoted values (foo="") are valid; empty token values (foo=) are not.
  • Code units above 0xff are rejected everywhere: header values are octets, and obs-text stops at %xFF.
  • __proto__ / constructor as parameter names land as ordinary own keys on the null-prototype object (test added).
  • RFC 6838 §4.2 restricts registered names further (first char alphanumeric, ≤127 chars, no ` / ' / * / % / | / ~). Not enforced: this parser implements the HTTP grammar, which is token, and */*-style ranges would break.
  • No whitespace is permitted around = (§5.6.6), nor between type, / and subtype.

Implementation

  • One TCHAR class shared by type, subtype, names and token values.
  • QDTEXT and QUOTED_PAIR bits in the lookup table (QUOTED_PAIR = QDTEXT ∪ { DQUOTE, "\" }), so the escape check is a single table lookup.
  • isTrimWhitespace is gone; every whitespace loop is code !== SP && code !== HTAB.
  • First-occurrence-wins is a parameters[name] === undefined check on the null-prototype object.

Verification

  • Differential fuzzing against a reference parser transcribed literally from the RFC 9110 ABNF into regular expressions (^OWS token "/" token ( OWS ";" OWS [ token "=" ( token / quoted-string ) ] )* OWS $): two generators (random structural characters; well-formed headers with mutations), 5 seeds × 1M inputs, 0 mismatches in accepted/rejected status, error message, type and parameters.
  • npm test: 98 tests, 100% coverage, tstyche; npm run lint clean.
  • npm run benchmark: within noise of perf: replace regex parser with a charCode scanner #81 (application/json; charset=utf-8: 5.13M vs 5.17M ops/sec).

Breaking changes

This is semver-major. Inputs that previously parsed and now throw / return defaultContentType: CR, LF, FF, VT or Unicode whitespace around the media type (#6), VT inside quoted strings (#3), \<DEL> (#4). Inputs that previously threw and now parse: #1, #2, #5, #7. Duplicate parameters now resolve to the first value (#8). The README gains a "Grammar" section documenting all of this.

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_018GeeQqdAmma5RNEEr7r75C

Addresses the review on #81: the quirks carried over from the previous
regular expressions were bugs, not behaviour worth preserving.

- tchar includes "`" in type and subtype, as in parameter names
- qdtext accepts HTAB, not VT
- quoted-pair accepts HTAB / SP / VCHAR / obs-text; DEL (0x7f) is rejected
- whitespace is OWS (SP / HTAB) only: the String.prototype.trim() set,
  CR, LF, FF, VT and Unicode whitespace are rejected
- OWS is accepted on both sides of ";" and after the last parameter
- empty parameters ("text/html;", "text/html; ; a=b") are accepted, as
  allowed by RFC 9110 Section 5.6.6
- the first occurrence of a duplicate parameter wins, matching
  util.MIMEType, the WHATWG MIME Sniffing Standard and content-type

Verified by differential fuzzing against a reference parser transcribed
from the RFC 9110 ABNF: 5M inputs, 0 mismatches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018GeeQqdAmma5RNEEr7r75C

@jsumners jsumners left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not familiar with the code in this repo, so the diff doesn't really tell me much. But the intention is a good one. If it works as advertised, that's a good thing. So have a stamp.

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.

2 participants