--- id: tunnels/wire-codec name: Data-plane codec (frame_datagram / DatagramReader) + sentinel layering tests status: completed depends_on: [tunnels/crate-init, tunnels/params] scope: narrow risk: medium impact: component level: implementation tags: [wire, codec, udp] --- ## Description Implement `src/wire.rs` per ADR-003 + wire.md §The Data Plane + bast.md: the mandatory UDP length-framing codec. This is a direct port of the forward POC's `/workspace/alktunnels-udp-poc/src/wire.rs` (17 tests rode it; the shape is settled) with the POC-to-crate generalizations the ADRs pin. ### API ```rust pub const MAX_DATAGRAM_LEN: usize = u16::MAX as usize; // 65535 pub struct DatagramCodecError { .. } // thiserror: Oversize(usize), InvalidLength(...) pub fn frame_datagram(payload: &[u8]) -> Result // [len: u16 BE][payload]; len=0 is a legal empty datagram; >65535 = Oversize at frame time pub struct DatagramReader { /* incremental decoder state */ } impl DatagramReader { pub fn new() -> Self pub fn feed(&mut self, chunk: &[u8]) -> Result, DatagramCodecError> // zero or more complete datagrams per chunk; buffers partial frames // across chunk boundaries } ``` ### The invariants the tests MUST pin (all POC-validated; they are the spec's executable form) 1. Single datagram round-trip (frame → feed → exact bytes out). 2. Empty datagram (`len=0`): survives as a real datagram — NEVER confused with EOF (the F-2 invariant; the codec layer never emits a zero-length read). 3. Split across chunks (feed a frame in awkward 7-byte chunks; exact reassembly). 4. Two datagrams batched in one chunk (feed once, two out, in order). 5. Partial header at a chunk boundary (1-byte `len` prefix split). 6. Mid-datagram state observable (incremental decode correctness). 7. Oversize rejected at frame time (never a wire overflow). 8. Truncation fail-loud (OQ-TN-13): the adapter-level receive that would truncate surfaces as an error — test at the codec boundary by asserting `frame_datagram` + reader round-trip with a buffer smaller than a full datagram is not silently accepted (the concrete API shape lands with `tunnels/local-socket-halves`; here, pin the codec-side invariant: a truncated stream yields an error, not a partial datagram). No `stream_type` byte, no 5-byte header — a tunnel has one data stream per direction (ADR-003). No EOF sentinel in the codec — EOF is the channels-level `length=0` chunk, a different layer (the two coexist; the POC proved no collision). ## Acceptance Criteria - [x] All 8 test families above pass (the POC's 7 + the truncation pin) - [x] `frame_datagram` returns `Bytes` (zero-copy handoff to the mux) - [x] `DatagramReader` state is incremental across arbitrary chunk boundaries - [x] `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` clean - [x] wasm32 check passes (the codec is pure byte work — it must be wasm-clean) ## References - docs/architecture/decisions/003-codec-and-udp-framing.md - docs/architecture/bast.md (the binary contract) - docs/architecture/wire.md §The Data Plane (normative byte diagrams) - POC reference: `/workspace/alktunnels-udp-poc/src/wire.rs` (port with the crate's doc-comment style) ## Notes - Direct port of the POC's wire.rs (17 tests rode it) with the crate conventions: thiserror doc comments, module doc per ADR-003 + wire.md + bast.md layering (sentinel layering called out — the F-2 invariant is load-bearing, so the module doc states why raw pass-through is structurally broken for UDP). - Error naming per the task's API: `Oversize(usize, usize)` (was POC's `TooLarge`) + `InvalidLength {declared, have}` (the truncated-stream shape — the OQ-TN-13 fail-loud posture's codec-side anchor). - `feed` returns `Vec` per the task API (the POC's `Vec` wrapper dropped — `Bytes` is the payload; the mux handoff is zero-copy via `BytesMut::freeze`). - `MAX_DATAGRAM_LEN`/`DATAGRAM_LEN_FIELD` constants; `DATAGRAM_LEN_FIELD` is private (implementation detail), `MAX_DATAGRAM_LEN` public (the frame-time bound consumers need). - 8 test families: single round-trip, empty-datagram-not-EOF, 7-byte-chunk split reassembly, two-datagram batch, partial header, mid-datagram observability, oversize at frame time, truncated stream (feeds an empty chunk + a one-byte-short tail — never yields a partial datagram; the codec-side half of OQ-TN-13; the adapter-level receive shape lands with local-socket-halves). - POC's `Datagram` struct dropped: `Bytes` suffices (the task's `feed -> Vec` signature); `is_empty()` moves to callers (trivial on `Bytes`). ## Summary `src/wire.rs` complete: `frame_datagram` (Oversize at frame time, `len=0` legal), `DatagramReader` (incremental decoder, `is_mid_datagram` teardown diagnostic), `DatagramCodecError {Oversize, InvalidLength}` — the ADR-003/BAST contract in executable form. 8 codec tests + 6 params tests = 14 passing. Verified: cargo test, clippy --all-targets -D warnings (native + wasm32), fmt --check, wasm32 check — all clean.