The Fuzz workflow step was still iterating the old v0.1 target names (parse_radio_config, cobs_decoder_feed, cobs_roundtrip, command_roundtrip, cobs_decoder_multi_frame, cobs_chunk_invariant). After the Protocol v2 rewrite only five targets exist: decoder_feed, frame_roundtrip, lora_config_roundtrip, parse_command, parse_device_message. Update the workflow to match.
GitHub renamed the repo from donglora/protocol to donglora/protocol-rs (the Rust implementation is now distinguished from a forthcoming protocol-py sibling). Update Cargo.toml repository/homepage and README badge / link URLs so the published 1.0.0 crate points at the new location.
The wire protocol is re-specified as DongLoRa Protocol v2 (see PROTOCOL.md). Every type_id, tag-correlation rule, and field layout is incompatible with 0.1.0.
* Add PROTOCOL.md as the normative spec for the v2 wire format.
* Replace framing.rs with a streaming FrameDecoder that emits (type_id, tag, payload) triples on each COBS-delimited, CRC-checked frame.
* Split the single-file lib.rs into dedicated modules: frame, commands, events, errors, modulation, info, crc, chip_id. Re-exports preserve convenience imports at the crate root.
* Replace RadioConfig / Bandwidth with a Modulation enum covering LoRa, FSK/GFSK, LR-FHSS, and FLRC, each with its own typed parameter struct.
* Add TxFlags, RxOrigin, SetConfigResult, OkPayload to model v2 response shapes, and RadioChipId for GET_INFO.
* Rewrite fuzz corpus: replace the 4 cobs_* / command_roundtrip targets with decoder_feed, frame_roundtrip, lora_config_roundtrip, and parse_device_message; drop parse_radio_config.
* Expand proptest and mutation-killer coverage for the new wire shape.
* Rewrite canonical-byte test vectors against the v2 spec.
cargo-fuzz defaults to x86_64-unknown-linux-musl on Linux so ASAN can
statically link, but dtolnay/rust-toolchain@nightly doesn't ship a
musl std and GH runners don't have it preinstalled — build failed with
'can't find crate for core' on byteorder. Pinning both fuzz build and
fuzz run to the gnu target uses the std that comes with the toolchain
and works out of the box.
- Cargo.toml: add documentation and readme fields; expand the exclude
list so deny.toml, justfile, mise.toml, .github, .jj, fuzz, and any
cargo-mutants output never ship in the published tarball.
- README: badges, install snippet for both feature shapes, testing
matrix.
- CHANGELOG.md: keep-a-changelog skeleton with a 0.1.0 entry.
- Revert framing.rs let-chain back to nested if-let so the published
MSRV holds at the edition-2024 floor (1.85). Behaviour unchanged.
cargo publish --dry-run packages cleanly.
Eight parallel jobs on push/PR:
- test × 3 (default / no-default / all-features) via nextest + doc
- clippy × 3 feature permutations, -D warnings
- fmt
- doc (all-features, -D warnings on rustdoc)
- deny (cargo-deny action)
- miri (nightly, lib + vectors under default + no-default)
- fuzz (nightly, 60 s smoke on every target)
- mutants × 2 feature configs (same equivalence exclusions as local)
Swatinem/rust-cache on every cargo job; taiki-e/install-action for
nextest / cargo-fuzz / cargo-mutants. Uploads mutants.out on failure.
Drops test-fuzz-long from the justfile — 60 s per target in CI every
push plus the local proptest 1M-case suite already covers this.
test-mutants now runs cargo-mutants twice — once with defaults (defmt
derives present) and once with --no-default-features (derives absent)
— so feature-gated paths are covered in both shapes. Same three
equivalent mutants are excluded in both runs.
- command_roundtrip: structure-aware (via arbitrary crate) Command
encode → parse → equal. Symmetric to cobs_roundtrip.
- cobs_decoder_multi_frame: chunked feeding with arbitrary split sizes;
asserts bounded payload and that command count never exceeds sentinel
count.
- cobs_chunk_invariant: same stream through two decoders (chunked vs
all-at-once) must produce the same command sequence.
Adds test-fuzz-long recipe (600s per target, ~1h 10m total) for CI
cron / pre-release. All seven targets smoke-tested at 100k runs each
with zero findings.
Scaled the proptest suite hard and added 13 new properties:
- cobs_decoder_multi_frame: N frames decode to N commands in order.
- cobs_decoder_chunk_invariant: arbitrary chunk splits yield the same
sequence as feed-all-at-once.
- cobs_decoder_byte_by_byte_equivalent: stronger form of chunking.
- cobs_decoder_recovers_after_garbage: decoder is not poisoned by a
leading garbage frame.
- cobs_decoder_ignores_sentinel_runs: arbitrary leading/trailing 0x00
runs do not affect decoding.
- cobs_decoder_oversized_discards: frames > MAX_FRAME are discarded;
subsequent frames still decode.
- response_full_roundtrip: encode + COBS + decode + re-parse yields the
original Response (via a test-only decode_response helper).
- response_encoding_deterministic: byte-identical output across runs.
- cobs_encoded_has_single_trailing_zero: COBS invariant check.
- bandwidth_u8_is_bijection: sweeps the whole u8 space.
- radio_config_write_to_stays_in_bounds: buffer-write sanity.
- resolve_then_validate_consistent: cross-field contract.
Split into two proptest! blocks: lightweight at 100,000 cases each, and
heavyweight (MAX_FRAME-sized buffers, multi-frame streams) at 10,000.
Total ~1.13M test cases per suite run, finishing in ~20 s.
Adds tests/mutation_killers.rs with six targeted tests derived from
cargo-mutants findings: cad_enabled true/false paths, Transmit
with-config boundary, CobsDecoder::reset behaviour, empty-frame
handling, and the cobs_encode_response success path.
Three mutants remain as provably equivalent — the justfile excludes
them by exact regex so any genuinely new mutant on the same line will
still surface. Updates test-miri to also run the integration tests
(proptest stays skipped because its file-persistence layer calls
getcwd). Gitignore picks up mutants.out / mutants.out.old.
cargo mutants: 89 caught, 9 unviable, 0 missed.
Hand-encoded byte sequences for RadioConfig, every Command tag, and
every Response tag, taken directly from the spec. Fields use hex-clean
values (freq_hz = 0x12345678, sync_word = 0xABCD, etc.) so each
little-endian byte is visually verifiable.
Locks firmware and host to the written spec — a refactor that quietly
changes a field layout fails here, not in the field.
Four libFuzzer targets:
- parse_command: Command::from_bytes never panics; bounded payload.
- parse_radio_config: RadioConfig::from_bytes never panics.
- cobs_decoder_feed: CobsDecoder::feed is panic-free across arbitrary
byte streams split into arbitrary chunks.
- cobs_roundtrip: structure-aware (via arbitrary crate) Response →
cobs_encode_response → ucobs::decode yields identical raw bytes.
All four smoke-tested at 50k runs each with no findings. Intended CI
cadence is a weekly cron with a larger time budget; per-PR would be
expensive. Requires nightly + cargo-fuzz.
Ten properties covering: RadioConfig roundtrip and validate/resolve
determinism; no-panic on arbitrary bytes for both Command::from_bytes
and RadioConfig::from_bytes; bounded-capacity for Transmit payloads
(guards the heapless::Vec size); Command roundtrip via a test-only
encoder (the library only decodes commands); COBS reversibility; and
two streaming CobsDecoder properties (no-panic on random byte streams
with random chunk boundaries, and a full encode→feed→decode roundtrip).
Adds Clone to Command and Response — benign public-API expansion that
unblocks proptest's Just strategy.
Imports CobsDecoder and cobs_encode_response from firmware's
src/host/framing.rs, adjusted so the defmt::warn on encode overflow is
gated on the crate's defmt feature rather than an unconditional import.
The route_command helper stays in firmware — it depends on embassy
channels, which have no business in a transport-agnostic crate.
Copied src/protocol.rs from firmware verbatim, added crate-level #![no_std],
migrated the defmt::Format derive gate from cfg_attr(not(test), ...) to
cfg_attr(feature = "defmt", ...) so host-side (no-default-features) builds
compile cleanly. Tests unchanged; all 35 pass.
Firmware still owns the file for now — the re-export swap lands in a
follow-up firmware commit to keep each change standalone.