From befec53c5d85800d9251c247163af674d45a3392 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 9 Sep 2026 16:00:18 -0400 Subject: [PATCH] test: port the property suite from #1 onto the Encode/Decode API simple_doip#1 carries 26 property tests written against the pre-`no_std` `write`/`read` API that 0.2.0 removed, on a branch whose merge base predates that migration, the error-taxonomy refactor and the 0.4.0 server break. The properties were sound; only the calls were stale. Rebasing that branch would have resolved textually and then failed to compile, so the cases are ported instead. 24 of the 26 land, all passing on the first run. The two that do not are byte roundtrips through serde, and this crate has no serde dependency to roundtrip through. They live in `tests/property.rs` rather than in `#[cfg(test)]` modules inside `src/` as the original did. proptest needs `std`, the library is `no_std`, and an integration test target gets `std` without any conditional-compilation gymnastics -- at the cost of only exercising the public API, which is all these properties touch anyway. What they check that `golden_vectors.rs` does not: the golden fixtures pin the exact bytes the crate emits, so they catch the wire format changing. These check that `encode` and `decode` agree with each other across the whole input space, which catches a field written in one order and read in another. Neither finds a misreading of the standard that both directions share symmetrically. Co-Authored-By: Claude Opus 5 (1M context) --- .github/ISSUE_TEMPLATE/bug.yml | 48 +++ .github/ISSUE_TEMPLATE/epic.yml | 13 + .github/ISSUE_TEMPLATE/task.yml | 34 ++ .github/PULL_REQUEST_TEMPLATE.md | 22 ++ .github/workflows/main.yml | 23 +- Cargo.lock | 266 ++++++++++++++ Cargo.toml | 1 + fuzz/.gitignore | 6 + fuzz/Cargo.lock | 445 ++++++++++++++++++++++++ fuzz/Cargo.toml | 47 +++ fuzz/fuzz_targets/fuzz_codec_decode.rs | 27 ++ fuzz/fuzz_targets/fuzz_message_parse.rs | 12 + fuzz/fuzz_targets/fuzz_payload_parse.rs | 16 + fuzz/fuzz_targets/fuzz_roundtrip.rs | 44 +++ tests/property.rs | 339 ++++++++++++++++++ 15 files changed, 1334 insertions(+), 9 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/epic.yml create mode 100644 .github/ISSUE_TEMPLATE/task.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/fuzz_codec_decode.rs create mode 100644 fuzz/fuzz_targets/fuzz_message_parse.rs create mode 100644 fuzz/fuzz_targets/fuzz_payload_parse.rs create mode 100644 fuzz/fuzz_targets/fuzz_roundtrip.rs create mode 100644 tests/property.rs diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..da065e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,48 @@ +name: Bug Report +description: Report a reproducible defect. +title: "[Bug]: " +labels: [bug] +body: + - type: input + id: summary + attributes: + label: Summary + description: Brief description of the bug. + placeholder: "Brief summary" + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Clear steps to make the bug happen. + placeholder: | + 1. Do thing + validations: + required: false + + - type: textarea + id: expected + attributes: + label: Expected Behavior + placeholder: "N/A" + validations: + required: false + + - type: textarea + id: actual + attributes: + label: Actual Behavior + placeholder: "N/A" + validations: + required: false + + - type: textarea + id: logs + attributes: + label: Logs or Dumps + description: Paste logs or attach relevant binary/debug dumps. + placeholder: "Include only relevant logs" + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/epic.yml b/.github/ISSUE_TEMPLATE/epic.yml new file mode 100644 index 0000000..62ea9da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yml @@ -0,0 +1,13 @@ +name: Epic +description: High-level initiative composed of multiple tasks. +title: "[Epic]: " +labels: [epic] +body: + - type: input + id: objective + attributes: + label: Objective + description: What is this epic trying to achieve? + placeholder: "Add capability" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..291fae7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,34 @@ +name: Task +description: A standalone task or implementation item. +title: "[Task]: " +labels: [task] +body: + - type: input + id: summary + attributes: + label: Summary + description: One-line description of the task. + placeholder: "Implement feature X" + validations: + required: true + + - type: textarea + id: context + attributes: + label: Context + description: Any background or links needed to understand the task. + placeholder: | + - Related issue: #123 + validations: + required: false + + - type: textarea + id: acceptance + attributes: + label: Acceptance Criteria + description: What must be true for this to be "done"? + placeholder: | + - Unit tests written + - Compiles without warnings + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..57064c7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + +## Commit Message Details + +## Issue URL + +Closes # + + + +## Testing + + diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 43b5f1f..bb55dfb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,15 +67,20 @@ jobs: # that kind still have to be marked `!` on the PR title by hand. run-semver-checks: false - # OFF until the crate has cargo-fuzz targets. `cargo fuzz build` fails - # without a `fuzz/` directory; simple_doip#1 adds four targets, and this - # flips when that lands. - run-fuzz-tests: false - - # OFF because this crate has no `prop_` tests. The filter would select - # nothing, and `cargo nextest` exits 4 on an empty selection -- so the - # job would fail rather than skip. Unit tests therefore run everything. - run-property-tests: false + # Both on as of the ported suites in `tests/property.rs` and `fuzz/`. + # + # `unit-test-filter: 'all()'` deliberately overlaps the property job + # rather than excluding `prop_`: the unit job is the one that measures + # coverage, and coverage should describe the whole suite. The property + # job re-runs the same tests under their own name for a readable signal, + # which costs a fraction of a second. + # + # Do not narrow this to a filter that can select nothing -- + # `cargo nextest` exits 4 on an empty selection, so the job fails + # rather than skips. + run-fuzz-tests: true + run-property-tests: true + property-test-filter: 'test(~prop_)' unit-test-filter: 'all()' # `thumbv7em-none-eabihf` rather than the workflow's `thumbv6m` default: diff --git a/Cargo.lock b/Cargo.lock index c0fd8fe..e997eca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,12 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "automotive-wire-codec" version = "0.3.0" @@ -28,6 +34,27 @@ dependencies = [ "embedded-io", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "bytes" version = "1.12.1" @@ -46,6 +73,28 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures" version = "0.3.34" @@ -134,6 +183,29 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "heck" version = "0.5.0" @@ -152,6 +224,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.34" @@ -184,6 +262,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -196,6 +283,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -205,6 +301,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -214,6 +335,87 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -233,6 +435,7 @@ dependencies = [ "bytes", "embedded-io", "futures", + "proptest", "strum", "thiserror", "tokio", @@ -306,6 +509,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.20" @@ -420,6 +636,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -432,12 +654,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -452,3 +692,29 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml index 5d47b84..c9d91da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ tokio-util = { version = "0.7", optional = true, features = ["net", "codec"] } [dev-dependencies] anyhow = "1" +proptest = "1" tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "io-util", "time"] } tracing-subscriber = { version = "0.3", features = ["fmt"] } diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..b5f8573 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,6 @@ +# cargo-fuzz working state: the corpus and any crash artifacts are generated, +# machine-specific, and can grow without bound. +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..7f27926 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,445 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "automotive-wire-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3619f2db27c70d93c5390ce2e90482c26c99675a85392271d2bfe07451234f6" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simple_doip" +version = "0.5.2" +dependencies = [ + "async-trait", + "automotive-wire-codec", + "bytes", + "embedded-io", + "futures", + "strum", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "simple_doip-fuzz" +version = "0.0.0" +dependencies = [ + "bytes", + "libfuzzer-sys", + "simple_doip", + "tokio-util", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..23dd36e --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "simple_doip-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +simple_doip = { path = "..", features = ["client", "server"] } +bytes = "1" +tokio-util = { version = "0.7", features = ["codec"] } + +# Its own workspace: the fuzz crate must not be built by, or share a lockfile +# with, the library it fuzzes. +[workspace] +members = ["."] + +[[bin]] +name = "fuzz_message_parse" +path = "fuzz_targets/fuzz_message_parse.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_payload_parse" +path = "fuzz_targets/fuzz_payload_parse.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_codec_decode" +path = "fuzz_targets/fuzz_codec_decode.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_roundtrip" +path = "fuzz_targets/fuzz_roundtrip.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_codec_decode.rs b/fuzz/fuzz_targets/fuzz_codec_decode.rs new file mode 100644 index 0000000..726c7e8 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_codec_decode.rs @@ -0,0 +1,27 @@ +#![no_main] + +use bytes::BytesMut; +use libfuzzer_sys::fuzz_target; +use simple_doip::message_codec::MessageCodec; +use tokio_util::codec::Decoder; + +// The tokio `Decoder`, which is where a streaming TCP connection meets the +// parser: arbitrary bytes arrive in arbitrary chunk boundaries, so the codec +// has to handle partial frames, malformed headers and truncated payloads +// without panicking. +fuzz_target!(|data: &[u8]| { + let mut codec = MessageCodec::new(); + let mut buf = BytesMut::from(data); + + // Decode repeatedly: one buffer can hold several frames, and the codec + // returns `Ok(None)` when it wants more bytes. + loop { + match codec.decode(&mut buf) { + // Decoded a frame; there may be more behind it. + Ok(Some(_message)) => {} + // Incomplete frame, or a parse error. Both are expected of + // fuzzed input; neither is a crash. + Ok(None) | Err(_) => break, + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_message_parse.rs b/fuzz/fuzz_targets/fuzz_message_parse.rs new file mode 100644 index 0000000..75ae208 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_message_parse.rs @@ -0,0 +1,12 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use simple_doip::messages::{Decode, Message}; + +// The whole-frame parser -- header plus payload -- against arbitrary bytes. +// Every input must produce `Ok` or `Err`, never a panic and never undefined +// behavior. `Message` borrows from the input buffer, so this also covers the +// zero-copy slicing that borrowing depends on. +fuzz_target!(|data: &[u8]| { + let _ = Message::decode(data); +}); diff --git a/fuzz/fuzz_targets/fuzz_payload_parse.rs b/fuzz/fuzz_targets/fuzz_payload_parse.rs new file mode 100644 index 0000000..022ecf6 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_payload_parse.rs @@ -0,0 +1,16 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use simple_doip::messages::{Payload, PayloadType}; + +// Payload decoding for an arbitrary payload type, independent of header +// validation: the first two bytes choose the `PayloadType`, the rest are the +// body. That reaches decode branches a valid header would never select -- +// a response type arriving where a request belongs, for instance. +fuzz_target!(|data: &[u8]| { + let Some((type_bytes, payload_bytes)) = data.split_at_checked(2) else { + return; + }; + let payload_type = PayloadType::from(u16::from_be_bytes([type_bytes[0], type_bytes[1]])); + let _ = Payload::decode(payload_bytes, payload_type); +}); diff --git a/fuzz/fuzz_targets/fuzz_roundtrip.rs b/fuzz/fuzz_targets/fuzz_roundtrip.rs new file mode 100644 index 0000000..b1d9db1 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_roundtrip.rs @@ -0,0 +1,44 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use simple_doip::messages::{Decode, Encode, Header, Message}; + +// The asymmetry hunt: if a frame decodes, re-encoding it and decoding that +// again must yield the same message. +// +// decode(encode(decode(bytes))) == decode(bytes) +// +// A field written in one order and read in another survives a hand-written +// round-trip test whenever both sides share the mistake, but not this -- the +// re-encoded bytes have to be acceptable to the decoder that produced them. +fuzz_target!(|data: &[u8]| { + let Ok((message, _rest)) = Message::decode(data) else { + return; + }; + + let Ok(size) = message.encoded_size() else { + return; + }; + + // Skip frames whose header declares a length the payload does not actually + // occupy. `decode` accepts those, keeps the declared length verbatim, and + // `encode` then writes it beside a payload of the real size -- emitting a + // frame no decoder will accept. That is luminartech/simple_doip#15, not an + // asymmetry in the field order this target is hunting for, and the check + // comes out when #15 is fixed. + if message.header.payload_length as usize != size - Header::SIZE { + return; + } + let mut encoded = vec![0u8; size]; + { + let mut writer: &mut [u8] = &mut encoded; + if message.encode(&mut writer).is_err() { + return; + } + } + + let (reparsed, rest) = + Message::decode(&encoded).expect("re-decoding an encoded message must not fail"); + assert!(rest.is_empty(), "re-encoding left trailing bytes"); + assert_eq!(message, reparsed, "round trip changed the message"); +}); diff --git a/tests/property.rs b/tests/property.rs new file mode 100644 index 0000000..9683b4e --- /dev/null +++ b/tests/property.rs @@ -0,0 +1,339 @@ +//! Property tests over the wire format: every byte-coded enum survives a byte +//! conversion round trip, and every message type survives an encode/decode +//! round trip, for arbitrary inputs rather than the handful of values a +//! table-driven test happens to name. +//! +//! These complement `golden_vectors.rs` rather than duplicating it. The golden +//! vectors pin the exact bytes the crate emits, so they catch a change to the +//! wire format; these check that `encode` and `decode` agree with each other +//! across the whole input space, which catches a field written in one order and +//! read in another. Neither finds a misreading of the standard that `encode` +//! and `decode` share symmetrically -- that is what the golden fixtures are for. +//! +//! The cases here were written by `@gavin-dunlap-luminar` in `simple_doip#1` +//! against the pre-`no_std` `write`/`read` API that 0.2.0 removed. The +//! properties are his; the calls are rewritten for `Encode`/`Decode`. + +use proptest::prelude::*; +use simple_doip::{ + LogicalAddress, + messages::{ + ActivationTypeCode, AliveCheckResponse, Decode, DiagnosticAckCode, DiagnosticMessage, + DiagnosticMessageAck, DiagnosticPowerModeCode, Encode, EntityStatusNodeType, + EntityStatusResponse, FurtherActionRequired, Header, Message, MessageError, NackCode, + Payload, PayloadType, ProtocolVersion, RoutingActivationRequest, RoutingActivationResponse, + RoutingActivationResponseCode, VehicleIdentificationResponse, VinGidSyncStatus, + }, +}; + +/// Encode into a fixed buffer the way a bare-metal caller would -- a `&mut [u8]` +/// is the `embedded_io::Write` this crate is built around -- and return the +/// written prefix. Also asserts `encoded_size()` agrees with what `encode` +/// wrote, since a closed-form size override that drifts from its `encode` +/// corrupts the header's `payload_length` silently. +fn encode_to<'buf>(value: &impl Encode, buf: &'buf mut [u8]) -> &'buf [u8] { + let written = { + let mut writer: &mut [u8] = buf; + value.encode(&mut writer).expect("encode failed") + }; + assert_eq!( + value.encoded_size().expect("encoded_size failed"), + written, + "encoded_size() disagrees with encode()" + ); + &buf[..written] +} + +/// `byte -> enum -> byte` for every enum whose wire form is a single byte. +/// A variant that discards the byte it did not recognise fails here. +macro_rules! byte_code_roundtrip { + ($name:ident, $ty:ty) => { + proptest! { + #[test] + fn $name(byte in any::()) { + let decoded = <$ty>::from(byte); + let back: u8 = decoded.into(); + prop_assert_eq!(byte, back); + } + } + }; +} + +byte_code_roundtrip!(prop_protocol_version_roundtrip, ProtocolVersion); +byte_code_roundtrip!(prop_activation_type_roundtrip, ActivationTypeCode); +byte_code_roundtrip!(prop_diagnostic_ack_code_roundtrip, DiagnosticAckCode); +byte_code_roundtrip!(prop_entity_node_type_roundtrip, EntityStatusNodeType); +byte_code_roundtrip!(prop_further_action_roundtrip, FurtherActionRequired); +byte_code_roundtrip!(prop_nack_code_roundtrip, NackCode); +byte_code_roundtrip!(prop_power_mode_code_roundtrip, DiagnosticPowerModeCode); +byte_code_roundtrip!( + prop_routing_response_code_roundtrip, + RoutingActivationResponseCode +); +byte_code_roundtrip!(prop_vin_gid_sync_roundtrip, VinGidSyncStatus); + +proptest! { + /// `PayloadType` is the one two-byte code. + #[test] + fn prop_payload_type_roundtrip(value in any::()) { + let decoded = PayloadType::from(value); + let back: u16 = decoded.into(); + prop_assert_eq!(value, back); + } + + /// ISO 13400-2 reserves the tester address range `0x0E00..=0x0FFF`. + #[test] + fn prop_logical_address_client_range(addr in 0x0E00u16..=0x0FFF) { + prop_assert!(LogicalAddress(addr).is_valid_client_address()); + } + + /// Everything outside that range is not a tester address. + #[test] + fn prop_logical_address_outside_client_range( + addr in prop_oneof![0x0000u16..0x0E00, 0x1000u16..=0xFFFF], + ) { + prop_assert!(!LogicalAddress(addr).is_valid_client_address()); + } + + #[test] + fn prop_header_roundtrip( + version in any::().prop_map(ProtocolVersion::from), + payload_type in any::().prop_map(PayloadType::from), + payload_length in any::(), + ) { + let header = Header::new(version, payload_type, payload_length); + let mut buf = [0u8; Header::SIZE]; + let bytes = encode_to(&header, &mut buf); + prop_assert_eq!(bytes.len(), Header::SIZE); + + let parsed = Header::decode_exact(bytes).expect("header decode failed"); + prop_assert_eq!(header, parsed); + } + + #[test] + fn prop_alive_check_response_roundtrip(source in any::()) { + let value = AliveCheckResponse { source_address: LogicalAddress(source) }; + let mut buf = [0u8; 16]; + let bytes = encode_to(&value, &mut buf); + let parsed = AliveCheckResponse::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + #[test] + fn prop_diagnostic_message_roundtrip( + source in any::(), + target in any::(), + user_data in prop::collection::vec(any::(), 0..64), + ) { + let value = DiagnosticMessage { + source_address: LogicalAddress(source), + target_address: LogicalAddress(target), + user_data: &user_data, + }; + let mut buf = [0u8; 128]; + let bytes = encode_to(&value, &mut buf); + let parsed = DiagnosticMessage::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + #[test] + fn prop_diagnostic_message_ack_roundtrip( + source in any::(), + target in any::(), + ack_code in any::().prop_map(DiagnosticAckCode::from), + previous in prop::collection::vec(any::(), 0..64), + ) { + let value = DiagnosticMessageAck { + source_address: LogicalAddress(source), + target_address: LogicalAddress(target), + ack_code, + previous_message_data: &previous, + }; + let mut buf = [0u8; 128]; + let bytes = encode_to(&value, &mut buf); + let parsed = DiagnosticMessageAck::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + #[test] + fn prop_entity_status_response_roundtrip( + node_type in any::().prop_map(EntityStatusNodeType::from), + max_concurrent_tcp_sockets in any::(), + open_tcp_sockets in any::(), + max_data_size in any::(), + ) { + let value = EntityStatusResponse { + node_type, + max_concurrent_tcp_sockets, + open_tcp_sockets, + max_data_size, + }; + let mut buf = [0u8; 32]; + let bytes = encode_to(&value, &mut buf); + let parsed = EntityStatusResponse::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + /// Both shapes of the request: with and without the optional + /// vehicle-manufacturer field, which is the length difference the decoder + /// has to infer. + #[test] + fn prop_routing_activation_request_roundtrip( + source in any::(), + activation_type in any::().prop_map(ActivationTypeCode::from), + reserved in any::<[u8; 4]>(), + vehicle_manufacturer in proptest::option::of(any::<[u8; 4]>()), + ) { + let value = RoutingActivationRequest { + source_address: LogicalAddress(source), + activation_type, + reserved, + reserved_vehicle_manufacturer: vehicle_manufacturer, + }; + let mut buf = [0u8; 32]; + let bytes = encode_to(&value, &mut buf); + let parsed = RoutingActivationRequest::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + #[test] + fn prop_routing_activation_response_roundtrip( + tester in any::(), + entity in any::(), + code in any::().prop_map(RoutingActivationResponseCode::from), + reserved_oem in any::<[u8; 4]>(), + oem_specific in proptest::option::of(any::<[u8; 4]>()), + ) { + let value = RoutingActivationResponse { + logical_address_tester: LogicalAddress(tester), + logical_address_of_doip_entity: LogicalAddress(entity), + routing_activation_response_code: code, + reserved_oem, + oem_specific, + }; + let mut buf = [0u8; 32]; + let bytes = encode_to(&value, &mut buf); + let parsed = RoutingActivationResponse::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + /// `group_id` is optional on the wire, so both shapes go through the same + /// property rather than needing a test each. + #[test] + fn prop_vehicle_identification_response_roundtrip( + vin in any::<[u8; 17]>(), + logical_address in any::(), + entity_id in any::<[u8; 6]>(), + group_id in proptest::option::of(any::<[u8; 6]>()), + further_action in any::().prop_map(FurtherActionRequired::from), + vin_gid_sync_status in any::().prop_map(VinGidSyncStatus::from), + ) { + let value = VehicleIdentificationResponse { + vin, + logical_address: LogicalAddress(logical_address), + entity_id, + group_id, + further_action, + vin_gid_sync_status, + }; + let mut buf = [0u8; 64]; + let bytes = encode_to(&value, &mut buf); + let parsed = VehicleIdentificationResponse::decode_exact(bytes).expect("decode failed"); + prop_assert_eq!(value, parsed); + } + + /// A whole frame -- header plus body -- through `Message`, which is what a + /// peer actually puts on the wire. + #[test] + fn prop_full_nack_frame_roundtrip(code in any::().prop_map(NackCode::from)) { + let payload = Payload::DoIPNack(code); + let mut buf = [0u8; 32]; + let bytes = frame(ProtocolVersion::V2012, PayloadType::NegativeAcknowledge, &payload, &mut buf); + + let (parsed, rest) = Message::decode(bytes).expect("message decode failed"); + prop_assert!(rest.is_empty()); + prop_assert_eq!(parsed.header.payload_type, PayloadType::NegativeAcknowledge); + prop_assert_eq!(parsed.payload, payload); + } + + #[test] + fn prop_full_diagnostic_frame_roundtrip( + source in any::(), + target in any::(), + user_data in prop::collection::vec(any::(), 0..64), + ) { + let payload = Payload::DiagnosticMessage(DiagnosticMessage { + source_address: LogicalAddress(source), + target_address: LogicalAddress(target), + user_data: &user_data, + }); + let mut buf = [0u8; 128]; + let bytes = frame(ProtocolVersion::V2012, PayloadType::DiagnosticMessage, &payload, &mut buf); + + let (parsed, rest) = Message::decode(bytes).expect("message decode failed"); + prop_assert!(rest.is_empty()); + prop_assert_eq!(parsed.payload, payload); + } + + #[test] + fn prop_full_alive_check_frame_roundtrip(source in any::()) { + let payload = Payload::AliveCheckResponse(AliveCheckResponse { + source_address: LogicalAddress(source), + }); + let mut buf = [0u8; 32]; + let bytes = frame(ProtocolVersion::V2012, PayloadType::AliveCheckResponse, &payload, &mut buf); + + let (parsed, rest) = Message::decode(bytes).expect("message decode failed"); + prop_assert!(rest.is_empty()); + prop_assert_eq!(parsed.payload, payload); + } + + #[test] + fn prop_full_routing_activation_request_frame_roundtrip( + source in any::(), + activation_type in any::().prop_map(ActivationTypeCode::from), + reserved in any::<[u8; 4]>(), + vehicle_manufacturer in proptest::option::of(any::<[u8; 4]>()), + ) { + let payload = Payload::RoutingActivationRequest(RoutingActivationRequest { + source_address: LogicalAddress(source), + activation_type, + reserved, + reserved_vehicle_manufacturer: vehicle_manufacturer, + }); + let mut buf = [0u8; 64]; + let bytes = frame( + ProtocolVersion::V2012, + PayloadType::RoutingActivationRequest, + &payload, + &mut buf, + ); + + let (parsed, rest) = Message::decode(bytes).expect("message decode failed"); + prop_assert!(rest.is_empty()); + prop_assert_eq!(parsed.payload, payload); + } +} + +/// Header (with `payload_length` taken from the body's `encoded_size`) followed +/// by the body, which is the frame layout ISO 13400-2 specifies. +fn frame<'buf>( + protocol_version: ProtocolVersion, + payload_type: PayloadType, + payload: &Payload<'_>, + buf: &'buf mut [u8], +) -> &'buf [u8] { + let payload_len = payload.encoded_size().expect("payload encoded_size failed"); + let header = Header::new( + protocol_version, + payload_type, + u32::try_from(payload_len).expect("payload fits in u32"), + ); + let written = { + let mut writer: &mut [u8] = buf; + header.encode(&mut writer).expect("header encode failed") + + payload.encode(&mut writer).expect("payload encode failed") + }; + assert_eq!(written, Header::SIZE + payload_len, "frame size disagrees"); + &buf[..written] +}