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] +}