netrom: LinBPQ L4 payload compression (compact hand-rolled zlib, feature-gated) - #68
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the BPQ
L4Compressinterop feature (Table B follow-up, decided In), default-off, behind thenetrom-compresscargo 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/NetRomCompressionbyte-for-byte):Compressed=0x10on Information frames; extended Connect-Request (17-octet, offer bit0x40) / 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::takeretransmit refactor; the connect-request parser already tolerated the extended trailer). Feature-ON: 756 tests (+19 L4 + 13 codec). clippy clean both ways, fw--lockedgreen, 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