What decode refuses, and who each refusal accuses (L2/L3 codec)

decode answers std::expected<tlv_t, err_t>, and the error side is the RFC-0002 registry (status module) rather than a decode-private vocabulary — so err_path, severity and disposition come for free. Four verdicts are reachable, and they do not all mean the same kind of thing.

Three are permanent accusations about the bytes: FRAME_TRUNCATED (the frame stops mid-TLV), FRAME_INVALID (a clean TLV with bytes left over — or a reserved opt bit set) and FRAME_CRC_FAIL (see the trailer).

What to notice

  • decode consumes the whole input. A single trailing byte after a well-formed TLV is FRAME_INVALID, not a successful parse of the prefix. A stream reader frames first and decodes exactly one TLV’s worth.

  • A reserved opt bit is checked before anything is believed. Bits 7 and 0 are MUST-be-zero; a peer that sets one has said something this version cannot interpret, and the frame is refused rather than partially honoured.

  • TLV_NESTING_TOO_DEEP is different in kind. It means “exceeds this receiver’s decode resources” (RFC-0006, CONTEXT.md §Resource bound). The structural walk starts in inline stack slots and spills into a caller-injected block_source_t; decode(bytes, mem::null_source()) is the spelling of “no spill at all”, and the same bytes decode once the caller injects a source that can serve. The bound is the injected resource, and two receivers may legitimately disagree about one frame.

  • The example asserts the verdict, never a depth number. There is no constant to assert. The inline slot count is a tuning knob whose overflow changes cost, not behaviour — see the arena decode for the same seam used deliberately, and note that neither a “depth cap” nor a kMaxDepth exists to point at.

  • Nothing here is conditional — the target builds and runs under every CI leg.

Source

 1/*
 2 * SPDX-License-Identifier: Apache-2.0
 3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
 4 */
 5
 6/**
 7 * @file
 8 * @brief ONE CONCEPT — the four verdicts `decode` returns, and who each one accuses.
 9 *
10 * `decode` answers `std::expected<tlv_t, err_t>`, and the error side is the RFC-0002 registry
11 * (`libtracer/error.hpp`) rather than a decode-private vocabulary. The three a reader hits
12 * first are permanent accusations against the bytes: `FRAME_TRUNCATED` (the frame stops
13 * mid-TLV), `FRAME_INVALID` (well-formed prefix, trailing bytes after it — or a reserved
14 * `opt` bit set) and `FRAME_CRC_FAIL` (see `wire_trailer`).
15 *
16 * The fourth is different in kind. `TLV_NESTING_TOO_DEEP` means "exceeds **this receiver's**
17 * decode resources" (RFC-0006, `CONTEXT.md` §Resource bound): the walk stack starts in inline
18 * slots and spills into a caller-injected `block_source_t`, so the SAME bytes that a
19 * heap-spilled decode accepts are refused by `decode(bytes, mem::null_source())` — the
20 * spelling of "no spill at all". Nothing here asserts a depth number, because there is no
21 * constant to assert: the bound is the source the caller passed.
22 *
23 * Runs under ctest as `example_wire_decode_refusals`; returns non-zero on any failed check.
24 */
25
26#include <cstddef>
27#include <cstdint>
28#include <cstdio>
29#include <span>
30#include <utility>
31#include <vector>
32
33#include "libtracer/tracer.hpp"
34
35namespace {
36
37using tr::wire::err_t;
38using tr::wire::tlv_t;
39using tr::wire::type_t;
40
41/** @brief Report expectation @p what and record a failure on @p ok. */
42void check(bool& ok, bool cond, const char* what) {
43    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
44    ok = ok && cond;
45}
46
47/** @brief `depth` nested structured `POINT`s wrapping one `VALUE` over @p body. */
48tlv_t nest(std::span<const std::byte> body, int depth) {
49    tlv_t cur{.type = type_t::VALUE, .payload = body};
50    for (int i = 0; i < depth; ++i) {
51        tlv_t parent;
52        parent.type = type_t::POINT;
53        parent.opt.pl = true;
54        parent.children.push_back(std::move(cur));
55        cur = std::move(parent);
56    }
57    return cur;
58}
59
60}  // namespace
61
62int main() {
63    bool ok = true;
64    const std::vector<std::byte> body(4, std::byte{0x5A});
65
66    std::vector<std::byte> frame = tr::wire::encode(tlv_t{.type = type_t::VALUE, .payload = body});
67    check(ok, tr::wire::decode(frame).has_value(), "the reference frame decodes cleanly");
68
69    std::vector<std::byte> short_frame(frame.begin(), frame.end() - 1);
70    const auto truncated = tr::wire::decode(short_frame);
71    check(ok, !truncated && truncated.error() == err_t::FRAME_TRUNCATED,
72          "one byte short is FRAME_TRUNCATED");
73
74    std::vector<std::byte> extra = frame;
75    extra.push_back(std::byte{0x00});
76    const auto trailing = tr::wire::decode(extra);
77    check(ok, !trailing && trailing.error() == err_t::FRAME_INVALID,
78          "a byte after the one TLV is FRAME_INVALID — decode consumes the whole input");
79
80    std::vector<std::byte> reserved = frame;
81    reserved[1] |= std::byte{0x01};  // bit 0 of opt is reserved-MUST-be-zero
82    const auto bad_opt = tr::wire::decode(reserved);
83    check(ok, !bad_opt && bad_opt.error() == err_t::FRAME_INVALID,
84          "a set reserved opt bit is FRAME_INVALID");
85
86    // The receiver-resource verdict. Same bytes, two injected spill sources, two answers.
87    const std::vector<std::byte> deep = tr::wire::encode(nest(body, 16));
88    std::printf("a 16-deep frame is %zu bytes\n", deep.size());
89    const auto no_spill = tr::wire::decode(deep, tr::mem::null_source());
90    check(ok, !no_spill && no_spill.error() == err_t::TLV_NESTING_TOO_DEEP,
91          "with a source that serves nothing, the walk stack cannot spill");
92    check(ok, tr::wire::decode(deep, tr::mem::heap_source()).has_value(),
93          "and the very same bytes decode once the caller injects one that can");
94    return ok ? 0 : 1;
95}

See also: frame codec · status module · data-format reference.