Wire codec round-trip (L2/L3)

The frame codec is the one place bytes become a tlv_t tree and back. This example builds a structured PATH TLV for /sensor/temp (two NAME children) with a CRC trailer, encodes it to wire bytes, decodes those bytes into a fresh tree, and proves the round-trip is exact.

What to notice

  • Decode borrows, never copies — the decoded NAME payloads are std::spans that point into the encoded buffer; the example checks the payload address lies inside wire. Keeping a decoded tlv_t means keeping its backing bytes alive (that is what views provide).

  • The CRC trailer is verified on decodeopt.cr makes encode append a CRC-32C over the body; decode recomputes and checks it, and surfaces the parsed crc_t.

  • The round-trip invariant — re-encoding the decoded tree 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 the one place bytes become a `tlv_t` tree and back
 11 * (`docs/modules/frame-codec.md`). This example builds a structured PATH TLV
 12 * (`/sensor/temp` — two NAME children) with a CRC trailer, `encode`s it to wire
 13 * bytes, `decode`s those bytes into a fresh tree, and checks that re-encoding
 14 * reproduces the exact wire bytes.
 15 * It also shows the zero-copy nature of decode: the decoded payloads are
 16 * `std::span`s that BORROW 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<tlv_t, tr::wire::err_t> decoded =
 81        tr::wire::decode(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        std::printf("decoded: type=0x%02X, %zu children, trailer.crc=%s\n",
 87                    static_cast<unsigned>(decoded->type), decoded->children.size(),
 88                    (decoded->trailer && decoded->trailer->crc) ? "present" : "absent");
 89        check(ok, decoded->type == type_t::PATH, "decoded root is a PATH");
 90        check(ok, !decoded->opt.pl, "a packed PATH is NOT structured (opt.PL = 0, RFC-0018)");
 91        check(ok, decoded->children.empty(), "a packed PATH has no child TLVs");
 92        check(ok, decoded->payload.size() == 1 + seg0.size() + 1 + seg1.size(),
 93              "the body is one length byte per segment plus the segment text");
 94        check(ok, decoded->trailer && decoded->trailer->crc.has_value(),
 95              "decoded PATH carries the verified CRC trailer");
 96        // The decoded payload borrows the encoded buffer — zero copy.
 97        {
 98            const auto body = decoded->payload;
 99            check(ok, body.data() >= wire.data() && body.data() < wire.data() + wire.size(),
100                  "the packed body is a span INTO the encoded buffer (zero copy)");
101            const auto want = bytes_of(seg0);
102            const bool same =
103                body.size() > want.size() &&
104                static_cast<std::size_t>(static_cast<std::uint8_t>(body[0])) == want.size() &&
105                std::equal(want.begin(), want.end(), body.begin() + 1);
106            check(ok, same, "the first packed record round-trips to \"sensor\"");
107        }
108        // The strongest round-trip invariant: re-encoding the decoded tree
109        // reproduces the exact wire bytes (byte-identical, CRC and all).
110        check(ok, tr::wire::encode(*decoded) == wire, "encode(decode(bytes)) == bytes");
111    }
112
113    std::printf("%s\n", ok ? "round-trip OK" : "round-trip FAILED");
114    return ok ? 0 : 1;
115}

See also: frame-codec module · bit-level wire walkthrough · data-format reference.