moon-sfv

RFC 9651 Structured Field Values parser, serializer, and conformance toolkit for MoonBit.

moon add 6P66006/moon-sfv@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
11 days ago
Downloads
3
README

#moon-sfv

RFC 9651 Structured Field Values for HTTP — parser, serializer, and conformance toolkit for MoonBit.

moon-sfv is a strict, dependency-free implementation of RFC 9651 ("Structured Field Values for HTTP", formerly RFC 8941). It parses and serializes the three Structured Fields top-level types — Items, Lists, and Dictionaries — with exact, lossless semantics, and ships a conformance harness that runs the official HTTP Working Group test vectors against the implementation.

#Goals and scope

Goal. Provide a strict, exact, dependency-free MoonBit implementation of RFC 9651 so MoonBit applications can build and consume Structured Fields interoperably, plus tooling to prove conformance.

Scope.
  • Full parse / serialize / canonicalize support for all eight bare-item types and all three top-level containers (Item, List, Dictionary), including parameters, inner lists, multi-line fields, and the omit-empty-field rule.
  • Exact decimal arithmetic (no floating point), structured errors, and configurable input limits for hostile HTTP traffic.
  • A conformance harness running the official HTTP Working Group test vectors, and a CLI (sfv-tool) for ad-hoc checking.

Out of scope. Field-specific semantics (what a given field means) and stdin input for the CLI. The module namespace is 6P66006/moon-sfv.

#What is RFC 9651?

HTTP headers are normally defined as opaque strings, which forces every implementation to reinvent fragile parsing. RFC 9651 standardizes a small set of data types — integers, decimals, strings, tokens, byte sequences, booleans, dates, and display strings — and the exact algorithms for parsing and serializing them. Fields built on Structured Fields get a well-defined syntax, a canonical wire form, and interoperable behavior by construction.

Common fields already defined in terms of Structured Fields include Cache-Status, CDN-Cache-Control, Priority, Accept-CH, and many others.

#Supported types

moon-sfv implements every abstract type in RFC 9651:

TypeWire exampleBareItem variant
Integer42, -123456789012345Integer(Int64)
Decimal4.5, -0.125Decimal(SfDecimal)
String"hello world"StringItem(String)
Tokenfoo/bar:bazToken(String)
Byte Sequence:cHJldGVuZCB0aGlzIGlzIGJpbmFyeSBjb250ZW50Lg==:ByteSequence(Bytes)
Boolean?1, ?0Boolean(Bool)
Date@1659578233Date(Int64)
Display String%"hello %c3%a9"DisplayString(String)

The three top-level containers are Item, SfList, and SfDictionary; items can carry Parameters, and lists and dictionaries can contain InnerList members. Decimals use an exact coefficient × 10^-scale representation (no floating point), and serialization rounds to three decimal places using ties-to-even, exactly as the RFC requires.

#Parsing

fn main {
match parse_item("5; foo=bar") {
Err(e) => println(e.to_string())
Ok(item) => {
// item.bare -> Integer(5)
// item.parameters.len() -> 1
// item.parameters.get_by_key("foo") -> Some(Token("bar"))
}
}
}

The public entry points are parse_item, parse_list, and parse_dictionary (plus *_bytes and *_with_limits variants), and the multi-line helpers parse_item_lines, parse_list_lines, and parse_dictionary_lines.

#Serialization

let item = parse_item("5; a=?1; b=?0")?
match serialize_item(item) {
Err(e) => println(e.to_string())
Ok(wire) => println(wire) // "5;a;b=?0" — Boolean true is omitted
}

serialize_item returns Result[String, SfError]; serialize_list and serialize_dictionary return Result[SerializedField, SfError], where the Omit variant expresses the RFC 9651 rule that an empty List or Dictionary is represented by omitting the field entirely — the caller can distinguish "field omitted" from "field with an empty value".

#Canonicalization

// "0002" -> "2"
// "4.500" -> "4.5"
// "5; a=?1" -> "5;a"
// "1, 42" -> "1, 42"
canonicalize(input, FieldType::List)?

canonicalize parses then strictly re-serializes, producing the canonical wire form. It is idempotent: canonicalize(canonicalize(x)) equals canonicalize(x), and parse(serialize(value)) is semantically equal to value.

#Error handling

Errors are structured, never bare strings:

match parse_item("abc, def") {
Err(e) => {
e.kind() // SfErrorKind::TrailingInput
e.offset() // 3 — a UTF-8 byte offset into the input
e.to_string() // "trailing input after value at byte 3: near \", def\""
}
Ok(_) => ()
}

There are 25 distinct SfErrorKind values covering every failure category. Offsets are UTF-8 byte offsets (the same unit the parser works in), and error context is truncated so errors never echo unbounded input back.

#Command-line tool

sfv-tool is a small checker built on the library.

sfv-tool parse --type item "5; foo=bar" sfv-tool validate --type list "1, 42" sfv-tool canonicalize --type dictionary "a=1, b=2;c" sfv-tool roundtrip --type item "0002" sfv-tool conformance

Field commands read the input from the single positional argument; errors are printed with the error kind and byte offset and exit with a non-zero status. Byte sequences are rendered as hex, and dates keep their raw seconds value.

#Building and running

Requires the MoonBit toolchain (moon, moonc, moonrun). No external MoonBit dependency is used beyond the standard library.

# check, build, and test on the default target moon check moon build moon test # test a specific target moon test --target native moon test --target js moon test --target wasm-gc # run the CLI moon run cmd/sfv-tool -- --help # run an example moon run examples/parse_item # run the official httpwg conformance suite moon run cmd/sfv-tool -- conformance # full verification for all targets (format, build, test, snapshot check) powershell -ExecutionPolicy Bypass -File scripts/verify_all.ps1

#Security limits

Parsing is bounded by a configurable ParseLimits structure. The defaults satisfy every RFC 9651 requirement (1024+ List members, 256 parameters, 1024+ String characters, 512+ Token characters, 16384+ decoded Byte Sequence octets) while capping hostile inputs: 4 MiB of input, 100k members, and 1 MiB per string/sequence. Every cursor read is bounds-checked, and truncation at any byte position is covered by tests.

#Test status

  • 113 unit/property tests, covering every type, boundaries, rounding, escaping, truncation safety, multi-line fields, and a fixed-seed fuzzer that round-trips thousands of generated values.
  • 1591 official HTTP Working Group vectors imported into the conformance harness. Current results: required valid 721/721, required invalid 864/864, canonical round-trip 721/721, optional 6/6, expected structure 717/717, zero failures.
  • Verified on the native, js, and wasm-gc targets with 0 errors and 0 warnings.

#Not yet verified / out of scope

  • Reading input from stdin in sfv-tool (the CLI currently accepts input only as a command-line argument).
  • Benchmarks and long-running fuzz campaigns (the bundled fuzzer is deterministic and bounded for CI).
  • Field-level semantics beyond the RFC grammar — this library validates and canonicalizes syntax, it does not interpret field-specific meaning.

#Development

See docs/architecture.md for the internal design, docs/testing.md for how to run the full suite, and CONTRIBUTING.md for contribution guidelines. The import of official test data is documented in THIRD_PARTY_NOTICES.md.

This is a local development project. It is not published, and no maintainer or author information is attached.

#
BareItem

pub(all) enum BareItem {
Integer(Int64)
Decimal(SfDecimal)
StringItem(String)
Token(String)
ByteSequence(Bytes)
Boolean(Bool)
Date(Int64)
DisplayString(String)
} derive(Eq,
Debug
)

A bare item: one of the eight Structured Fields scalar types.

#
BareItem::boolean

fn BareItem::boolean(v : Bool) -> BareItem

#
BareItem::byte_sequence

fn BareItem::byte_sequence(v : Bytes) -> BareItem

#
BareItem::date

fn BareItem::date(v : Int64) -> BareItem

#
BareItem::decimal

fn BareItem::decimal(v : SfDecimal) -> BareItem

#
BareItem::display_string

fn BareItem::display_string(v : String) -> BareItem

#
BareItem::integer

fn BareItem::integer(v : Int64) -> BareItem

Convenience constructors for bare items.

#
BareItem::string_item

fn BareItem::string_item(v : String) -> BareItem

#
BareItem::token

fn BareItem::token(v : String) -> BareItem

#
ConformanceStats

pub(all) struct ConformanceStats {
required_valid_total : Int
required_valid_passed : Int
required_invalid_total : Int
required_invalid_passed : Int
canonical_total : Int
canonical_passed : Int
optional_total : Int
optional_passed : Int
optional_failed : Int
expected_total : Int
expected_passed : Int
failures : Array[String]
} derive(Eq,
Debug
)

The conformance harness that runs the imported httpwg structured-field-tests vectors against this implementation and reports categorized pass/fail statistics.

Required tests (per RFC 9651 MUST/SHOULD behavior) must all pass; a single required failure is a conformance bug. Optional (can_fail) tests are counted and reported but do not fail the run. The report is also used by the sfv-tool conformance CLI command.

#
ConformanceStats::summary_line

fn ConformanceStats::summary_line(self : ConformanceStats) -> String

A one-line summary line for the CLI / test report.

#
Cursor

pub struct Cursor {
input : Bytes
position : Int
}

A bounds-checked cursor over the UTF-8 byte encoding of a structured field value.

All reads are checked against the end of the input; index arithmetic never reaches the underlying array without a length check first. The cursor's position is reported in UTF-8 byte offsets, which is exactly the unit used for error offsets in [SfError].

#
Cursor::at

fn Cursor::at(self : Cursor, offset : Int) -> Byte?

Reads the byte at absolute offset offset, or None out of bounds.

#
Cursor::checkpoint

fn Cursor::checkpoint(self : Cursor) -> Int

Saves the current position for later restoration.

#
Cursor::consume

fn Cursor::consume(self : Cursor) -> Byte?

Reads and consumes the byte at the current position, or None at end of input.

#
Cursor::consume_if

fn Cursor::consume_if(self : Cursor, b : Byte) -> Bool

Consumes b if it is the next byte, returning whether it matched.

#
Cursor::context_string

fn Cursor::context_string(self : Cursor, offset : Int, limit : Int) -> String

Builds a short, lossy-decoded display context starting at byte offset (see [SfError::context]). The slice is capped so errors never echo an unbounded amount of input.

#
Cursor::expect

fn Cursor::expect(self : Cursor, b : Byte) -> Bool

Alias of [Cursor::consume_if] for call sites that read more naturally with "expect".

#
Cursor::input

fn Cursor::input(self : Cursor) -> Bytes

The full input behind the cursor, copied.

#
Cursor::input_length

fn Cursor::input_length(self : Cursor) -> Int

The total length of the input, without copying.

#
Cursor::is_end

fn Cursor::is_end(self : Cursor) -> Bool

Whether the cursor has reached the end of the input.

#
Cursor::new

fn Cursor::new(input : Bytes) -> Cursor

Creates a cursor over input.

#
Cursor::peek

fn Cursor::peek(self : Cursor) -> Byte?

The byte at the current position, or None at end of input.

#
Cursor::peek_n

fn Cursor::peek_n(self : Cursor, n : Int) -> Byte?

The byte n bytes ahead of the current position, or None if that offset is past the end of the input.

#
Cursor::position

fn Cursor::position(self : Cursor) -> Int

The current UTF-8 byte offset.

#
Cursor::remaining

fn Cursor::remaining(self : Cursor) -> Int

Number of bytes remaining after the current position.

#
Cursor::restore

fn Cursor::restore(self : Cursor, pos : Int) -> Unit

Restores a previously saved position.

#
Cursor::skip_ows

fn Cursor::skip_ows(self : Cursor) -> Unit

Skips OWS (SP / HTAB). Used between List and Dictionary members, where the RFC allows tab characters.

#
Cursor::skip_spaces

fn Cursor::skip_spaces(self : Cursor) -> Unit

Skips SP (0x20) characters. The Item grammar only permits SP, not HTAB.

#
Cursor::slice

fn Cursor::slice(self : Cursor, start : Int, end : Int) -> Bytes

Returns a copy of the input bytes in the half-open range [start, end). Clamps out-of-range bounds.

#
DictionaryEntry

pub(all) struct DictionaryEntry {
key : String
value : ListMember
} derive(Eq,
Debug
)

One member of a Dictionary.

#
ExpectedBare

pub enum ExpectedBare {
ExpInteger(Int64)
ExpDecimal(Int64, Int)
ExpString(String)
ExpToken(String)
ExpBinary(String)
ExpBoolean(Bool)
ExpDate(Int64)
ExpDisplayString(String)
}

The abstract types used to express expected values from the httpwg test suite.

#
ExpectedField

pub enum ExpectedField {
ExpItemField(ExpectedItem)
ExpListField(Array[ExpectedMember])
ExpDictField(Array[(String, ExpectedMember)])
}

#
ExpectedItem

pub struct ExpectedItem {
bare : ExpectedBare
params : Array[ExpectedParam]
}

#
ExpectedMember

pub enum ExpectedMember {
ExpItemMember(ExpectedItem)
ExpInnerList(Array[ExpectedItem], Array[ExpectedParam])
}

#
ExpectedParam

pub struct ExpectedParam {
key : String
value : ExpectedBare
}

#
FieldType

pub(all) enum FieldType {
Item
List
Dictionary
} derive(Eq,
Debug
)

The three top-level Structured Fields types.

#
FieldType::from_wire_name

fn FieldType::from_wire_name(s : String) -> FieldType?

Parses a wire field-type name ("item", "list", "dictionary").

#
FieldType::parse

fn FieldType::parse(self : FieldType, input : String) -> Result[ParsedValue, SfError]

Parses input as a field of the given type, returning a type-erased [ParsedValue]. Useful when the field type is only known at runtime.

#
FieldType::wire_name

fn FieldType::wire_name(self : FieldType) -> String

The wire name of a field type, used by the CLI and the conformance harness.

#
HttpwgCase

pub struct HttpwgCase {
name : String
header_type : String
raw : Array[String]
must_fail : Bool
can_fail : Bool
canonical : Array[String]?
expected : ExpectedField?
}

#
InnerList

pub(all) struct InnerList {
items : Array[Item]
parameters : Parameters
} derive(Eq,
Debug
)

An Inner List: an ordered array of Items plus its own parameters.

#
InnerList::items

fn InnerList::items(self : InnerList) -> Array[Item]

The ordered Items of an Inner List.

#
InnerList::len

fn InnerList::len(self : InnerList) -> Int

The number of Items in the Inner List.

#
InnerList::new

fn InnerList::new() -> InnerList

An empty Inner List.

#
Item

pub(all) struct Item {
bare : BareItem
parameters : Parameters
} derive(Eq,
Debug
)

An Item: a bare item with associated parameters.

#
Item::of

fn Item::of(bare : BareItem) -> Item

Convenience constructor for an Item from a bare item with no parameters.

#
Item::with_parameters

fn Item::with_parameters(bare : BareItem, parameters : Parameters) -> Item

Convenience constructor for an Item from a bare item and parameters.

#
ListMember

pub(all) enum ListMember {
ItemMember(Item)
InnerListMember(InnerList)
} derive(Eq,
Debug
)

One member of a List or Dictionary: an Item or an Inner List.

#
ListMember::inner_list

fn ListMember::inner_list(v : InnerList) -> ListMember

Convenience constructor for a member holding an Inner List.

#
ListMember::item

fn ListMember::item(v : Item) -> ListMember

Convenience constructor for a member holding an Item.

#
Parameter

pub(all) struct Parameter {
key : String
value : BareItem
} derive(Eq,
Debug
)

A single key/value parameter.

#
Parameters

pub(all) struct Parameters {
entries : Array[Parameter]
} derive(Eq,
Debug
)

An ordered map of parameters attached to an Item or Inner List.

Duplicate keys parsed from the wire collapse to their last occurrence (RFC 9651 §4.2.3.2). The map preserves insertion order.

#
Parameters::entries

fn Parameters::entries(self : Parameters) -> Array[Parameter]

The ordered list of (key, value) entries.

#
Parameters::get_by_index

fn Parameters::get_by_index(self : Parameters, i : Int) -> Parameter?

Access by position.

#
Parameters::get_by_key

fn Parameters::get_by_key(self : Parameters, key : String) -> BareItem?

Access by key; returns the stored value.

#
Parameters::is_empty

fn Parameters::is_empty(self : Parameters) -> Bool

Whether the parameter map is empty.

#
Parameters::keys

fn Parameters::keys(self : Parameters) -> Array[String]

A list of the parameter keys, in order.

#
Parameters::len

fn Parameters::len(self : Parameters) -> Int

The number of parameters.

#
Parameters::new

fn Parameters::new() -> Parameters

An empty parameter map.

#
Parameters::set

fn Parameters::set(self : Parameters, key : String, value : BareItem) -> Unit

Insert or replace (by key) a parameter. A duplicate key keeps its original position and receives the new value, matching RFC 9651 §4.2.3.2.

#
ParseLimits

pub(all) struct ParseLimits {
max_input_bytes : Int
max_members : Int
max_parameters : Int
max_string_bytes : Int
max_inner_list_items : Int
max_nesting_depth : Int
} derive(Eq,
Debug
)

Resource limits applied while parsing. RFC 9651 requires parsers to support at least 1024 List/Dictionary members, 256 parameters, 1024 String characters, 512 Token characters, and 16384 decoded Byte Sequence octets; the defaults below satisfy all of those while still bounding hostile inputs.

#
ParseLimits::default

fn ParseLimits::default() -> ParseLimits

The default [ParseLimits], comfortably above every RFC requirement.

#
ParsedValue

pub(all) enum ParsedValue {
ItemValue(Item)
ListValue(SfList)
DictionaryValue(SfDictionary)
}

The parsed field value, erased across the three top-level types.

#
ParsedValue::serialize

fn ParsedValue::serialize(self : ParsedValue) -> Result[SerializedField, SfError]

Serializes a type-erased parsed value back to its canonical wire form.

#
SerializedField

pub(all) enum SerializedField {
Omit
Value(String)
} derive(Eq,
Debug
)

The result of serializing a whole field. An empty List or Dictionary is represented by omitting the field entirely (RFC 9651 §4.1 step 1), so the result is [Omit] rather than an empty string, letting callers distinguish "field omitted" from "field with an empty value".

#
SerializedField::is_omitted

fn SerializedField::is_omitted(self : SerializedField) -> Bool

Whether the field is omitted entirely.

#
SerializedField::to_string

fn SerializedField::to_string(self : SerializedField) -> String

Convenience accessors for [SerializedField].

#
SfDecimal

pub(all) struct SfDecimal {
coefficient : Int64
scale : Int
} derive(
Debug
)

Exact decimal arithmetic for Structured Field Decimals (RFC 9651 §3.3.2).

A decimal is stored as an exact rational coefficient × 10^-scale using an Int64 coefficient. No floating-point type is used anywhere in the value's representation, so parsing, comparison, and rounding are exact.

Wire-format restrictions (at most 3 fractional digits, at most 12 integer digits) are enforced by the parser and by serialization.
impl Eq for SfDecimal

#
SfDecimal::coefficient

fn SfDecimal::coefficient(self : SfDecimal) -> Int64

The signed coefficient.

#
SfDecimal::compare

fn SfDecimal::compare(self : SfDecimal, other : SfDecimal) -> Int

Exact comparison. Returns a negative value if self < other, zero if equal, positive if self > other.

#
SfDecimal::from_parts

fn SfDecimal::from_parts(coefficient : Int64, scale : Int) -> SfDecimal

Constructs a decimal from raw parts; alias of [SfDecimal::new].

#
SfDecimal::is_negative

fn SfDecimal::is_negative(self : SfDecimal) -> Bool

Whether the value is negative (nonzero).

#
SfDecimal::new

fn SfDecimal::new(coefficient : Int64, scale : Int) -> SfDecimal

Constructs a decimal coefficient × 10^-scale, normalizing -0.

#
SfDecimal::normalize

fn SfDecimal::normalize(self : SfDecimal) -> SfDecimal

Strips trailing zeros from the fractional part and normalizes -0. Example: 1.200 (1200, 3) becomes 1.2 (12, 1).

#
SfDecimal::scale

fn SfDecimal::scale(self : SfDecimal) -> Int

The exponent (value = coefficient × 10^-scale).

#
SfDecimal::to_canonical_string

fn SfDecimal::to_canonical_string(self : SfDecimal) -> Result[String, SfError]

Serializes the decimal per RFC 9651 §4.1.5: rounds to three decimal places using ties-to-even, rejects more than twelve integer digits, and always keeps at least one fractional digit.

#
SfDictionary

pub(all) struct SfDictionary {
entries : Array[DictionaryEntry]
} derive(Eq,
Debug
)

A Dictionary: an ordered map of key to member.

Duplicate keys parsed from the wire collapse to their last occurrence (RFC 9651 §4.2.2). The map preserves insertion order.

#
SfDictionary::entries

The ordered list of (key, member) entries.

#
SfDictionary::get_by_index

fn SfDictionary::get_by_index(self : SfDictionary, i : Int) -> DictionaryEntry?

Access a member by position.

#
SfDictionary::get_by_key

fn SfDictionary::get_by_key(self : SfDictionary, key : String) -> ListMember?

Access a member by key.

#
SfDictionary::is_empty

fn SfDictionary::is_empty(self : SfDictionary) -> Bool

Whether the Dictionary is empty (i.e. the field should be omitted).

#
SfDictionary::keys

fn SfDictionary::keys(self : SfDictionary) -> Array[String]

A list of the member keys, in order.

#
SfDictionary::len

fn SfDictionary::len(self : SfDictionary) -> Int

The number of members.

#
SfDictionary::new

An empty Dictionary.

#
SfDictionary::set

fn SfDictionary::set(self : SfDictionary, key : String, entry_member : ListMember) -> Unit

Insert or replace (by key) a member. A duplicate key keeps its original position and receives the new value, matching RFC 9651 §4.2.2.

#
SfError

pub(all) struct SfError {
kind : SfErrorKind
offset : Int
context : String
} derive(Eq,
Debug
)

#
SfError::context

fn SfError::context(self : SfError) -> String

A short description of the surrounding input.

#
SfError::kind

fn SfError::kind(self : SfError) -> SfErrorKind

The category of the failure.

#
SfError::make

fn SfError::make(kind : SfErrorKind, offset : Int, context : String) -> SfError

Builds a structured error.

#
SfError::offset

fn SfError::offset(self : SfError) -> Int

The UTF-8 byte offset into the input at which the failure occurred.

#
SfError::to_string

fn SfError::to_string(self : SfError) -> String

Renders a human-readable, single-line description of the error.

The context is truncated so that very large inputs are never echoed back in full.

#
SfErrorKind

pub(all) enum SfErrorKind {
UnexpectedEnd
UnexpectedByte(Byte)
InvalidTopLevelType
InvalidKey
InvalidInteger
IntegerOutOfRange
InvalidDecimal
DecimalOutOfRange
InvalidString
InvalidEscape
InvalidToken
InvalidByteSequence
InvalidBase64
InvalidBoolean
InvalidDate
InvalidDisplayString
InvalidPercentEncoding
InvalidUtf8
InvalidParameter
InvalidInnerList
InvalidDictionary
TrailingInput
TooManyMembers
TooManyParameters
InputTooLarge
SerializationError
} derive(Eq,
Debug
)

Structured error type for all parsing, serialization, and canonicalization failures in this crate.

The error carries a structured kind, a UTF-8 byte offset into the original input, and a short human-readable context. Offsets are byte offsets into the UTF-8 encoding of the input, not Unicode scalar counts.

#
SfErrorKind::label

fn SfErrorKind::label(self : SfErrorKind) -> String

Returns the label of an error kind without offset/context details.

#
SfList

pub(all) struct SfList {
members : Array[ListMember]
} derive(Eq,
Debug
)

A List: an ordered array of members.

#
SfList::get_by_index

fn SfList::get_by_index(self : SfList, i : Int) -> ListMember?

Access a member by position.

#
SfList::is_empty

fn SfList::is_empty(self : SfList) -> Bool

Whether the List is empty (i.e. the field should be omitted).

#
SfList::len

fn SfList::len(self : SfList) -> Int

The number of members.

#
SfList::members

fn SfList::members(self : SfList) -> Array[ListMember]

The ordered list of members.

#
SfList::new

fn SfList::new() -> SfList

An empty List.

#
canonicalize

fn canonicalize(input : String, field_type : FieldType) -> Result[SerializedField, SfError]

Canonicalizes a field value of the given type.

#
canonicalize_dictionary

fn canonicalize_dictionary(input : String) -> Result[SerializedField, SfError]

Canonicalizes a Dictionary field value.

#
canonicalize_item

fn canonicalize_item(input : String) -> Result[SerializedField, SfError]

Canonicalizes an Item field value.

#
canonicalize_list

fn canonicalize_list(input : String) -> Result[SerializedField, SfError]

Canonicalizes a List field value.

#
combine_field_lines

fn combine_field_lines(lines : Array[String]) -> String

Combines field lines with ", ", matching the combining convention the httpwg structured-field-tests suite assumes. Empty lines contribute empty members (which fail to parse, per the RFC's strict behavior).

#
decode_base64_lenient

fn decode_base64_lenient(content : Bytes, base_offset : Int) -> Result[Bytes, SfError]

Decodes base64 content with missing-padding and non-zero-pad-bit tolerance. The content must already have been alphabet-validated by the caller. base_offset is the byte offset of content inside the original input, used for error reporting.

#
expected_matches

fn expected_matches(actual : ParsedValue, expected : ExpectedField, header_type : String) -> Bool

Compares a parsed field against its expected abstract structure.

#
hex_value

fn hex_value(b : Byte) -> Int

Hex digit value, or -1 if b is not a hex digit.

#
httpwg_case_count

let httpwg_case_count : Int

#
httpwg_cases

let httpwg_cases : Array[HttpwgCase]

Marks the start of a fresh text segment.

#
httpwg_cases_part_0

let httpwg_cases_part_0 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_1

let httpwg_cases_part_1 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_10

let httpwg_cases_part_10 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_11

let httpwg_cases_part_11 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_12

let httpwg_cases_part_12 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_13

let httpwg_cases_part_13 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_14

let httpwg_cases_part_14 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_15

let httpwg_cases_part_15 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_16

let httpwg_cases_part_16 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_17

let httpwg_cases_part_17 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_18

let httpwg_cases_part_18 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_19

let httpwg_cases_part_19 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_2

let httpwg_cases_part_2 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_20

let httpwg_cases_part_20 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_21

let httpwg_cases_part_21 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_22

let httpwg_cases_part_22 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_23

let httpwg_cases_part_23 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_24

let httpwg_cases_part_24 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_25

let httpwg_cases_part_25 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_26

let httpwg_cases_part_26 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_27

let httpwg_cases_part_27 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_28

let httpwg_cases_part_28 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_29

let httpwg_cases_part_29 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_3

let httpwg_cases_part_3 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_30

let httpwg_cases_part_30 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_31

let httpwg_cases_part_31 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_32

let httpwg_cases_part_32 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_33

let httpwg_cases_part_33 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_34

let httpwg_cases_part_34 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_35

let httpwg_cases_part_35 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_36

let httpwg_cases_part_36 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_37

let httpwg_cases_part_37 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_38

let httpwg_cases_part_38 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_39

let httpwg_cases_part_39 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_4

let httpwg_cases_part_4 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_40

let httpwg_cases_part_40 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_41

let httpwg_cases_part_41 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_42

let httpwg_cases_part_42 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_43

let httpwg_cases_part_43 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_44

let httpwg_cases_part_44 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_45

let httpwg_cases_part_45 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_46

let httpwg_cases_part_46 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_47

let httpwg_cases_part_47 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_48

let httpwg_cases_part_48 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_49

let httpwg_cases_part_49 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_5

let httpwg_cases_part_5 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_50

let httpwg_cases_part_50 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_51

let httpwg_cases_part_51 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_52

let httpwg_cases_part_52 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_53

let httpwg_cases_part_53 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_54

let httpwg_cases_part_54 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_55

let httpwg_cases_part_55 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_56

let httpwg_cases_part_56 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_57

let httpwg_cases_part_57 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_58

let httpwg_cases_part_58 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_59

let httpwg_cases_part_59 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_6

let httpwg_cases_part_6 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_60

let httpwg_cases_part_60 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_61

let httpwg_cases_part_61 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_62

let httpwg_cases_part_62 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_63

let httpwg_cases_part_63 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_64

let httpwg_cases_part_64 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_65

let httpwg_cases_part_65 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_66

let httpwg_cases_part_66 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_67

let httpwg_cases_part_67 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_68

let httpwg_cases_part_68 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_69

let httpwg_cases_part_69 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_7

let httpwg_cases_part_7 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_70

let httpwg_cases_part_70 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_71

let httpwg_cases_part_71 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_72

let httpwg_cases_part_72 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_73

let httpwg_cases_part_73 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_74

let httpwg_cases_part_74 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_75

let httpwg_cases_part_75 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_76

let httpwg_cases_part_76 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_77

let httpwg_cases_part_77 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_78

let httpwg_cases_part_78 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_79

let httpwg_cases_part_79 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_8

let httpwg_cases_part_8 : Array[HttpwgCase]

Begins a new text segment.

#
httpwg_cases_part_9

let httpwg_cases_part_9 : Array[HttpwgCase]

Begins a new text segment.

#
is_alpha

fn is_alpha(b : Byte) -> Bool

ALPHA: %x41-5A / %x61-7A.

#
is_base64_char

fn is_base64_char(b : Byte) -> Bool

Characters allowed in the base64 content of a Byte Sequence: ALPHA / DIGIT / "+" / "/" / "=".

#
is_digit

fn is_digit(b : Byte) -> Bool

DIGIT: %x30-39.

#
is_h_tab

fn is_h_tab(b : Byte) -> Bool

Horizontal tab (0x09), allowed only as part of OWS in List/Dictionary.

#
is_hex_digit

fn is_hex_digit(b : Byte) -> Bool

Hexadecimal digit (0-9, a-f, A-F).

#
is_key_char

fn is_key_char(b : Byte) -> Bool

Subsequent characters of a key: lcalpha / DIGIT / "_" / "-" / "." / "*".

#
is_key_start

fn is_key_start(b : Byte) -> Bool

First character of a key: lcalpha or *.

#
is_lower_alpha

fn is_lower_alpha(b : Byte) -> Bool

lcalpha: %x61-7A.

#
is_lower_hex

fn is_lower_hex(b : Byte) -> Bool

Percent-encoding hex digits in Display Strings must be 0-9 or lowercase a-f (RFC 9651 §4.2.10 step 4.3.2).

#
is_ows

fn is_ows(b : Byte) -> Bool

OWS from RFC 9110: SP / HTAB.

#
is_sp

fn is_sp(b : Byte) -> Bool

Space (0x20), the only whitespace permitted by the Item grammar.

#
is_tchar

fn is_tchar(b : Byte) -> Bool

tchar from RFC 9110: ! / # / $ / % / & / ' / * / + / - / . / ^ / _ / ` / | / ~ / DIGIT / ALPHA.

#
is_token_char

fn is_token_char(b : Byte) -> Bool

A token character: tchar / ":" / "/".

#
is_visible_ascii

fn is_visible_ascii(b : Byte) -> Bool

Visible ASCII for String / Display String content: %x20-7E.

#
parse_bare_item_cursor

fn parse_bare_item_cursor(cursor : Cursor, limits : ParseLimits) -> Result[BareItem, SfError]

Parses a bare item, dispatching on the first byte.

#
parse_boolean_cursor

fn parse_boolean_cursor(cursor : Cursor) -> Result[Bool, SfError]

Parses a Boolean (RFC 9651 §4.2.8): exactly ?0 or ?1.

#
parse_byte_sequence_cursor

fn parse_byte_sequence_cursor(cursor : Cursor, limits : ParseLimits) -> Result[Bytes, SfError]

Parses a Byte Sequence (RFC 9651 §4.2.7), delimited by colons and base64-encoded. Padding is synthesized when missing, matching the RFC's "SHOULD NOT fail" guidance for both missing padding and non-zero pad bits.

#
parse_date_cursor

fn parse_date_cursor(cursor : Cursor, limits : ParseLimits) -> Result[Int64, SfError]

Parses a Date (RFC 9651 §4.2.9): @ followed by an Integer. The value is kept as a UTC seconds delta; no timezone conversion is performed.

#
parse_dictionary

fn parse_dictionary(input : String) -> Result[SfDictionary, SfError]

Parses input as a Dictionary field value.

#
parse_dictionary_bytes

fn parse_dictionary_bytes(input : Bytes) -> Result[SfDictionary, SfError]

Parses input as a Dictionary field value, in UTF-8 bytes.

#
parse_dictionary_cursor

fn parse_dictionary_cursor(cursor : Cursor, limits : ParseLimits) -> Result[SfDictionary, SfError]

Parses a Dictionary.

#
parse_dictionary_lines

fn parse_dictionary_lines(lines : Array[String]) -> Result[SfDictionary, SfError]

Parses a Dictionary field that may be spread across multiple lines.

#
parse_dictionary_with_limits

fn parse_dictionary_with_limits(input : String, limits : ParseLimits) -> Result[SfDictionary, SfError]

Parses input as a Dictionary field value with custom limits.

#
parse_display_string_cursor

fn parse_display_string_cursor(cursor : Cursor, limits : ParseLimits) -> Result[String, SfError]

Parses a Display String (RFC 9651 §4.2.10): %"..." with percent-encoded UTF-8 bytes. Percent hex digits must be lowercase.

#
parse_field_bytes

fn[T] parse_field_bytes(input : Bytes, limits : ParseLimits, parse_fn : (Cursor, ParseLimits) -> Result[T, SfError]) -> Result[T, SfError]

Runs the top-level field parsing algorithm (RFC 9651 §4.2): discard leading SP, parse by the given function, discard trailing SP, and require that nothing remains. This enforces the field-value boundary and produces [TrailingInput] errors for stray characters.

#
parse_inner_list_cursor

fn parse_inner_list_cursor(cursor : Cursor, limits : ParseLimits) -> Result[InnerList, SfError]

Parsing of Inner Lists (RFC 9651 §4.2.1.2).

An Inner List is "(" SP *[item *(SP item)] ")" followed by its own parameters. Items are separated by one or more spaces.

#
parse_integer_or_decimal_cursor

fn parse_integer_or_decimal_cursor(cursor : Cursor, _limits : ParseLimits) -> Result[BareItem, SfError]

Parses an Integer or Decimal (RFC 9651 §4.2.4). Returns an [BareItem] carrying the parsed value.

#
parse_item

fn parse_item(input : String) -> Result[Item, SfError]

Parses input as an Item field value.

#
parse_item_bytes

fn parse_item_bytes(input : Bytes) -> Result[Item, SfError]

Parses input as an Item field value, in UTF-8 bytes. Avoids a redundant string-to-bytes conversion when the input is already bytes.

#
parse_item_cursor

fn parse_item_cursor(cursor : Cursor, limits : ParseLimits) -> Result[Item, SfError]

Parses an Item: a bare item followed by its parameters.

#
parse_item_lines

fn parse_item_lines(lines : Array[String]) -> Result[Item, SfError]

Parses an Item field that may be spread across multiple lines.

#
parse_item_or_inner_list_cursor

fn parse_item_or_inner_list_cursor(cursor : Cursor, limits : ParseLimits) -> Result[ListMember, SfError]

Parses an Item or Inner List (RFC 9651 §4.2.1.1).

#
parse_item_with_limits

fn parse_item_with_limits(input : String, limits : ParseLimits) -> Result[Item, SfError]

Parses input as an Item field value with custom limits.

#
parse_key

fn parse_key(cursor : Cursor) -> Result[String, SfError]

Parses a Structured Fields key (RFC 9651 §4.2.3.3).

Grammar: key = ( lcalpha / "*" ) *( lcalpha / DIGIT / "_" / "-" / "." / "*" ).

#
parse_list

fn parse_list(input : String) -> Result[SfList, SfError]

Parses input as a List field value.

#
parse_list_bytes

fn parse_list_bytes(input : Bytes) -> Result[SfList, SfError]

Parses input as a List field value, in UTF-8 bytes.

#
parse_list_cursor

fn parse_list_cursor(cursor : Cursor, limits : ParseLimits) -> Result[SfList, SfError]

Parses a List: comma-separated Items and Inner Lists.

#
parse_list_lines

fn parse_list_lines(lines : Array[String]) -> Result[SfList, SfError]

Parses a List field that may be spread across multiple lines.

#
parse_list_with_limits

fn parse_list_with_limits(input : String, limits : ParseLimits) -> Result[SfList, SfError]

Parses input as a List field value with custom limits.

#
parse_parameters_cursor

fn parse_parameters_cursor(cursor : Cursor, limits : ParseLimits) -> Result[Parameters, SfError]

Parsing of Parameters (RFC 9651 §4.2.3.2).

Each parameter starts with ;, optionally followed by SP, a key, and an optional =value. A parameter with no explicit value is Boolean true. Duplicate keys collapse to their last occurrence.

#
parse_string_cursor

fn parse_string_cursor(cursor : Cursor, limits : ParseLimits) -> Result[String, SfError]

Parses a quoted String (RFC 9651 §4.2.5). Only printable ASCII and the two permitted escapes (\" and \\) are accepted.

#
parse_token_cursor

fn parse_token_cursor(cursor : Cursor, limits : ParseLimits) -> Result[String, SfError]

Parses a Token (RFC 9651 §4.2.6). The first character must be ALPHA or *; subsequent characters are tchar / : / /.

#
run_conformance

fn run_conformance() -> ConformanceStats

Runs every imported httpwg vector and returns the statistics.

#
serialize_bare_item

fn serialize_bare_item(buf :
Buffer
, bare : BareItem) -> Result[Unit, SfError]

Serializes a bare item, dispatching on its variant.

#
serialize_boolean

fn serialize_boolean(buf :
Buffer
, v : Bool) -> Result[Unit, SfError]

Serializes a Boolean (RFC 9651 §4.1.9).

#
serialize_byte_sequence

fn serialize_byte_sequence(buf :
Buffer
, b : Bytes) -> Result[Unit, SfError]

Serializes a Byte Sequence (RFC 9651 §4.1.8) using canonical padded base64.

#
serialize_date

fn serialize_date(buf :
Buffer
, v : Int64) -> Result[Unit, SfError]

Serializes a Date (RFC 9651 §4.1.10): @ followed by an Integer. The value is a UTC seconds delta; no timezone conversion is performed.

#
serialize_dictionary

fn serialize_dictionary(value : SfDictionary) -> Result[SerializedField, SfError]

Serializes a Dictionary. An empty Dictionary is represented by omitting the field entirely, so the result is [Omit] rather than an empty string.

#
serialize_dictionary_impl

fn serialize_dictionary_impl(buf :
Buffer
, dict : SfDictionary) -> Result[Unit, SfError]

Serializes a Dictionary into buf. Empty Dictionaries are not written here; the top-level [serialize_dictionary] reports [Omit] for them.

#
serialize_display_string

fn serialize_display_string(buf :
Buffer
, s : String) -> Result[Unit, SfError]

Serializes a Display String (RFC 9651 §4.1.11): %"..." with percent-encoded UTF-8 bytes. Percent hex digits are lowercase.

#
serialize_inner_list

fn serialize_inner_list(buf :
Buffer
, inner_list : InnerList) -> Result[Unit, SfError]

Serializes an Inner List into buf.

#
serialize_integer

fn serialize_integer(buf :
Buffer
, v : Int64) -> Result[Unit, SfError]

Serializes an Integer (RFC 9651 §4.1.4), validating the fifteen-digit range.

#
serialize_item

fn serialize_item(value : Item) -> Result[String, SfError]

Serializes an Item to its ASCII wire form.

#
serialize_item_impl

fn serialize_item_impl(buf :
Buffer
, item : Item) -> Result[Unit, SfError]

Serializes an Item into buf.

#
serialize_key

fn serialize_key(buf :
Buffer
, key : String) -> Result[Unit, SfError]

Serializes a key (RFC 9651 §4.1.1.3). The key must start with lcalpha or * and otherwise contain only key characters.

#
serialize_list

fn serialize_list(value : SfList) -> Result[SerializedField, SfError]

Serializes a List. An empty List is represented by omitting the field entirely, so the result is [Omit] rather than an empty string.

#
serialize_list_impl

fn serialize_list_impl(buf :
Buffer
, list : SfList) -> Result[Unit, SfError]

Serializes a List into buf. Empty Lists are not written here; the top-level [serialize_list] reports [Omit] for them.

#
serialize_parameters

fn serialize_parameters(buf :
Buffer
, parameters : Parameters) -> Result[Unit, SfError]

Serializes Parameters (RFC 9651 §4.1.1.2). A Boolean true parameter omits its value.

#
serialize_string

fn serialize_string(buf :
Buffer
, s : String) -> Result[Unit, SfError]

Serializes a String (RFC 9651 §4.1.6), escaping " and \.

#
serialize_token

fn serialize_token(buf :
Buffer
, s : String) -> Result[Unit, SfError]

Serializes a Token (RFC 9651 §4.1.7).

#
write_hex_byte

fn write_hex_byte(buf :
Buffer
, b : Byte) -> Unit

Writes a byte as two lowercase hexadecimal digits (RFC 9651 §4.1.11 step 4.1.3).