RFC 9651 Structured Fields
Small set of cross-primitive helpers for parsing RFC 8941 Structured Fields header values without each parser open-coding its own quote-aware top-level splitter. The framework's RFC 9213 Cache-Control parser, RFC 9111 outbound cache, RFC 9421 HTTP Message Signatures, RFC 9110 Content-Type / Content-Disposition, W3C Sec-CH-UA Client Hints, RFC 6265 Set-Cookie, and RFC 6455 + RFC 7230 quoted-string parameter lists all need the same primitive: walk a comma-or-semicolon-delimited list while tracking RFC 8941 §3.3.3 quoted-string state with backslash- escape so a , or ; inside "..." doesn't fake-split the list.
splitTopLevel(s, sep) returns the array of top-level pieces. sep must be , or ;. Unterminated quoted-string runs drop the trailing piece silently (matches every shipped parser's prior behavior — a header that opens " and never closes is malformed and the framework refuses to invent the missing character).
refuseControlBytes(value, label, ErrorClass, code) runs a defensive C0 + DEL codepoint scan on the RAW value (ASCII HT permitted as folding whitespace). The throw discipline matches b.mail.requireTls.parseTlsRequiredHeader — gate the value BEFORE any .trim() strips leading/trailing C0/DEL bytes.
unquoteSfString(s) strips RFC 8941 §3.3.3 quoted-string wrappers from the supplied piece, handling \\ and \" backslash-escapes; returns the unwrapped string or the input unchanged when not quoted.
b.structuredFields.splitTopLevel(s, sep) #
Split s on top-level occurrences of sep (one of , or ;), respecting RFC 8941 §3.3.3 quoted-string boundaries with backslash-escape. Returns the array of trimmed-by-caller pieces.
Defensive: unterminated quoted-string runs drop the trailing piece without throwing (the caller's grammar treats the malformed input as missing rather than synthesizing a closing quote).
b.structuredFields.splitTopLevel('private="A, B", max-age=60', ",");
// → ['private="A, B"', ' max-age=60']
b.structuredFields.splitTopLevel('alg="x;y";nonce=42', ";");
// → ['alg="x;y"', 'nonce=42']
b.structuredFields.splitUnquoted(s, sep) #
Split s on every sep that falls OUTSIDE a "..." quoted run — the iCalendar / vCard variant of the quote-aware splitter. A " toggles the quoted state and there is NO backslash escaping of the quote (RFC 5545 3.1.1 / RFC 6350 3.3 QSAFE-CHAR excludes DQUOTE, so a " always opens or closes a run). This is deliberately simpler than splitTopLevel, which honours HTTP structured-field backslash-quote escapes; the two are NOT interchangeable. Accepts any single-character separator.
b.structuredFields.splitUnquoted('a;b="x;y";c', ";");
// returns ['a', 'b="x;y"', 'c']
b.structuredFields.stripDoubleQuotes(s) #
Strip ONE layer of surrounding " from s when both ends are a double quote (length ≥ 2), otherwise return s unchanged. The plain DQUOTE unwrap used by the iCal / vCard parsers for a quoted parameter value — no backslash-escape processing (RFC 5545 3.1 / RFC 6350 5: a quoted param value is QSAFE-CHAR, which excludes DQUOTE, so there is nothing to unescape). Distinct from unquoteSfString, which decodes HTTP structured-field \" / \\ escapes.
b.structuredFields.stripDoubleQuotes('"a;b"');
// returns 'a;b'
b.structuredFields.stripDoubleQuotes('plain');
// returns 'plain'
b.structuredFields.unfoldHeaderContinuations(value) #
Collapse RFC 5322 folding whitespace — a CRLF (or bare LF) followed by one or more spaces/tabs — back to a single space, reversing the line folding a header value may carry in transit. Used before parsing DKIM / ARC / Authentication-Results tag lists, where a folded b= / bh= value must be rejoined before its base64 is read.
b.structuredFields.unfoldHeaderContinuations("v=DKIM1;\r\n k=rsa");
// returns "v=DKIM1; k=rsa"
b.structuredFields.parseKeyValuePiece(piece, kvSep?, lowerKey?) #
Parse ONE already-split list piece into a { key, value } pair: key is the text before the first kvSep (default "="), trimmed and — unless lowerKey is false — lower-cased; value is the raw remainder (the caller trims / unquotes / sf-string-parses it per its own grammar). A piece with NO kvSep is a "bare" item — value is null — which the caller handles per its grammar (skip it, or treat as a flag-style directive).
The shared per-pair step behind every key=value list parser: the naive parseTagList loop AND the quote-aware parsers that first splitTopLevel then parse each piece (Cache-Control directives, Client-Hints brand params, Content-Type parameters). lowerKey:false serves the rare grammar whose key is case-sensitive (a verbatim tag-list passthrough).
b.structuredFields.parseKeyValuePiece("Max-Age=60"); // → { key: "max-age", value: "60" }
b.structuredFields.parseKeyValuePiece("no-store"); // → { key: "no-store", value: null }
b.structuredFields.parseKeyValuePiece("Key=a=b", "=", false); // → { key: "Key", value: "a=b" }
b.structuredFields.parseTagList(input, opts?) #
{
sep: string|RegExp, // entry separator. default: ";" (MTA-STS uses /\r?\n/)
kvSep: string, // key/value separator. default: "=" (MTA-STS uses ":")
unfold: boolean, // collapse CRLF+WSP folds to a space first (DKIM FWS). default: false
stripValueWs: boolean, // strip all whitespace inside each value (DKIM/ARC FWS). default: false
lowerKey: boolean, // lower-case each key. default: true (set false to preserve case)
}
Parse a NAIVE delimited key tag list into an ordered array of [key, value] pairs. This is the non-quote-aware sibling of splitTopLevel: it is for grammars whose RFC forbids the DQUOTE structured-string form, so a bare split is correct and a quote-aware walk would be wrong — DKIM (RFC 6376 §3.2), DMARC (RFC 7489 §6.4), ARC (RFC 8617 §4), BIMI (RFC 9091 §4), and the MTA-STS policy grammar (RFC 8461, line/colon delimited).
Pairs are returned (not a map) so a caller whose grammar permits a repeated key — MTA-STS mx: lines list one host each — keeps every occurrence; a caller wanting last-wins map semantics folds the pairs into a plain object itself. Order is preserved, so a caller that throws on a malformed value throws at the same point it would have mid-loop.
Entries that are empty after trimming, or carry no kvSep, are skipped (matching every shipped parser's prior behavior).
b.structuredFields.parseTagList("v=DKIM1; k=rsa; p=MIGf");
// → [ ["v", "DKIM1"], ["k", "rsa"], ["p", "MIGf"] ]
b.structuredFields.parseTagList("version:STSv1\nmx:a.example\nmx:b.example",
{ sep: /\r?\n/, kvSep: ":" });
// → [ ["version","STSv1"], ["mx","a.example"], ["mx","b.example"] ]
b.structuredFields.parseKeyValuePieces(pieces, startIndex?, kvSep?, lowerKey?) #
{
startIndex: number, // default: 0 — first piece treated as a key/value pair
kvSep: string, // default: "=" — key/value separator within a piece
lowerKey: boolean, // default: true — lower-case each parsed key
}
Iterate an already-split list of structured-field pieces, trimming each, dropping empties, and parsing the survivors into { key, value } records via parseKeyValuePiece. startIndex skips a leading non-pair token (the media type in a Content-Type, the brand in a Sec-CH-UA member); kvSep overrides the default "="; lowerKey:false preserves key case for a case-sensitive grammar.
The split is left to the caller because the boundary discipline differs by grammar — splitTopLevel honours quotes/parens for RFC 8941 lists, while a plain String(value).split(";") is right where inner separators cannot be quoted. Per-piece dispatch (unquote, numeric coercion, poisoned-key drops) stays with the caller; this owns only the uniform iterate-trim-skip-parse spine the parsers shared verbatim.
var kvps = b.structuredFields.parseKeyValuePieces(
b.structuredFields.splitTopLevel("max-age=600, immutable", ","));
// → [ { key: "max-age", value: "600" }, { key: "immutable", value: null } ]
b.structuredFields.forEachKeyValue(kvps, handler) #
Consume the { key, value } records from parseKeyValuePieces: skip the bare entries (those with a null value — a key that carried no separator), trim each surviving value, and invoke handler(key, trimmedValue, index). The mirror of parseKeyValuePieces on the consuming side — that primitive owns the parse spine, this owns the iterate-skip-bare-trim spine every header parser repeated verbatim before dispatching.
Per-key dispatch (sf-string unquoting, numeric coercion, poisoned-key drops, building a typed result) stays in the handler — a handler that returns skips the current entry, exactly like a continue. Parsers that instead treat a bare key as meaningful (a value-less directive) iterate the records directly rather than calling this.
var b = require("blamejs");
var out = {};
var kvps = b.structuredFields.parseKeyValuePieces("a=1; b=2".split(";"));
b.structuredFields.forEachKeyValue(kvps, function (key, value) {
out[key] = value;
});
// out → { a: "1", b: "2" }
b.structuredFields.refuseControlBytes(value, opts) #
{
ErrorClass: Function, // required — error class to throw
code: string, // required — error code (e.g. "foo/bad-header-value")
label: string, // required — operator-readable label for the value
allowHt: boolean, // default: true — permit ASCII HT (folding ws)
}
Scan a header value for C0 control characters (codepoints < 32) and DEL (127) and throw via the supplied error class when any appear. ASCII HT (9) is permitted as folding-whitespace — RFC 9110 §5.5 lists HT as a structural separator that downstream .trim() then absorbs.
Must run on the RAW value BEFORE any .trim() call. Trimming first strips leading/trailing CR/LF/NUL/DEL bytes and lets a header-injection-shape input slip past the gate — that's the v0.8.90 b.mail.requireTls.parseTlsRequiredHeader bug class.
b.structuredFields.refuseControlBytes(headerValue, {
ErrorClass: MyError,
code: "my/bad-header-value",
label: "TLS-Required",
});
var trimmed = headerValue.trim(); // safe — the gate ran on raw
b.structuredFields.unquoteSfString(s) #
Strip RFC 8941 §3.3.3 quoted-string wrapping from a piece value, handling \\ and \" backslash-escapes. Returns the unwrapped string when the piece is "..."-shaped; returns the input unchanged otherwise (tolerates bare-token values some upstream proxies emit). Returns null for an unterminated "... shape so callers can surface a parser-level error.
b.structuredFields.unquoteSfString('"hello, world"');
// → 'hello, world'
b.structuredFields.unquoteSfString('"a\\"b\\\\c"');
// → 'a"b\c'
b.structuredFields.unquoteSfString('bare');
// → 'bare' (operator-supplied bare-token form passes through)
b.structuredFields.unescapeSfStringBody(body) #
Undo the RFC 8941 §3.3.3 quoted-string backslash-escapes from the BODY of an sf-string (the bytes BETWEEN the surrounding double quotes). Only \\\\ and \\" are legal escapes; every other backslash is literal.
This is a single left-to-right scan, NOT two chained .replace() passes. The two-pass form (.replace(/\\\\/g,"\\").replace(/\\"/g,'"'), in either order) is not equivalent to a single decode: whichever pass runs first can rewrite a backslash the other escape sequence legitimately owns, so a lone escaped backslash (\\\\) decodes to two backslashes instead of one. The single pass consumes each escape exactly once. Non-string input passes through unchanged.
b.structuredFields.unescapeSfStringBody('a\\"b\\\\c');
// → 'a"b\c'
b.structuredFields.containsControlBytes(value, opts?) #
{
allowHt: boolean, // default true — permit ASCII HT
}
Predicate variant of refuseControlBytes for defensive request-shape readers that RETURN DEFAULTS rather than throw (the framework's third validation tier). Returns true when the RAW value contains any C0 / DEL byte (ASCII HT permitted by default as folding-whitespace).
function parseChallenge(headerValue) {
if (b.structuredFields.containsControlBytes(headerValue)) return null;
// ...safe to .trim() / .slice() now
}
b.structuredFields.parse(input, type, opts?) #
{
ErrorClass?: Function, // typed error class (default: native Error with .code)
}
Parse an RFC 8941 Structured Field value. type is "item", "list", or "dictionary". Returns the value model: an item is { value, params } (params is a Map); a list is an array of items / inner lists; a dictionary is a Map. Tokens, byte sequences, dates, and display strings come back as SfToken / SfByteSequence / SfDate / SfDisplayString instances so they stay distinct from plain strings and integers. Strictly enforces the grammar — integer / decimal digit caps, printable-ASCII strings, canonical base64, no trailing characters — and throws on any malformed input (pass opts.ErrorClass for a typed error).
b.structuredFields.parse("a=1, b=(x y);q=2", "dictionary");
// → Map { "a" => { value: 1, params: Map{} },
// "b" => { items: [...], params: Map{ "q" => 2 } } }
b.structuredFields.serialize(value, type, opts?) #
{
ErrorClass?: Function, // typed error class (default: native Error with .code)
}
Serialize a value model back to an RFC 8941 field value (the inverse of parse). type is "item", "list", or "dictionary". Numbers serialize as Integers when integral and Decimals (rounded to 3 fractional digits) otherwise; wrap Tokens / byte strings in SfToken / SfByteSequence. Throws on values outside the RFC's ranges or grammar (out-of-range integers, non-printable string characters, invalid tokens / keys).
var sf = b.structuredFields;
sf.serialize({ value: new sf.Token("gzip"), params: new Map([["q", 1]]) }, "item");
// → "gzip;q=1"
Last updated 2026-08-08T16:39:49.652Z by seeder.