frame-codec — the TLV wire codec (L2/L3)¶
In one paragraph
The codec turns wire bytes into a borrowed tlv_t tree and back. A TLV is a 4- or
6-byte header (type, an opt bitfield, a length) + payload + an optional trailer
(timestamp, CRC). decode never copies payloads — they are std::spans into
the input buffer; encode serializes a tlv_t and recomputes the CRC. The
bit-level walkthrough shows every bit.
Decode and encode¶
decode(bytes) → std::expected<tlv_t, err_t> parses exactly one TLV that fills the
input: it reads the header, rejects reserved bits and bad structure, verifies the
trailer CRC, and — when opt.PL=1 (payload-is-structured) — walks child TLVs
iteratively, never recursively. The result borrows the input, so holding it
requires keeping the bytes alive (that is what views provide).
encode(tlv) does the reverse, recomputing the CRC over the body when opt.CR is
set. It does not take the length width from the model verbatim: a body over
0xFFFF widens to the u32 LL form whatever tlv.opt.ll says, because encode
emits through emit_tlv (below) — the one home of the length-width policy. A
programmatically built tree therefore cannot serialize a length truncated to
size & 0xFFFF; bodies at or under 0xFFFF are unchanged and opt.ll is never
cleared. Decode failure is one of FRAME_TRUNCATED, FRAME_INVALID, FRAME_CRC_FAIL
or TLV_NESTING_TOO_DEEP — the RFC-0002 registry codes, not a decode-only error
vocabulary (core/include/libtracer/frame.hpp:26-31).
flowchart TD
H["read header:<br/>type · opt · length"] --> V{"bounds ok?<br/>reserved bits zero?"}
V -->|no| E["err_t"]
V -->|yes| P{"opt.PL?"}
P -->|"1 — structured"| C["push children region<br/>onto the open-node stack"]
P -->|"0 — opaque"| O["payload = span into input"]
C --> T["verify trailer CRC"]
O --> T
T --> N{"more bytes<br/>in region?"}
N -->|yes| H
N -->|no| D["tlv_t tree (borrowed)"]
The opt byte is the protocol’s compactness lever: six 1-bit flags select
structure, trailer contents, and field widths, so the common frame is just 4
bytes of header. Bits 7 and 0 are reserved-MUST-be-zero; a set reserved bit makes
the frame invalid (opt_t::kReservedMask, core/include/libtracer/tlv.hpp).
Trailer CRCs¶
Trailer CRCs are CRC-32C (Castagnoli, reflected poly 0x82F63B78, the default)
or CRC-16-CCITT (FALSE) (poly 0x1021, init 0xFFFF, no final xor) when
opt.CW=1. Both tables are constexpr and built at compile time — crc32c_table
and crc16_table, core/include/libtracer/crc.hpp:38,51 — so a constant-evaluated
CRC needs no runtime table build.
Compile-time tables are not the whole runtime story. CRC-32C dispatches once, on
first use, to the SSE4.2 _mm_crc32_* or ARMv8 __crc32c* instruction where the
CPU carries it, and folds through a portable slice-by-8 table otherwise; all three
paths — hardware, slice-by-8, byte-at-a-time — produce byte-identical checksums,
which is what lets frozen test vectors hold across targets
(crc32c_update_runtime, crc.hpp:168-180). The intrinsics are confined to a
target-attributed function so the translation unit stays runnable on a CPU
without the extension. The slice-by-8 tables are 8 × 256 × u32 = 8 KiB of rodata
that a hardware-CRC CPU never touches (crc32c_slice_tables, crc.hpp:71-83) —
the one footprint line a constrained target should know about here.
crc32c and crc16_ccitt each take one span or two, and a crc32c_state /
crc16_ccitt_state accumulator takes any number of chunks: a CRC over a
payload-plus-trailer region, or over a rope crossing link boundaries, is computed
without concatenating the pieces into a fresh buffer. Trailer CRC placement is
ADR-0004, CRC in optional trailer.
Frame shape¶
byte: 0 1 2 3 4 … (4+len-1) … trailer …
┌──────┬────────┬───────────────┬───────────────────┬─────────────────┐
│ type │ opt │ length (u16) │ payload │ [timestamp][crc]│
└──────┴────────┴───────────────┴───────────────────┴─────────────────┘
│ (u32 if LL=1) opaque bytes, OR TS? then CR?
│ concatenated child
│ TLVs when PL=1
opt bits (MSB→LSB): R · PL · TS · CR · LL · CW · TF · R
(bits 7 and 0 are reserved-must-be-zero)
Nesting depth¶
Nesting depth is bounded by the receiver’s decode resources, never by a constant. No depth constant exists in the codec, and an implementation that hardcodes one is not implementing the rule (RFC-0006, resource-bounded nesting depth).
Both decoders share one structural descent, grammar::walk
(core/include/libtracer/grammar.hpp, ADR-0048, one wire grammar).
Recursion is forbidden there: a malicious deep frame must not overflow a small MCU
call stack, so the walk keeps one open-node record per open level in a
walk_stack_t. That stack starts in a caller-supplied inline span and, once those
slots are used, relocates into geometrically grown blocks drawn from a spill
source. The inline span is a tuning knob, not a limit — overflowing it changes
cost, not behaviour (grammar.hpp:363-369). Exhausting the spill source rejects
the frame with TLV_NESTING_TOO_DEEP, which means exactly “exceeds this receiver’s
decode resources” (grammar.hpp:461-465).
The two decoders differ only in what they spill to, and therefore in what bounds them:
decoder |
inline slots |
spill source |
the depth bound is |
|---|---|---|---|
|
8 ( |
the nothrow heap source ( |
the heap — an owning-tree decode allocates there regardless |
|
8 ( |
the caller’s |
whatever resource the caller injected |
The 8 is the typical FWD nesting (three to four levels) with headroom, not a
ceiling: the arena test decodes a frame nested 100 deep (core/tests/tlv_arena_test.cpp:324).
A receiver that wants a hard bound gets one by injecting a small source: a
stack-buffer mem::bump_source_t makes that buffer the whole decode budget
(mem_source.hpp), and exhaustion is then a returned err_t rather than an
allocation failure.
Interface¶
enum class type_t : std::uint8_t { VALUE=0x01, NAME=0x02, /*…*/ STATUS=0x09, ROUTER=0x0D };
struct opt_t { // the 6 option bits
bool pl, ts, cr, ll, cw, tf;
static constexpr std::uint8_t kReservedMask = 0b1000'0001;
static constexpr bool reserved_set(std::uint8_t); // bit 7 or 0 set ⇒ invalid
static constexpr opt_t decode(std::uint8_t);
constexpr std::uint8_t encode() const;
constexpr opt_t without_trailer() const; // clears TS/CR/CW/TF
};
struct tlv_t {
type_t type; opt_t opt;
std::span<const std::byte> payload; // opaque TLVs (borrowed)
std::vector<tlv_t> children; // structured TLVs (opt.PL=1)
std::optional<trailer_t> trailer; // {timestamp_t?, crc_t?}
};
std::expected<tlv_t, err_t> decode(std::span<const std::byte>); // borrowed
std::expected<tlv_t, err_t> decode(const view::view_t&); // the L1→L2 cast
std::vector<std::byte> encode(const tlv_t&); // recomputes CRC
std::vector<std::byte> path_key(const tlv_t& path); // canonical PATH key
bool equal(const tlv_t&, const tlv_t&); // spans by content
// tlv_emit.hpp — bytes without a model object
void emit_header(std::vector<std::byte>&, type_t, opt_t, std::size_t body_len);
void emit_tlv (std::vector<std::byte>&, type_t, opt_t, std::span<const std::byte> body);
void emit_name (std::vector<std::byte>&, std::span<const std::byte>);
void emit_name (std::vector<std::byte>&, std::string_view);
// tlv_arena.hpp — the terminus decoder
std::expected<tlv_arena_t, err_t> decode_into(std::span<const std::byte>,
mem::block_source_t&);
namespace tr::crc { constexpr std::uint32_t crc32c(...); // 1 or 2 spans
constexpr std::uint16_t crc16_ccitt(...);
struct crc32c_state; struct crc16_ccitt_state; } // n chunks
decode(const view::view_t&) is the L1→L2 cast — “a TLV is a cast from a view.” It
lives at L2 because it produces a tlv_t, and consumes an L1 view; the
returned tree borrows the view’s bytes, so the view and its segment must outlive
it.
Byte emission without a model object¶
tlv_emit.hpp appends one TLV — <type> <opt> <length> <body> — straight into a
std::vector<std::byte>, with no intermediate tlv_t. It is the one
representation of the header byte layout (ADR-0048 §3): encode and every
structural byte-builder in the tree share it instead of each hand-rolling
type/opt/little-endian length. emit_tlv is also the one home of the
length-width policy — encode goes through it rather than calling
emit_header itself, so there is no second widen rule to drift (#924).
function |
what it appends |
|---|---|
|
the header alone; length is |
|
header + body, auto-setting |
|
a |
|
one packed PATH segment record — |
|
ONE path element, whatever its kind — the byte-exact inverse of |
|
a |
Building a PATH is emit_path_segment per segment into one buffer
(packed_path.hpp); that concatenation of packed [u8 len][utf8] records is exactly
the canonical PATH key path_key produces from a decoded PATH, and — since
RFC-0018
gave an address exactly one spelling — the body a resolver can key on in place,
unconditionally, where a flag on the arena node used to say whether it could. The
PATH’s own header carries opt_t{}: a packed body is not a child run, so PL stays
clear. Building an FWD request is emit_tlv for the outer frame over a body built the
same way. Pass opt_t{.pl = true} for a structured payload.
These live in tr::wire (L2/L3) because they produce wire bytes from wire types;
the layer-free little-endian byte helper they build on stays in tr::detail
(byteorder.hpp). For decoding, and for emitting a full tlv_t value with
payload, children and trailers, frame.hpp’s decode/encode are the entry
points.
The BATCH record — folding a flush into one written value¶
batch.hpp is the one canonical spelling of
RFC-0025’s
batch convention: BATCH is type_t::BATCH (0x80), the single code assigned inside the
user range, and a batch is otherwise an ordinary structured TLV — zero new grammar, so
every conforming decoder above already decodes one and the graph never interprets it.
function |
what it does |
|---|---|
|
FOLDS N already-encoded sample frames into one record: a |
|
the whole size function, so a caller sizes the buffer before filling it |
|
the reader half: a |
|
the SAMPLE clock of frame |
Two properties are worth stating where a reader will look for them.
A uniform stream spends 0 bytes per sample on time. dt_ns is the nominal sample period
the stream’s descriptor declares — a SETTINGS LKV beside the data vertex, negotiated once
and never repeated per batch — so nothing per-sample is transmitted and a 4-byte sample costs
4 bytes. That is also why dt_ns is a parameter of read_batch and not something the record
is asked for: whether the child after the TIME is an offset array or the first sample frame
is a fact about the descriptor. A non-uniform stream (dt_ns == 0) carries one packed i32
run in a single child — 4 bytes per sample, contiguous, no per-child TLV header, no anchor
walk.
A batch carries no trailer. Wire/TX time is opt.TS on the outermost frame only and
is always TF=0; a written value is an inner TLV. Sample time is the payload TIME child;
playout time is never transmitted at all (the three-clock model of
01-data-format.md). read_batch declines bytes that do not
spell the convention — a reader’s refusal, never the codec’s: the same bytes still decode,
still forward and still round-trip.
The terminus arena decoder¶
Alongside the owning tlv_t model, the codec ships a second decoder for the FWD
terminus: wire::decode_into(span, tr::mem::block_source_t&) → tlv_arena_t
(public header tlv_arena.hpp). It parses the same frames with the same
validation — bounds, reserved bits, type 0x00, the bound-path (0x14/0x15) body shape, trailer
CRC, trailing bytes ⇒ FRAME_INVALID — but the result is a flat, pre-order array of arena_tlv_t
span-nodes: {type, opt, wire (trailer-excluded), body, end}.
Every span borrows the input frame; every node is drawn from the injected
block_source_t.
The arena contract is structure only, never bytes (ADR-0041, terminus arena decode span contract). The decode holds structure — node types, option bits, subtree extents — and the payload bytes are never copied. A borrowed span may be read, copied once to its owner, or sub-viewed off a refcounted owner; it may never be stored as a borrowed span. The arena is a resolve-scoped view: read it, take the ownership copies, drop it.
Subtree navigation is index arithmetic rather than pointer chasing. end is one
past the last descendant, so node i’s children start at i + 1 and siblings walk
j = i + 1; while (j < node[i].end) { visit(j); j = node[j].end; }. An opaque
node’s end is its own index + 1.
decode_into does not replace decode. The owning tlv_t model remains the
general codec — it materializes, it outlives its input by copying, and it is what
encode round-trips; the arena form is a resolve-scoped view over an inbound
frame, chosen where a decode must not touch the heap. Both are tr::wire, both
share grammar::walk, and an equivalence test decodes every conformance vector
through each and requires them to agree (core/tests/tlv_arena_test.cpp).
Every draw decode_into makes is nothrow and guarded, because it runs on the wire
RX path behind no ACL and a peer chooses both the nesting depth and the node count.
Exhaustion answers TLV_NESTING_TOO_DEEP, never std::bad_alloc — which on a
-fno-exceptions node is a link-wrapped abort()
(core/include/libtracer/tlv_arena.hpp:130-134).
Consequences¶
Self-describing and compact — one 4-byte header covers the common case; the
optbits opt into width and trailer only when needed.Decode is a cast, not a copy — payloads are spans; structured TLVs are sub-spans of the same buffer. Pairs with views for zero-copy.
Bounded and safe — fixed-width length (no varint ambiguity), an iterative parse with no recursion and no depth constant, CRC verified before the bytes are trusted.
The receiver sets its own ceiling — depth and node count are bounded by the resource the receiver injects, so the same codec serves a heap-backed host and a stack-slab MCU terminus without a build flag.
Pitfalls¶
A decoded tree outliving its buffer.
tlv_t::payloadand every child payload point into the decode input. Freeing or reusing that buffer while the tree is live is a dangling read; aview_t— and thus its segment — is what keeps the bytes alive.Storing an arena span.
arena_tlv_t::wireand::bodypoint into an inbound frame that the transport recycles. An implementation that stores one instead of copying it once to an owner reads another peer’s later frame.Copying
wirewithout clearing the trailer bits.wireexcludes the trailer by construction, so a whole-TLV copy that keeps the sourceoptbyte advertises a trailer it does not carry.opt_t::without_trailer()is the typed fix.Hardcoding a nesting-depth limit. A decoder that rejects at a fixed depth rejects frames a conforming sender may emit, and reports a resource condition it does not have. The limit is the decode resource; the inline slot count is a tuning knob.
Ignoring the reserved bits. Bits 7 and 0 of
optmust be zero. A decoder that masks them off accepts frames no conforming sender emits and spends the protocol’s extension point.Treating trailing bytes as a second frame.
decodeanddecode_intoeach require the input to be exactly one TLV; trailing bytes areFRAME_INVALID. Splitting a stream into frames is the transport’s job (length_prefix_framer.hpp).
API reference¶
Headers: frame.hpp, tlv.hpp, tlv_emit.hpp, tlv_arena.hpp, batch.hpp,
path_ref.hpp, crc.hpp — all under core/include/libtracer/.
tr::wire::opt_t — the option bits carried in every header — is documented once, on
wire format bits, alongside the bit layout it names.
-
struct tlv_t¶
A decoded TLV — the materialized, eager representation of one wire frame node.
For opaque TLVs (
opt.pl == 0) payload holds the bytes and children is empty; for structured TLVs (opt.pl == 1) children holds the parsed sub-TLVs and payload is empty. payload (and child payloads) BORROW the input buffer.Public Members
-
std::span<const std::byte> payload = {}¶
Opaque bytes (borrowed); empty when structured.
-
std::span<const std::byte> payload = {}¶
-
struct trailer_t¶
A decoded TLV trailer: an optional timestamp_t and/or crc_t.
Public Functions
Public Members
-
std::optional<timestamp_t> ts¶
The trailer timestamp, if present.
-
std::optional<timestamp_t> ts¶
-
std::expected<tlv_t, err_t> tr::wire::decode(std::span<const std::byte> input, mem::block_source_t &spill = mem::heap_source())¶
Decode exactly one TLV that fills
input.The structural descent’s open-node stack starts in inline slots sized for the typical FWD nesting and SPILLS to
spillfor deeper frames, so the RFC-0006 decode-resource bound is a property the caller injects rather than one this function fixes (#873, ADR-0079).decode(input, mem::null_source())is the spelling of “no spill at all”: a frame nested past the inline slots is refused withTLV_NESTING_TOO_DEEPinstead of growing (grammar::walk_stack_t).Note
spillbounds the WALK STACK only. The returned tree is an OWNINGtlv_t, whose child vectors allocate on the global heap by construction — this parameter does not make an owning decode allocation-free, and a caller that needs the whole decode on an injected store wants the terminus arena (wire::decode_into) instead.- Parameters:
input – The bytes to decode — must be exactly one TLV; trailing bytes ⇒
FrameInvalid.spill – The block source the walk stack spills into once its inline slots are full. Default: the process heap, i.e. today’s behaviour unchanged.
- Returns:
The decoded tlv_t (borrowing
input), or anerr_ton failure.
-
inline std::expected<tlv_t, err_t> tr::wire::decode(const view::view_t &v, mem::block_source_t &spill = mem::heap_source())¶
Decode exactly one TLV from a flat view (the L1↔L2 cast, zero-copy).
The L1↔L2 cast — “a TLV is a cast from a view.” It lives at L2 (it produces a tlv_t) and consumes an L1 view::view_t. The returned
tlv_tborrows the view’s bytes, so keep the view — and thus its segment — alive while using it.- Parameters:
v – The view whose bytes are exactly one TLV.
spill – Forwarded to the span overload — the walk stack’s spill source, defaulting to the process heap.
- Returns:
The decoded tlv_t (borrowing
v'sbytes), or anerr_ton failure.
-
std::vector<std::byte> tr::wire::encode(const tlv_t &tlv)¶
Encode a TLV to its wire bytes (recomputing the trailer CRC when
opt.cris set).The length width is not taken from
tlvverbatim: a body larger than 0xFFFF widens to the u32LLform regardless oftlv.opt.ll, the same rulewire::emit_tlvapplies (#924), so a programmatically built tree can no longer serialize a length truncated tosize & 0xFFFF. A body at or under 0xFFFF is emitted unchanged;tlv.opt.llis never cleared. A body over 0xFFFFFFFF still truncates modulo 2^32 — the grammar has no wider length form, so that residual is a wire-format limit rather than somethingencodecan express.Encoding is SYMMETRIC with
decode: the grammar’s one per-type structural rule — aPATH_REFbody is a fixed-stride 8-byte record array, soopt.PLandopt.LLare both forbidden and the length is bounded (RFC-0024 §4.2/§4.3,wire::path_ref_body_valid) — is applied here too, so this codec cannot mint a frame it would itself reject (#886). A refused TLV anywhere in the tree refuses the whole tree rather than silently dropping a component.The trailer timestamp is LOUD, not defaulted (#1109): a TLV whose
opt.tsis set with notrailer->tsvalue — or whose trailer value’srelativeflag contradictsopt.tf— is refused (empty vector), never emitted with a silently-zero stamp.stamp_tssets the bit and the value together, so a stamped TLV cannot reach this refusal.- Parameters:
tlv – The TLV tree to serialize.
- Returns:
The encoded frame bytes, or an EMPTY vector when
tlv(or any descendant) is an ill-formedPATH_REFor claims a timestamp it does not carry. Empty is unambiguous: a serialized TLV always carries at least its 4-byte header, so no well-formedtlvencodes to nothing.
-
inline void tr::wire::emit_tlv(std::vector<std::byte> &out, type_t type, opt_t opt, std::span<const std::byte> body)¶
Append one TLV:
<type> <opt> <length> <body>.Length is u16 LE, widening to u32 LE (the LL bit set) when
bodyexceeds 0xFFFF.optcarries the structural bits — passopt_t{.pl = true}for a structured (list) payload.The TRAILER bits of
optare cleared by construction (#1109): this emitter writes headerbody and nothing after, so an
opt.ts/opt.crpassed here used to mint a frame that CLAIMED a trailer it did not carry — which a receiver reads as truncation (the grammar counts the trailer intototal) or, worse, eats the last body bytes as a bogus stamp. A caller with a trailer to write uses’sframe.hppencode(values) or emits the header viaemit_headerand appendsemit_trailer_ts/ CRC bytes itself (byte builders).
The bound-path element codec (RFC-0024 §4) — the element’s layout and the purely STRUCTURAL rules the grammar enforces. What an element means — the bounds check into a host’s vertex map, the generation compare, the ACL re-check at the dereferenced vertex — is L4 routing and lives in fwd-router.md §the bound hop.
-
struct path_ref_element_t¶
One bound-path element: a node-scoped reference to one host’s own vertex.
Element 0 is the origin’s reference to the connection vertex for its first hop; element i is forwarder i’s reference to its connection vertex for hop i+1; the last element is the terminus host’s reference to the target vertex itself. Nothing in an element means anything anywhere but on the host that minted it — it is an address, never a capability (RFC-0024 §4.1, §6).
Public Functions
-
constexpr bool operator==(const path_ref_element_t&) const noexcept = default¶
Value equality over both fields.
-
constexpr bool operator==(const path_ref_element_t&) const noexcept = default¶
-
constexpr std::size_t tr::wire::path_ref_element_count(std::size_t body_len) noexcept¶
The element count a
PATH_REFbody ofbody_lenbytes carries (RFC-0024 §4.3).There is no count field on the wire: the count IS
length / 8. Only meaningful for a body path_ref_body_valid accepts.
-
constexpr bool tr::wire::path_ref_body_valid(bool pl, bool ll, std::size_t body_len) noexcept¶
The purely STRUCTURAL
PATH_REFrules of RFC-0024 §4.2/§4.3 — the grammar’s check.A
PATH_REFheader that fails this istr::frame::invalid, exactly as a set reserved bit is. All four clauses are shape, not meaning, which is what makes them a codec rule rather than a resolver one (contrastPATH’s child-type constraint, reference/05 §Pitfalls):**
opt.PLMUST be 0** — the body is a fixed-stride record array, not child TLVs; a genericPL = 1walker would read the first four body bytes as a TLV header and mis-frame the whole body.**
opt.LLMUST be 0** — the u32 length width buys nothing under a 2040-byte cap, so it is forbidden rather than merely unused.**
length % 8 == 0** — a body that is not a whole number of elements has no reading.**
length <= 2040** — the §4.3 element-count bound, in bytes.
-
constexpr path_ref_element_t tr::wire::path_ref_element_at(std::span<const std::byte> body, std::size_t i) noexcept¶
Read element
iout of aPATH_REFbody (little-endian, both fields).Note
Precondition:
(i + 1) * 8 <= body.size()— i.e.iis below path_ref_element_count of a body path_ref_body_valid accepted (debug-asserted). The bound is the caller’s to hold because it is free there and not here: a decode path has already had the whole body shape checked by the grammar, so re-deriving the count per element would price a division into every hop of a forward. A caller that cannot provei— anything walking a foreign frame’s elements — gates its loop on path_ref_element_count first; past the end this reads whatever follows the body, it does not fail.
-
constexpr void tr::wire::path_ref_store_element(std::span<std::byte> out, const path_ref_element_t &e) noexcept¶
Write one element into the 8 bytes at
out(little-endian, both fields).Note
Precondition:
out.size() >= 8(debug-asserted). The inverse of path_ref_element_at, and the same contract: the emitter sizes the buffer from the element count before the first store, so the check belongs there once rather than here per element.
-
inline void tr::wire::emit_name(std::vector<std::byte> &out, std::span<const std::byte> name)¶
Append a NAME TLV over opaque bytes — the PATH-segment / metadata-tag workhorse.
-
inline void tr::wire::emit_name(std::vector<std::byte> &out, std::string_view name)¶
Append a NAME TLV over a text segment (no temporary buffer).
-
struct arena_tlv_t¶
One decoded TLV node in a tlv_arena_t (structure only, zero-copy).
Both spans borrow the
decode_intoinput buffer, which must outlive the arena.wiredeliberately excludes the trailer so a whole-TLV copy made from it (a stored WRITE value, the reply route bytes) is trailer-less at rest by construction (ADR-0041 §4) — a copier must also clear the trailer bits from the copiedoptbyte to keep the copy self-consistent.Public Members
-
std::span<const std::byte> wire = {}¶
Header + body bytes, trailer excluded — the whole-TLV-copy span.
-
std::span<const std::byte> body = {}¶
The body: payload bytes (opaque) or the children region (
opt.pl).
-
std::uint32_t end = 0¶
One past the last descendant’s index (pre-order subtree encoding).
Children of node
istart ati + 1; iterate siblings withj = i + 1; while (j < node[i].end) { visit(j); j = node[j].end; }. An opaque node’sendis its own index + 1.
-
std::span<const std::byte> wire = {}¶
-
std::expected<tlv_arena_t, err_t> tr::wire::decode_into(std::span<const std::byte> input, mem::block_source_t &src)¶
Decode exactly one TLV filling
inputinto a flat arena drawn fromsrc.The terminus-side counterpart of
decode(ADR-0041 §1): identical validation (bounds, reserved bits, type 0x00, trailer CRC, trailing bytes ⇒ FRAME_INVALID), iterative (no recursion) with the node array, the sink’s open-node stack and the walk stack all drawn fromsrc— so the caller’s source IS the nesting-depth bound (RFC-0006; no depth constant exists) — but the result is a pre-order arena_tlv_t array of spans intoinputinstead of an owningtlv_ttree — zero heap whensrcis a stack-buffer mem::bump_source_t.inputandsrcmust outlive the arena.NOTHROW end to end (#588). This function is on the wire RX path and reachable behind no ACL — a peer picks the frame’s nesting depth and node count. Every draw from
srcis guarded: exhaustion answersTLV_NESTING_TOO_DEEP(“exceeds this receiver’s decode resources”), neverstd::bad_alloc, which on a-fno-exceptionsnode is the link-wrappedabort()stub.
-
class tlv_arena_t¶
A frame decoded as a flat pre-order node array over borrowed spans.
Produced by
decode_into; drawn from the injected memory resource, which must outlive the arena (as must the input buffer). A resolve-scoped object: read it, take the ADR-0041 ownership copies, and drop it — never store it.Public Functions
-
inline explicit tlv_arena_t(mem::block_source_t &src)¶
An empty arena drawing its nodes from
src.
-
inline const arena_tlv_t &root() const noexcept¶
The root node (index 0). Precondition: a successful decode (never empty).
-
inline const arena_tlv_t &operator[](std::size_t i) const noexcept¶
The node at pre-order index
i.
-
inline std::size_t size() const noexcept¶
Total node count (root + all descendants).
-
inline std::uint32_t next_sibling(std::uint32_t i) const noexcept¶
Index of node
i'snext sibling within its parent (compare against the parent’send).
-
inline std::optional<timestamp_t> root_trailer_ts() const noexcept¶
The ROOT node’s decoded trailer timestamp, if the frame carried one (#1109).
Captured at decode because the node spans deliberately EXCLUDE the trailer (the ADR-0041 §4 trailer-less-at-rest rule), so nothing reachable from a node can read the stamp back — and the reply echo needs exactly this value. Root-only on purpose: the echo reads the request’s OUTER stamp, and per-node capture would grow every
arena_tlv_ton the MCU terminus for a value only index 0 is ever asked for. A TF=1 root decodes here too (relative == true), and the CONSUMER decides what an anchorless relative stamp is worth — the echo declines it.
Public Static Functions
-
static inline std::uint32_t first_child(std::uint32_t i) noexcept¶
Index of node
i'sfirst child (valid ifffirst_child(i) < (*this)[i].end).
-
inline explicit tlv_arena_t(mem::block_source_t &src)¶
-
struct crc_t¶
A decoded trailer CRC: its width and the (zero-extended) checksum value.
Public Types
Public Functions
-
struct timestamp_t¶
A decoded trailer timestamp: absolute u64 ns or a relative i32 ns delta.
Public Functions
-
constexpr bool operator==(const timestamp_t&) const noexcept = default¶
Value equality over the relative flag and value.
-
constexpr bool operator==(const timestamp_t&) const noexcept = default¶
-
std::expected<void, err_t> tr::wire::check_frame(const view::rope_t &r)¶
The cheap INGRESS check (CONTEXT.md §Validation timing): top-level header + total-size anchor + the whole-frame trailer CRC, one linear scan.
Everything ingress is allowed to verify, and nothing more:
parse_headeron the root (bounds,type == 0x00reject, reserved-bit reject,LLwidth, trailer sizing, and — whenopt.CRis set — the trailer CRC, a LINEAR link-by-link scan that never needs the tree) plus thetotal == sizeanchor. No descent: a malformed child TLV surfaces its error where that level is CONSUMED (per-TLV verify-at-access, ADR-0053), never at ingress. The strict whole-tree walk remains available as the opt-invalidate_rope.- Parameters:
r – The reassembled frame. Every link MUST be HOST (view::rope_t::all_host) — a device link cannot be CPU-read to verify a CRC and is rejected with
FRAME_INVALID.- Returns:
{}when the root header, size anchor and (if present) root CRC hold; otherwise theerr_tthe grammar rejects with.
-
std::expected<void, err_t> tr::wire::validate_rope(const view::rope_t &r, mem::block_source_t &spill = mem::heap_source())¶
STRICTLY validate one whole TLV frame delivered as a scatter-gather rope (ADR-0048 §1) — the opt-in eager whole-tree walk.
Applies the exact
grammar::parse_headergrammar — bounds,type == 0x00reject, reserved-bit reject,LLwidth, trailer sizing, the two-region CRC, and trailing-bytes reject — over EVERY level of the rope’s links WITHOUT flattening. Iterative (no recursion) via the onegrammar::walk; nesting depth is bounded by the walk stack’s spill source (RFC-0006 — no depth constant), which isspill. NOT an ingress step (ingress ischeck_frame): this is the strict mode a verify-all-then-apply consumer or a differential test opts into (ADR-0053 §4).The sink models nothing, so the spill block is the ONLY allocation this call can make: validating over an injected
spillis allocation-free with respect to the process heap, andmem::null_source()turns it into a fixed 8-level bound that answersTLV_NESTING_TOO_DEEP(#873, ADR-0079).- Parameters:
r – The reassembled frame. Every link MUST be HOST (view::rope_t::all_host) — a device link cannot be CPU-read to verify a CRC and is rejected with
FRAME_INVALID.spill – The block source the walk stack spills into once its inline slots are full. Default: the process heap, i.e. today’s behaviour unchanged.
- Returns:
{}whenris exactly one valid frame; otherwise theerr_tthe grammar rejects with — identical todecode(flatten(r))’s error for the same bytes, PROVIDED both are given the same depth resources.
The lazy tier — tlv_view_t¶
A rope-delivered frame does not have to become an owning tree to be read. A
tlv_view_t is one TLV whose bytes stay in the rope: it holds the parsed header
facts plus a refcounted subrope, and nothing that is not accessed is ever
decoded. Children are materialized one header at a time by stepping
children_t; a payload handed onward stays the subrope it already is; and
materialize() into a tlv_t is the single, explicit copy point.
Validation is lazy in the same sense. Anchoring a view checks the root header and
the exact total, with the CRC walk deferred; child headers are grammar-checked as
they are stepped over; and integrity — the per-TLV CRC trailer — is checked by
whichever consumer accesses a TLV, through verify(). An endpoint whose members
form one transaction verifies all of them before mutating any state, so a
partially-applied frame is not a reachable outcome.
-
class tlv_view_t¶
One TLV whose bytes live in a rope — the lazy decode-side node (ADR-0053 §1).
Holds the (CRC-deferred) parsed header plus a refcounted subrope of the inbound frame; copying a
tlv_view_tbumps segment refcounts, never bytes. The view — and any child view or subrope taken from it — keeps exactly its own links’ segments alive (view::rope_t::subrope) and may outlive the transport read loop: this is the owning delivery tier, the scoped revision of the ADR-0041 §2 span-arena contract (which itself is untouched).tlv_tremains the eager encode-side / materialized representation; materialize is the one explicit copy from this tier into it.Public Functions
-
inline bool structured() const noexcept¶
True for a structured TLV (
opt.PL— body = children region).
-
inline std::size_t body_size() const noexcept¶
Body (payload / children region) length in bytes.
-
inline const view::rope_t &wire() const noexcept¶
This TLV’s full wire bytes (header + body + trailer) as a rope.
The forwarding primitive: a hop that routed on the PATH prefix hands this (or a body subrope) to the next transport as-is — zero decode, zero copy, the links refcount-alive across the hop (ADR-0053 §3).
-
inline view::rope_t body() const¶
The body region as a refcounted subrope (payload of an opaque TLV; the packed children region of a structured one).
Never validated, never copied — a structured payload delivered to a consumer that does not descend stays exactly these bytes (“up to the
consumer to deal with it”, ADR-0053 §1).
-
inline children_t children() const¶
Begin lazy child iteration (an empty range for an opaque TLV).
-
std::expected<void, err_t> verify() const¶
Check THIS TLV’s integrity — its CRC trailer — now (ADR-0053 §4).
The access-time integrity point: walks this TLV’s body ++ timestamp bytes link-by-link (no copy) against the stored trailer. A TLV with no CRC trailer verifies trivially. Covers nested children byte-for-byte (they are the body), so an endpoint applying a multi-member write as one transaction calls this once — verify-all-then-apply.
-
std::optional<timestamp_t> timestamp() const¶
The decoded timestamp trailer, if
opt.TS(a bounded stitched read).
-
std::expected<materialized_t, err_t> materialize(mem::mem_backend_t &backend = mem::heap_backend()) const¶
The single explicit copy point (ADR-0053 §1): flatten + eager decode.
Everything lazy access deferred is paid here, once, by the consumer that asked for it: one contiguous copy and the full grammar walk INCLUDING every CRC trailer — byte-identical to
decode(flatten(wire())).- Parameters:
backend – Where the flat segment is allocated.
- Return values:
err_t::FLOW_BACKPRESSURE –
backendcould not allocate the segment. A LOCAL, transient failure of this node — retrying the same frame may succeed. Until #917 it was reported asFRAME_INVALID, i.e. as a PERMANENT accusation that the peer sent a malformed frame.- Returns:
The flat copy + eager tree, or the grammar’s
err_t.
Public Static Functions
-
static std::expected<tlv_view_t, err_t> over(view::rope_t frame)¶
The ingress bounds anchor (ADR-0053 §4): adopt
frameas one lazy TLV.Parses the root header with the CRC walk deferred and requires
total == frame.total_length()— a handful of byte reads plus an O(links) size sum, no payload walk, no copy. With the root bounds anchored, every later child materialization is containment-checked against its parent’s region, so lazy access is memory-safe without ever touching bytes nobody reads.- Parameters:
frame – The reassembled inbound frame (the rope is adopted — refcounted links, no copy). Every link must be HOST (view::rope_t::all_host): rejected
FRAME_INVALIDotherwise, exactly asvalidate_rope.- Returns:
The root view, or the grammar’s
err_t(FRAME_TRUNCATED/FRAME_INVALID— including trailing bytes).
-
class children_t¶
Lazy forward iteration over a structured TLV’s children (ADR-0053 §1).
Each next parses exactly ONE child header (CRC deferred, so a skipped sibling’s payload is never walked) and yields that child as its own
tlv_view_tsubrope. A grammar error in a child surfaces here — to whoever is iterating — and only for the child it belongs to; siblings already yielded are unaffected (partial consumption, ADR-0053 §4).Public Functions
-
std::expected<std::optional<tlv_view_t>, err_t> next()¶
The next child,
std::nulloptwhen the region is exhausted, or the child’s grammarerr_t.After an error the iterator is poisoned (further calls return the same error): child boundaries beyond a malformed header are unknowable.
-
std::expected<std::optional<tlv_view_t>, err_t> next()¶
-
struct materialized_t¶
A materialized view: the flat copy plus the eager tree borrowing it.
rootborrowsflat’s segment bytes (stable across moves — the segment is refcounted heap memory), so keep the pair together, exactly likedecode(view_t)’s “keep the view alive” contract.
-
inline bool structured() const noexcept¶
Keys — key_view_t¶
A vertex-map key is the concatenated packed segment records of its path — each
[u8 len][utf8], RFC-0018
— so every ancestor / descendant / child relation is a byte operation and no
string form is ever materialized. key_view_t is the navigation over those bytes
and the single home of that walking.
Why a byte prefix means “ancestor”: records are self-delimiting and parsed
left-to-right, so a shared byte prefix parses identically in both keys and every
prefix boundary lands on a record boundary — a differing length byte breaks the
byte match one record earlier. /a (01 'a') is a prefix of /a/b
(01 'a' 01 'b'); /ab (02 'a' 'b') correctly is not. A strict byte-prefix of
a valid key is therefore exactly a strict ancestor of it. This is why an escape
record is refused in key context: a key must stay pure-string for the property to
be stated over its bytes at all.
-
class key_view_t¶
A read-only view over a canonical PATH-payload key (packed
[u8 len][bytes]records, RFC-0018), with the ancestor / descendant / segment navigation the graph dispatch and ACL-inheritance walks need. Cheap to copy (wraps a span); borrows the key bytes, which must outlive it.Public Functions
-
constexpr key_view_t() noexcept = default¶
An empty key view (the root).
-
inline explicit constexpr key_view_t(std::span<const std::byte> key) noexcept¶
View over the canonical key bytes
key(packed segment records).
-
inline std::size_t record_end(std::size_t at) const noexcept¶
One past the last byte of the well-framed segment record starting at byte offset
at— THE single locus of the[u8 len][bytes]framing decode this whole module is about (#888), and so also where the NEXT record starts.A bare offset, not a record_t, because this is what the per-record loops of the Composite descent (
graph_t::find_ptr) want, and they want it INLINE: returning the fuller record grew the decode past the inliner’s budget and put a call in that loop, which is a vertex-resolution cost this refactor must not introduce. Under the packed body the decode is one byte load and one add — RFC-0018’s whole latency case — where it was aparse_headeroption decode before.- Returns:
0 when the bytes at
atare ragged: no length byte, a length that runs past the key’s end, or alen == 0record — which in KEY context is the RFC-0018 §5.4 label escape and is rejected here (see the file header; the rule is enforced once and every walk inherits it). A well-framed record ends atat+ 2 or later, so 0 is unambiguous. Every walk here stops there — a malformed tail is never half-decoded.
-
inline std::optional<record_t> record_from(std::size_t at) const noexcept¶
The whole well-framed NAME record starting at byte offset
at— record_end with the bounds and the payload span worked out.- Return values:
std::nullopt – Exactly when record_end reports ragged.
-
inline constexpr std::span<const std::byte> bytes() const noexcept¶
The underlying key bytes.
-
inline constexpr bool empty() const noexcept¶
True at the root (no segments).
-
inline std::span<const std::byte> last_segment() const noexcept¶
The last segment’s payload — the vertex’s own name; empty at the root. Walks records to the end; stops early on a malformed length (a ragged record, or an illegal zero-length one — see the file header).
-
inline key_view_t parent() const noexcept¶
The parent key: this key with its last segment record dropped (empty at the root). The ADR-0020 inheritance walk derives ancestor keys by iterating this — the key is the concatenated segment records, so no string form is needed. Stops early on malformed framing, as last_segment does.
-
inline bool is_ancestor_of(key_view_t other) const noexcept¶
True iff this key is a strict ancestor of
other— a segment-record-boundary byte-prefix of it (sootheris a descendant).
-
inline std::optional<std::span<const std::byte>> child_record_under(key_view_t parent) const noexcept¶
If this key is a direct child of
parent— exactly one more well-framed segment record beyond it — return that trailing record (the child’s own canonical record encoding); otherwisestd::nullopt. A deeper descendant (more than one further record) yieldsnullopt, as does an illegal zero-length record (see the file header) — therest.size() <= 1guard below.
-
template<class Emit>
inline bool for_each_level(Emit &&emit) const¶ Invoke
emiton each ancestor-prefix level, shallowest-first (the last equals the whole key) — themkdir -pcreation order, ALLOCATING NOTHING.The storage-free form of split_levels, for a caller on a peer-reachable path where a scratch container would be a heap draw it does not own (#1139).
- Parameters:
emit – Called as
emit(key_view_t)and returningbool;falseSTOPS the walk, which this function then reports as failure. A caller that must not act on a malformed key therefore walks TWICE — once with atrue-returningemitto validate the framing, then once to act — because raggedness is only discovered at the last record.- Returns:
false if
emitstopped the walk, the record framing is ragged (records do not tile the key exactly), any record carries an illegal zero-length payload (see the file header), or the key is empty; true otherwise.
-
inline bool split_levels(std::vector<key_view_t> &out) const¶
Append each ancestor-prefix level to
out, shallowest-first (the last element equals the whole key) — themkdir -pcreation order.- Returns:
false, appending nothing, if the record framing is ragged (records do not tile the key exactly), any record carries an illegal zero-length payload (see the file header), or the key is empty; true otherwise.
-
class record_cursor_t¶
A resumable INDEXED walk over a key’s segment records —
at(i)without the rescan-from-zero an indexed accessor would otherwise pay per call.Non-allocating: its whole state is the borrowed span, two offsets and one record. It exists for the strip-K mount descent (
child_registry_t::longest_prefixandfwd_router_t::subscribe_toward), which asks for segmentiby index and mostly ascends. An ask BEHIND the walk restarts from the first record — the same answer, at the cost of the rescan a forward ask avoids.Public Functions
-
inline explicit record_cursor_t(key_view_t key) noexcept¶
A cursor over
key, positioned before its first record.
-
inline std::optional<record_t> at(std::size_t i) noexcept¶
The
i-th(0-based) well-framed segment record.- Return values:
std::nullopt – The key has no
i-threcord — it ended, or a record at or beforeiis ragged.
-
inline std::size_t end_of(std::size_t n) noexcept¶
Where the run of the first
nrecords ENDS: 0 forn== 0, the key’s size when fewer thannrecords are well-framed.The offset a strip-K descent slices its residual at —
key.subspan(end_of(k))is everything below the mount, andend_of(k) >= sizeis “the address named the
mount exactly, with nothing below it”.
-
inline explicit record_cursor_t(key_view_t key) noexcept¶
-
constexpr key_view_t() noexcept = default¶
The grammar core¶
The header/trailer rules — the type-0x00 reject, the reserved-bit reject, the
LL length width, trailer sizing, the two-span CRC — are parsed and validated in
one place, read through a small chunk cursor so the same rules serve every
byte source. Both materializing decoders funnel through it. The cursor is the
byte-source seam: span_cursor is the contiguous case and rope_cursor walks
links, stitching a straddled header into a bounded scratch.
Cursor window containment — what holds in a RELEASE build (#986)¶
The cursors’ bounds contracts are not uniformly debug-only, and the split is
deliberate rather than incidental. It rests on one asymmetry: a span_cursor’s
window is its whole object, so overshooting it forms an out-of-range subspan
— undefined behaviour that the fuzz and sanitizer CI reports. A rope_cursor’s
window is a soft bound inside a longer link chain, so an overshooting read
walks bytes the chain genuinely holds. Nothing faults, no sanitizer can see it,
and the caller is handed real bytes from the wrong place and told it succeeded.
reader |
violation in a release build |
|---|---|
|
truncated to the window and the cursor latches ( |
|
UB — caught by fuzz/ASan CI, the backstop the rope case lacks |
|
debug-asserted only; callers bounds-check per read, and a wrong point read is one byte where a wrong feed is an unbounded run |
|
debug-asserted only |
The guarantee is spent on the bulk reader alone because it was priced. Giving
the contiguous cursor the same clamp-and-latch measured compact-forward at
x0.66 deliveries/s and compact-terminus at x0.82 — reproduced in 4/4 and 3/4
interleaved pairs with disjoint ranges — since a min()-derived subspan length
costs the CRC feed loop what a directly-derived one gives it, and carrying a latch
byte takes the cursor past two registers. A latency regression on the delivery
path is an automatic reject here, so span_cursor::poisoned() is a
static constexpr false that folds the shared grammar’s check away entirely.
What the shipped shape costs, measured: zero on the pinned symbols
(bench/symbol_ratchet.json, including route_fwd_forward<rope_cursor>), zero
on the Cortex-M0 P0 flash footprint — a span-only MCU never instantiates the rope
cursor at all — and x1.00 on the interleaved delivery-path A/B. Targets that link a
rope-delivering transport pay +50 B in parse_header<rope_cursor> (+3.0%) on rv32.
rope_cursor_assert_test gates the debug half; rope_cursor_release_guard_test
gates this one, compiled with NDEBUG forced on so it cannot pass vacuously.
-
struct header_t¶
A validated TLV header + trailer: the sink-neutral parse result.
Offsets/sizes only (no payload span), so each sink extracts the spans it wants — the owning tree its
payload/trailer, the arena its trailer-excludedwire+body— from the TLV’s own bytes. All offsets are relative to the TLV’s start.Public Members
-
std::size_t header = 0¶
Header length: 4, or 6 with the
LLbit.
-
std::size_t length = 0¶
Body (payload / children region) length.
-
std::size_t ts_size = 0¶
Timestamp-trailer bytes (0 / 4 / 8).
-
std::size_t crc_size = 0¶
CRC-trailer bytes (0 / 2 / 4).
-
std::size_t total = 0¶
Full encoded size: header + body + trailer.
-
std::size_t header = 0¶
-
enum class tr::wire::grammar::crc_check_t : std::uint8_t¶
When
parse_headerchecks a CRC trailer (ADR-0053 §4).VERIFYis the eager decoders’ policy (and the default — every pre-existing caller is unchanged): the trailer is checked during the header parse, which walks the whole payload.DEFERis the lazy tier’s policy: sizing and bounds are validated but the payload is never touched, so iterating past a sibling costs O(header) — integrity is checked by whichever consumer accesses the TLV (tlv_view_t::verify), per the end-to-end argument.Values:
-
enumerator VERIFY¶
Check the CRC trailer now (walks the payload).
-
enumerator DEFER¶
Skip the CRC walk; integrity is the accessor’s to check.
-
enumerator VERIFY¶
-
template<class Cursor>
std::expected<header_t, err_t> tr::wire::grammar::parse_header(const Cursor &cur, crc_check_t crc_policy = crc_check_t::VERIFY)¶ Parse + validate ONE TLV header and its trailer at offset 0 of
cur.Applies the whole grammar — minimum size,
type == 0x00reject, reserved-bit reject,LLlength width, thePATH_REFfixed-stride body shape (RFC-0024 §4.2/§4.3), trailer sizing, and the two-span CRC (payload ++ timestamp, fed without concatenation) — but does not recurse into a structured payload’s children; the sink’s iterative walk does that. On success the trailer has already been CRC-verified; the caller only re-reads the stored timestamp/CRC bytes it wants to model.- Template Parameters:
Cursor – A byte-source cursor (span_cursor, or the rope cursor).
- Parameters:
cur – The cursor positioned at the TLV’s first byte.
crc_policy – CRC-trailer policy (crc_check_t). Defaults to
VERIFY(the eager decoders’ behavior); the lazy tier passesDEFERso skipping a sibling never walks its payload (ADR-0053 §4).
- Returns:
The validated header_t, or the
err_tthe grammar rejects with (FRAME_TRUNCATED/FRAME_INVALID/FRAME_CRC_FAIL).
-
template<class Cursor, class Sink>
std::expected<void, err_t> tr::wire::grammar::walk(const Cursor &root, Sink &sink, walk_stack_t<Cursor> &stack, crc_check_t crc_policy = crc_check_t::VERIFY)¶ Drive the iterative TLV descent, modelling each node through
sink(ADR-0048 §1 — the ONE structural walk).The recursion-free open-node stack machine that turns validated headers into a tree, shared by both materializing decoders: the owning
tlv_ttree (frame.cpp decode) and the terminus arena (tlv_arena.cpp decode_into). ADR-0048 §1 unified the header grammar (parse_header); this unifies the descent that was still hand-written twice, held equal only by the decode↔decode_into equivalence test. Only the SINK differs — grafting owning children vs appending pre-order arena nodes.Recursion is forbidden (a malicious deep frame must not overflow a small MCU call stack, docs/reference/01 §Iterative parsing requirement): the walk keeps its open-node state in
stack, whose inline-slots + spill shape IS the receiver’s nesting-depth bound (RFC-0006 — no depth constant exists). A frame that exhausts the stack is rejected withTLV_NESTING_TOO_DEEP, amended to mean “exceeds this receiver’s decode resources”.The sink models each node through three hooks (balanced LIFO on success; a rejected frame abandons the sink mid-walk):
on_open(const header_t&, const Cursor& node)— a structured TLV opens;on_leaf(const header_t&, const Cursor& node)— an opaque TLV;on_close()— the current open node’s children are complete.nodeis a cursor over that TLV’s OWN bytes (offset 0 = its first byte), from which the sink extracts the spans it wants (payload / wire / trailer) using the header’s offsets.
- Template Parameters:
Cursor – A byte-source cursor (span_cursor, or the rope cursor).
Sink – A type providing the three hooks above.
- Parameters:
root – The cursor positioned at the frame’s first byte.
sink – The node model (built as the walk visits).
stack – The open-node stack (must be fresh/empty) — the caller’s decode-resource bound (walk_stack_t).
crc_policy – CRC-trailer policy forwarded to parse_header.
- Returns:
Nothing on success, or the first
err_tthe grammar rejects with (includingFRAME_INVALIDfor trailing bytes after the root, andTLV_NESTING_TOO_DEEPon stack exhaustion).
-
struct span_cursor¶
The contiguous byte-source cursor — the grammar’s only source today.
A thin adaptor over one
std::span: the grammar reads its bytes through this seam (parse_header) so the identical rules serve a rope cursor (link-walking, ADR-0048 §1) once that lands, with no rule change here.Public Functions
-
inline std::size_t size() const noexcept¶
Number of bytes available from the TLV’s start.
-
inline span_cursor region(std::size_t off, std::size_t len) const noexcept¶
A sub-cursor over the
[off, off + len)window of this cursor.The contiguous analogue of rope_cursor::region — a plain
subspan, so the same cursor-generic code (a forward-plane header read, a child descent) narrows either source with one call.
-
inline std::uint8_t byte_at(std::size_t off) const noexcept¶
The unsigned byte at offset
off.
-
inline std::uint64_t load_le(std::size_t off, std::size_t n) const noexcept¶
Load
nlittle-endian bytes atoffas a u64 (byteorder.hpp).
-
template<class Fn>
inline void for_each_span(std::size_t off, std::size_t n, Fn &&fn) const¶ Visit the
nbytes atoffas contiguous sub-spans, in order.The CRC-feed seam (
parse_header): a contiguous source yields exactly one span, so this is a straight call; the rope cursor yields one span per straddled link, letting the identical feed cross a link boundary with no concatenation buffer.
Public Members
-
std::span<const std::byte> buf¶
The bytes this cursor reads over.
Public Static Functions
-
static inline constexpr bool poisoned() noexcept¶
Never latched — a contiguous cursor cannot serve a byte from outside its window, so it has nothing to report (#986).
The rope source’s latch exists because its window is a SOFT bound: the link chain physically continues past it, so an overshooting feed reads real bytes belonging to another part of the rope and nothing faults. A
span_cursor’s window is its whole object — for_each_span clamps to it, and past that there are no bytes to serve, wrong or otherwise.static constexpr, not a member: this makesif (cur.poisoned())in the shared grammar fold away entirely on the span source, and — the reason it is written this way — keeps the cursor exactly onestd::spanwide. Carrying aboolhere cost compact-forward −26% deliv/s and compact-terminus −14% (measured, 4/4 and 3/4 interleaved pairs, disjoint ranges): at 24 bytes the cursor stops being two registers, andwalk/regionpass one per descent.
-
inline std::size_t size() const noexcept¶
-
class rope_cursor¶
The rope byte-source cursor — the link-walking twin of
span_cursor.Reads the grammar’s bytes across an ordered chain of view::view_t links so the same
parse_headerrules serve a scatter-gather frame. A window of size bytes ANCHORED at a(link index, intra-link offset)origin; region narrows it to descend into a structured node’s children region — the rope analogue of the span cursor’ssubspan.Public Functions
-
rope_cursor() noexcept = default¶
An empty cursor (zero bytes) — the walk-stack inline-slot default.
-
inline std::size_t size() const noexcept¶
Number of bytes available from this cursor’s origin.
-
inline bool poisoned() const noexcept¶
Has any read through THIS cursor been truncated at the window edge? (#986)
The release-mode half of for_each_span’s containment contract. A bulk feed that overshoots the window is clamped rather than served, and latches this — so the decode boundary can answer
FRAME_TRUNCATED(parse_headerdoes) instead of consuming a short feed as if it were whole. Sticky: it is never cleared, because a frame that produced one wrong-length read is not made sound by a later good one.Note
Checked on the cursor that was FED. region hands out a copy, so a sub-cursor’s latch does not travel back to its parent; the boundaries that map this to an error (
parse_header, the forward-plane gather) each test the cursor they passed, which is why a temporary sub-cursor — the shapewalkandpeek_fwd_*descend with — is still covered.
-
inline std::pair<std::size_t, std::size_t> anchor() const noexcept¶
This cursor’s origin as a
(link index, intra-link offset)anchor.
-
inline rope_cursor region(std::size_t off, std::size_t len) const noexcept¶
A sub-cursor over the
[off, off + len)window of this cursor.O(links crossed by
off) — walks the anchor forward from THIS cursor’s origin (never from link 0) and re-windows, sharing the same link chain and copying no link. Used to descend into a node’s children region exactly asdecode_intosubspans the payload.Note
Precondition:
off + len <= size()(debug-asserted) — the same containment contractspan_cursor::regiongets for free fromstd::span::subspan, and that view::view_t::subview asserts.
-
inline std::uint8_t byte_at(std::size_t off) const noexcept¶
The unsigned byte at offset
off(walks to its link).Note
Precondition:
off < size()(debug-asserted). The grammar callers bounds-check before every read; this makes that contract visible and a violation loud instead of a silent wrong byte.
-
inline std::uint64_t load_le(std::size_t off, std::size_t n) const noexcept¶
Load
nlittle-endian bytes atoffas a u64 (stitched across links).ONE
seekthen a forward walk — notnseeks. The trailer loads (parse_header’s length field, a CRC value, an 8-byte timestamp) are the cursor’s densest reads, and paying the chain walk per byte made a straddling timestamp cost eight of them.
-
template<class Fn>
inline void for_each_span(std::size_t off, std::size_t n, Fn &&fn) const¶ Visit the
nbytes atoffas contiguous per-link sub-spans, in order.The CRC-feed seam: a range wholly inside one link yields a single span; a straddling range yields one span per link it crosses, so the grammar’s incremental CRC crosses a link boundary with no concatenation buffer.
Note
Precondition:
off + n <= size(). Violating it is DEFINED, in every build (#986): the feed is truncated to the window, so no byte from outside it is ever handed tofn, and poisoned latches — see that accessor for why the window is enforced here rather than left to the caller. The debug assert below still fires first, so a violation stays loud in a debug build and CI.
Public Static Functions
-
static inline rope_cursor at(std::span<const view::view_t> links, std::size_t li, std::size_t intra, std::size_t len) noexcept¶
A cursor over
lenbytes oflinksstarting at linkli, byteintra— the RESUMABLE form.Lets a caller that already walked to a position (the lazy child iterator,
tlv_view.hpp) re-enter the chain there instead of from link 0, which is the same Θ(children × links) the anchored origin removes insidegrammar::walk.Note
Precondition: the anchor names a byte the chain holds (or its exact end, for an empty window) and
lenbytes follow it.
-
rope_cursor() noexcept = default¶
-
template<class Cursor>
struct walk_frame_t¶ One open structured node’s traversal state in
walk: a cursor over its children region and the walk position within it.- Template Parameters:
Cursor – A byte-source cursor (span_cursor, or the rope cursor).
-
template<class Cursor>
class walk_stack_t¶ The walk’s open-node stack — the RFC-0006 receiver-resource depth bound.
Nesting depth is bounded by the receiver’s decode resources, never by a constant. The stack starts in the caller-provided
inline_slotsspan — a TUNING knob, not a limit: overflowing it changes cost, not behavior — and, once those are exhausted, relocates into geometrically grown blocks drawn fromspill(one open-node record per open level, RFC-0006).A null
spillmakes the inline span the receiver’s whole decode budget: exhaustion makes push return false, whichwalkmaps toTLV_NESTING_TOO_DEEP(“exceeds this receiver’s decode resources”) — a clean reject, no throw, safe under-fno-exceptions. A non-nullspillis drawn from NOTHROW (#588): exhaustion there returnsnullptrand takes the SAME reject path, so the depth bound is honest whether or not a spill exists.Note
spillis a tr::mem::block_source_t and deliberately not astd::pmr::memory_resource. It used to be the latter, andgrowused its throwingallocateunguarded — so a peer sending a frame nested past the inline slots could reach__cxa_throw’sabort()stub on a-fno-exceptionsnode, from the RX decode path, behind no ACL (#588). The reject this replaces it with was already specified; only the allocation was dishonest.Public Functions
-
inline walk_stack_t(std::span<walk_frame_t<Cursor>> inline_slots, mem::block_source_t *spill) noexcept¶
A stack over
inline_slots, spilling tospillwhen they run out.
-
inline ~walk_stack_t()¶
Releases the spill block, if any (the inline slots are the caller’s).
-
walk_stack_t(const walk_stack_t&) = delete¶
Non-copyable (one walk, one stack — the spill block has one owner).
-
walk_stack_t &operator=(const walk_stack_t&) = delete¶
Non-assignable.
-
inline bool push(const walk_frame_t<Cursor> &f)¶
Open one node. False ⇔ the receiver’s decode resources are exhausted — the inline slots are full and the spill is absent OR itself exhausted. Never throws (#588).
-
inline bool empty() const noexcept¶
True when no node is open.
-
inline walk_frame_t<Cursor> &back() noexcept¶
The innermost open node.
-
inline void pop() noexcept¶
Close the innermost open node.
-
inline walk_stack_t(std::span<walk_frame_t<Cursor>> inline_slots, mem::block_source_t *spill) noexcept¶
Stream framing¶
Splitting a byte stream back into frames is the transport’s job, and it is one
state machine rather than one per transport. length_prefix_framer reassembles
u32-LE length-prefixed frames from arbitrary chunks: each complete frame lands in
one exactly-sized refcounted segment drawn from the caller’s backend, so
there is no library-owned buffer and exactly one copy off the wire. An allocation
failure is backpressure — the frame is drained so framing sync survives, and
counted — while an oversize prefix is malformed, because a desynchronized stream
cannot be re-framed and the caller must tear the connection down. The state
machine names no transport type, which is why it is tested directly with no live
connection.
-
class length_prefix_framer¶
Reassembles u32-LE length-prefixed frames from an arbitrarily-chunked stream.
Fed one chunk at a time via feed; each completed frame is delivered through the caller’s
on_framecallback as a fresh, exactly-sized segment. Holds only partial-reassembly state (a prefix scratch + the in-flight segment), so one framer serves one stream and is reused across a stream’s chunks; reset discards partial state when a new peer’s stream takes over.Public Functions
-
template<class OnFrame, class OnDrop>
inline result_t feed(mem::mem_backend_t &backend, std::size_t max_frame, const std::byte *p, std::size_t n, OnFrame &&on_frame, OnDrop &&on_drop)¶ Feed
nbytes atp; deliver each completed frame viaon_frameand report each backpressure drop viaon_drop, both in arrival order.The ORDERING is the contract, not an implementation detail (#1255):
on_dropruns at the moment the drop is decided, so it always precedes theon_frameof any frame that arrives after the dropped one — including when both share a single chunk, which is the common case for small frames. A caller whoseon_dropbumps a counter therefore gives every receiver this guarantee: by the time a delivered frame is observable, every drop before it is already counted. A relaxed atomic is enough to carry it — the increment is sequenced-before the delivery, so whatever release/acquire edge publishes the frame to an observer (a sink’s mutex, a queue) carries the count with it.- Template Parameters:
OnFrame – Callable
void(tr::view::segment_ptr_t seg, std::size_t len)—segowns exactlylenbytes of one reassembled frame.OnDrop – Callable
void()— one backpressure drop was just decided.
- Parameters:
backend – Where each frame’s segment is allocated (ADR-0042 §2/§4).
max_frame – The caller’s PROTOCOL frame ceiling (
configured_cap): a prefix beyond it is malformed and stops the feed. A frame within it that the backend cannot hold — exhausted, or larger than any segment it produces (effective_cap) — is drained and reported throughon_dropinstead, so local capacity backpressures a legitimate peer rather than disconnecting it (#932).p, n – The chunk (may split a prefix or a body arbitrarily).
on_frame – Invoked once per completed frame, in arrival order.
on_drop – Invoked once per frame shed to backpressure, at decision time.
- Returns:
Per-chunk
result_t: whether an oversize prefix stopped the feed (the caller shuts the peer down).
-
inline void reset() noexcept¶
Discard partial reassembly state (a new peer’s stream reuses the framer).
Public Static Functions
-
static inline constexpr std::size_t configured_cap(std::size_t max_frame) noexcept¶
Resolve a
:settings max_framevalue into the per-connection cap — TIGHTEN-ONLY against kDefaultMaxFrame.0(unset) keeps the default; a nonzero value yieldsmin(max_frame, kDefaultMaxFrame). The setting arrives through a config-writable key (a connection SPEC’s:settings), so it may only narrow what the node will buffer off the wire — never widen it above the protocol default (#1035). Every framed transport (tcp / ws / quic / webtransport) assigns its per-connection cap through this one home.
-
static inline std::size_t effective_cap(const mem::mem_backend_t &backend, std::size_t max_frame) noexcept¶
The effective RX frame cap:
min(max_frame, backend.max_segment_size())— the largest frame this connection can actually DELIVER.The bound is the injected resources’ real capacity, not a magic number (the no-synthetic-limits doctrine). It is a deliverability bound, not the protocol’s framing bound: a length above it but within
max_frameis shed as backpressure, not treated as malformed (#932) — see on_prefix. Transports that refuse an oversize frame from a DECLARED length before buffering a body byte (the WS frame header) compare against this.
-
static inline prefix_decision_t on_prefix(mem::mem_backend_t &backend, std::size_t max_frame, std::size_t len)¶
Apply the shared framing rules to one decoded u32 length prefix.
- Parameters:
backend – Where an accepted frame’s segment is allocated (ADR-0042 §2/§4).
max_frame – The PROTOCOL cap (from configured_cap) — the only bound whose violation means the stream is desynced. A length within it that the backend cannot satisfy (exhausted, or simply larger than any segment this backend produces) comes back as
DROP, so a legitimate peer is backpressured rather than disconnected over OUR local capacity (#932).len – The decoded length prefix.
- Returns:
The prefix_decision_t;
ACCEPTcarries the freshly allocated segment.
Public Static Attributes
-
static constexpr std::size_t kPrefixBytes = 4¶
The u32-LE length prefix’s size on the wire (transport framing).
-
static constexpr std::size_t kDefaultMaxFrame = 16u * 1024u * 1024u¶
The default receive frame cap when
:settings max_frameis unset (16 MiB) — one home for the default every u32-length-prefixed stream transport (tcp / quic / webtransport) restates as itskMaxFrame.A prefix announcing more is malformed (corrupt or hostile): the stream has lost framing sync and the connection is torn down. A per-connection
:settings max_framemay TIGHTEN the cap below this default, never raise it; the effective cap is further bounded by the injected backend’s real capacity (effective_cap — the no-synthetic-limits doctrine).
-
struct prefix_decision_t¶
What one length prefix means under the shared framing rules.
The single home for the per-prefix decisions every u32-length-prefixed stream applies identically:
len == 0is a no-op record,lenbeyond the protocol cap is malformed (a desynced stream cannot be re-framed), and a frame the backend cannot hold —allocfailed, including because the frame is larger than any segment this backend can ever produce — is backpressure (drain the body, count a drop). feed applies them to a chunk-fed stream; a pull-mode blocking reader (tcp_transport_t) applies the same rules but reads the body straight off the socket into the accepted segment — adopting feed there would add a scratch-buffer copy on the hot path (ADR-0042 §2/§4), so the rules are shared instead of the state machine.Public Types
-
enum class kind_t : std::uint8_t¶
The decision kind.
Values:
-
enumerator EMPTY¶
len == 0— the record carries no TLV; skip it.
-
enumerator MALFORMED¶
lenexceeds the PROTOCOL cap — tear the stream down.
-
enumerator DROP¶
The backend could not hold the frame (
allocfailed, or the frame exceeds this backend’s segment size) — drain the body, count a drop.
-
enumerator EMPTY¶
-
enum class kind_t : std::uint8_t¶
-
struct result_t¶
The outcome of feeding one chunk (the transport applies the effects).
Deliveries happen inline through
on_frameand drops inline throughon_drop; this reports only the one outcome that STOPS the feed, which the caller must act on with its connection handle. It deliberately carries no drop COUNT: a per-chunk tally can only be read after the whole chunk has been processed, i.e. after deliveries the drop preceded (#1255).Public Members
-
bool malformed = false¶
A prefix beyond the PROTOCOL cap was seen — stop, shut the peer down.
-
bool malformed = false¶
-
template<class OnFrame, class OnDrop>
See the reference data-format for the normative rules and wire-format-bits for worked byte dumps.