Skip to content

netrom: LinBPQ L4 payload compression (compact hand-rolled zlib, feature-gated) - #68

Merged
M0LTE merged 5 commits into
mainfrom
feat/netrom-deflate-codec
Jul 12, 2026
Merged

netrom: LinBPQ L4 payload compression (compact hand-rolled zlib, feature-gated)#68
M0LTE merged 5 commits into
mainfrom
feat/netrom-deflate-codec

Conversation

@M0LTE

@M0LTE M0LTE commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Implements the BPQ L4Compress interop feature (Table B follow-up, decided In), default-off, behind the netrom-compress cargo feature so the default build carries zero compression flash.

Codec (transport/deflate.rs): a compact hand-rolled zlib (RFC-1950) + DEFLATE (RFC-1951) — full inflate (stored/fixed/dynamic Huffman, puff.c structure) + a compact fixed-Huffman deflate, 8 KiB fail-closed cap. Proven correct both directions against miniz_oxide as a dev-only oracle (never shipped). Chosen over the library on measured flash: ~7 KiB hand-rolled vs ~37 KiB miniz_oxide on thumbv6m (~5×), matching the highly-constrained-flash requirement.

Protocol (mirrors C# NetRomCircuit/ConnectAckInfo/NetRomCompression byte-for-byte): Compressed=0x10 on Information frames; extended Connect-Request (17-octet, offer bit 0x40) / Connect-Ack ([window][ttl|0x80] agree) negotiation — compression used only when both ends advertise; compress-whole-then-fragment with per-send raw fallback; reassemble-then-inflate-once. Declining = vanilla plain NET/ROM.

Feature-OFF is byte-identical to today (724 tests unchanged; the only always-compiled change is a behaviour-preserving mem::take retransmit refactor; the connect-request parser already tolerated the extended trailer). Feature-ON: 756 tests (+19 L4 + 13 codec). clippy clean both ways, fw --locked green, no_std-clean, drift-guard 0 gaps. A self-hosted CI lane runs the feature so it can't rot.

NODESPACLEN was investigated and deliberately left out — it's a BPQ-ism (not in the NET/ROM spec), and because NODES entries are fixed 21-octet records, uniform chunking is byte-identical to BPQ's greedy pack, so there's no "properly" to do beyond the trivial port; pico-node's existing 11-entries/frame already matches C#'s default.

🤖 Generated with Claude Code

Tom Fanning and others added 5 commits July 12, 2026 21:24
Hand-rolled, correctness-critical zlib (RFC 1950) + DEFLATE (RFC 1951)
codec for NET/ROM L4 payload compression (BPQ L4Compress interop), the
Rust port of Packet.NetRom.Transport.NetRomCompression.

Gated behind a new `netrom-compress` cargo feature so the DEFAULT
on-target build carries ZERO compression code (the flash win).

- Inflate: full RFC-1951 (stored + fixed-Huffman + dynamic-Huffman),
  zlib-wrapped (2-octet header parse/verify + trailing Adler-32 verify),
  caller-supplied output cap (default 8 KiB, matching BPQ). Fail-closed
  on any malformed input, bad header, bad Adler-32, or cap-exceed. No
  panics, no unsafe, integer-only.
- Deflate: compact greedy LZ77 (hash-chain match finder) emitting a
  single fixed-Huffman block, zlib-wrapped (0x78 0x01 header + Adler-32).

miniz_oxide added as a [dev-dependencies]-ONLY test oracle (never
shipped; exempt from the one-dep rule like serde_json). 13 tests prove
both directions: self round-trip, Oracle A (miniz reads our output),
Oracle B (we read miniz incl. dynamic Huffman + stored), Adler-32
vectors, and fail-closed on corrupt/bad-header/bad-checksum/cap-exceed.

Gate: cargo test (feature off) 724 ok; (feature on) 737 ok; clippy
-D warnings clean (both configs); no_std build
(--no-default-features --features alloc,netrom-compress) clean.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…codecs

Feature-gated (netrom-compress) wire foundation for LinBPQ-compatible L4
compression negotiation, mirroring the C# Packet.NetRom.Wire reference:

- transport_header: FLAG_COMPRESSED (0x10, L4COMP) + compressed() accessor.
- connect_request_info: extended 17-octet form (encode_extended) with the
  0x40 compress-offer bit in the T1-timer trailer; offers_compression().
  The canonical decode() already tolerates the trailer (parse-and-ignore),
  so no always-compiled change was needed to accept an inbound BPQ extended
  connect.
- connect_ack_info (NEW): extended [window][ttl|0x80] agree form; a declining
  ack is the vanilla empty info field (byte-for-byte plain NET/ROM).

All behind #[cfg(feature = "netrom-compress")]; feature-off stays at 724
tests, byte-identical. Feature-on adds 5 wire unit tests (742).

Co-Authored-By: Claude Code <noreply@anthropic.com>
- circuit_options: compression_enabled (default false) + proposed_timer_seconds
  (default 60), gated behind netrom-compress. Mirrors C# NetRomCircuitOptions.
- compression (NEW): the parity-named compress / try_decompress seam over the
  deflate zlib codec, with the 8 KiB fail-closed cap (MAX_DECOMPRESSED_FRAME).
  Mirrors C# NetRomCompression.

Feature-off unchanged (724); feature-on 745 (+3 adapter tests).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…o the circuit

Phase-2 protocol glue over the committed deflate codec, mirroring the C#
NetRomCircuit compression path, all behind #[cfg(feature = "netrom-compress")]:

- Negotiation (both-ends-or-off): send_connect_request offers via the extended
  17-octet form when compression_enabled; accept_inbound settles on the peer's
  offer AND local opt; send_connect_acknowledge mirrors agreement (else the
  vanilla empty ack); on_connect_acknowledge now reads the ack PAYLOAD to test
  the agree bit (payload threaded in from on_packet, gated).
- Send: send() compresses the whole logical send as one zlib stream then
  fragments; every fragment carries FLAG_COMPRESSED; per-send raw fallback when
  zlib does not shrink (flag clear -- legal, the flag is per-frame).
- Receive: on_information tracks the compressed bit from the first fragment,
  reassembles all more-follows fragments, inflates once at completion; a
  corrupt/oversized stream (8 KiB cap) is dropped fail-closed but still acked.
- CircuitManager threads the peer's offer (IncomingCircuit.offered_compression)
  from mint_inbound into accept_inbound.
- close() resets the negotiated/reassembly compression state.

Always-compiled (feature-off) changes, all behaviour-preserving and covered by
the unchanged 724 feature-off suite + the green fw cross-build:
- tick / retransmit_from retransmit loops refactored from clone-into-tuple to
  core::mem::take (lets each frame carry the gated 'compressed' field without
  cfg-forking the tuple type; also drops a per-retransmit payload clone).
- send() fragments over a 'body' binding (== 'data' when the feature is off).
The Connect-Request parser already tolerated the extended trailer, so no
always-compiled parser change was needed.

Tests (feature-on): 9 circuit-level (offer/agree/decline both directions,
compressed multi-fragment round-trip, raw fallback, disabled==plain, corrupt
fail-closed) + 2 manager-Harness integration. Feature-off 724 unchanged;
feature-on 756.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The feature is OFF by default, so the existing lint/test steps never exercise
the L4Compress codec + circuit path. Add explicit clippy + test steps for it in
the same self-hosted check job so this gated path cannot silently rot (a prior
enum break slipped through exactly this way).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant