Fuzzing

fuzz/ holds cargo-fuzz targets for the code that reads input the collector does not control:

TargetInputChecks
decode_modelA bundled model ID and arbitrary registersdecode_model_labeled does not panic, agrees with decode_model, names each point once, labels only existing points, and returns only finite floats
model_chainAn arbitrary register map at the configured or a standard basediscover_models ends with a chain or an error; an accepted chain starts with SunS, has at most 256 contiguous models and ends with 0xFFFF; each model it finds decodes without panicking
configAn arbitrary config file, loaded as TOML and as JSONCollectorConfig::from_file_contents, validate() and device_config() return errors instead of panicking

discover_models reads through the ReadRegisters trait, which the target implements with an in-memory register map, so the chain walk runs without a network. The config target skips inputs that could name a *_file setting, since loading reads those files.

fuzz/ is its own workspace (it needs nightly Rust and sanitizers), so the workspace commands above do not build it. It has no committed lockfile: builds copy the workspace's Cargo.lock into fuzz/ first, so the targets use the same dependency versions as the collector, and only one lockfile needs updates. To run a target locally:

rustup toolchain install nightly
cargo install cargo-fuzz --locked
cp Cargo.lock fuzz/                          # build against the workspace's locked versions
fuzz/seeds.py fuzz/seeds                     # seed corpora from fixtures/ and the example config
cargo +nightly fuzz run -O decode_model fuzz/corpus/decode_model fuzz/seeds/decode_model \
  -- -max_total_time=300

A crash is saved under fuzz/artifacts/<target>/; cargo +nightly fuzz run -O <target> <file> replays it and cargo +nightly fuzz tmin -O <target> <file> shrinks it. Turn a fixed crash into a regression test in the crate's tests (see values_that_are_not_finite_are_null in crates/sunspec-parser/tests/decode_tests.rs, found by decode_model).

In CI, .github/workflows/fuzz.yml uses ClusterFuzzLite: it builds the targets in the OSS-Fuzz Rust image (.clusterfuzzlite/) with AddressSanitizer and seed corpora from fuzz/seeds.py. It fuzzes for two minutes on pull requests that change the fuzzed code (crates/sunspec-parser/, crates/poller-actor/, crates/collector-app/src/config.rs) or the fuzzing setup (fuzz/, .clusterfuzzlite/, the workflow), and for half an hour every Sunday, which also covers dependency updates; the corpus is kept between runs as workflow artifacts. A crash fails the run and is uploaded with its input. When you add a target, add a [[bin]] to fuzz/Cargo.toml, add it to targets in .clusterfuzzlite/build.sh, add the code it fuzzes to the paths in fuzz.yml, and give it seeds in fuzz/seeds.py if the fixtures have useful input.