Wire codec round-trip (L2/L3)¶
The frame codec is where a tlv_t model becomes bytes and
bytes become a validated tlv_node_t. This example builds a packed PATH TLV for
/sensor/temp (RFC-0018) with a CRC trailer, encodes it to wire bytes, reads those bytes
back with tlv_node_t::over, and proves the round-trip is exact.
What to notice¶
The read borrows, never copies — the node’s packed body is a
std::spanthat points into the encoded buffer; the example checks the payload address lies insidewire. Keeping a node means keeping its backing bytes alive (that is what views provide).The CRC trailer is verified on read —
opt.crmakesencodeappend a CRC-32C over the body;overrecomputes and checks it, andtrailer()surfaces the parsedcrc_t.The round-trip invariant — re-encoding what was read reproduces the exact wire bytes (
encode(decode(bytes)) == bytes), CRC and all.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief L2/L3 wire codec round-trip — build a TLV, encode to bytes, decode back.
9 *
10 * The wire codec is where a `tlv_t` model becomes bytes and bytes become a validated
11 * `tlv_node_t` read in place (`docs/modules/frame-codec.md`). This example builds a packed
12 * PATH TLV (`/sensor/temp`, RFC-0018) with a CRC trailer, `encode`s it to wire bytes, reads
13 * those bytes back with `tlv_node_t::over`, and checks that re-encoding what it read
14 * reproduces the exact wire bytes.
15 * It also shows the zero-copy nature of the read: the node's payload is a `std::span`
16 * that BORROWS the encoded buffer, so no payload bytes are copied.
17 *
18 * Runs under ctest as `example_wire_roundtrip`: it checks structure, byte-identity,
19 * and the verified CRC trailer, returning non-zero on any mismatch.
20 */
21
22#include <algorithm>
23#include <cstddef>
24#include <cstdio>
25#include <span>
26#include <string>
27#include <vector>
28
29#include "libtracer/tracer.hpp"
30
31namespace {
32
33using tr::wire::opt_t;
34using tr::wire::tlv_t;
35using tr::wire::type_t;
36
37/** @brief A byte span over the characters of @p s (no copy; @p s must outlive the span). */
38std::span<const std::byte> bytes_of(const std::string& s) {
39 return {reinterpret_cast<const std::byte*>(s.data()), s.size()};
40}
41
42/** @brief A NAME TLV borrowing @p name's bytes. */
43tlv_t name_tlv(const std::string& name) {
44 tlv_t t;
45 t.type = type_t::NAME;
46 t.payload = bytes_of(name);
47 return t;
48}
49
50/** @brief Record a failed expectation on @p ok and report it. */
51void check(bool& ok, bool cond, const char* what) {
52 if (!cond) {
53 std::printf(" [FAIL] %s\n", what);
54 ok = false;
55 }
56}
57
58} // namespace
59
60int main() {
61 // The NAME segment bytes must outlive every TLV that borrows them.
62 const std::string seg0 = "sensor";
63 const std::string seg1 = "temp";
64
65 // Build a PACKED PATH TLV (RFC-0018 — `opt.PL = 0`, the body is a run of
66 // `[u8 len][bytes]` segment records) with a CRC trailer (opt.cr — encode recomputes
67 // the CRC-32C over the body).
68 std::vector<std::byte> packed;
69 (void)tr::wire::emit_path_segment(packed, seg0);
70 (void)tr::wire::emit_path_segment(packed, seg1);
71 tlv_t path;
72 path.type = type_t::PATH;
73 path.opt = opt_t{.cr = true};
74 path.payload = std::span<const std::byte>(packed);
75
76 // Encode the model to wire bytes, then decode those bytes back into a tree.
77 const std::vector<std::byte> wire = tr::wire::encode(path);
78 std::printf("encoded /sensor/temp PATH TLV: %zu bytes\n", wire.size());
79
80 const std::expected<tr::wire::tlv_node_t, tr::wire::err_t> decoded =
81 tr::wire::tlv_node_t::over(std::span<const std::byte>(wire));
82
83 bool ok = true;
84 check(ok, decoded.has_value(), "decode succeeds (CRC trailer verifies)");
85 if (decoded) {
86 const auto trailer = decoded->trailer();
87 std::printf("decoded: type=0x%02X, %s children, trailer.crc=%s\n",
88 static_cast<unsigned>(decoded->type()),
89 decoded->children().empty() ? "no" : "has",
90 (trailer && trailer->crc) ? "present" : "absent");
91 check(ok, decoded->type() == type_t::PATH, "decoded root is a PATH");
92 check(ok, !decoded->opt().pl, "a packed PATH is NOT structured (opt.PL = 0, RFC-0018)");
93 check(ok, decoded->children().empty(), "a packed PATH has no child TLVs");
94 check(ok, decoded->payload().size() == 1 + seg0.size() + 1 + seg1.size(),
95 "the body is one length byte per segment plus the segment text");
96 check(ok, trailer && trailer->crc.has_value(),
97 "decoded PATH carries the verified CRC trailer");
98 // The decoded payload borrows the encoded buffer — zero copy.
99 {
100 const auto body = decoded->payload();
101 check(ok, body.data() >= wire.data() && body.data() < wire.data() + wire.size(),
102 "the packed body is a span INTO the encoded buffer (zero copy)");
103 const auto want = bytes_of(seg0);
104 const bool same =
105 body.size() > want.size() &&
106 static_cast<std::size_t>(static_cast<std::uint8_t>(body[0])) == want.size() &&
107 std::equal(want.begin(), want.end(), body.begin() + 1);
108 check(ok, same, "the first packed record round-trips to \"sensor\"");
109 }
110 // The strongest round-trip invariant: re-encoding what was read reproduces the
111 // exact wire bytes (byte-identical, CRC recomputed and all).
112 tlv_t again;
113 again.type = decoded->type();
114 again.opt = decoded->opt();
115 again.payload = decoded->payload();
116 check(ok, tr::wire::encode(again) == wire, "encode(decode(bytes)) == bytes");
117 }
118
119 std::printf("%s\n", ok ? "round-trip OK" : "round-trip FAILED");
120 return ok ? 0 : 1;
121}
See also: frame-codec module · bit-level wire walkthrough · data-format reference.