diff --git a/.gitignore b/.gitignore index 6fa0f2a8..71f87bff 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ src/board/mod.rs __pycache__/ .hypothesis/ examples/meshcore/corpus/generated/ +crates/donglora-cobs/fuzz/corpus/ +crates/donglora-cobs/fuzz/artifacts/ diff --git a/Cargo.lock b/Cargo.lock index beb6236a..e608f194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,21 @@ dependencies = [ "serde", ] +[[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 = "bitfield" version = "0.13.2" @@ -367,7 +382,8 @@ name = "donglora-cobs" version = "0.1.0" dependencies = [ "corncobs", - "rand", + "proptest", + "rand 0.8.5", ] [[package]] @@ -485,7 +501,7 @@ dependencies = [ "nrf5340-net-pac", "nrf9120-pac", "nrf9160-pac", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -742,6 +758,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[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 = "esp-backtrace" version = "0.14.2" @@ -813,7 +839,7 @@ dependencies = [ "nb 1.1.0", "paste", "portable-atomic", - "rand_core", + "rand_core 0.6.4", "serde", "strum", "ufmt-write", @@ -918,6 +944,12 @@ dependencies = [ "vcell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fixed" version = "1.29.0" @@ -945,6 +977,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fugit" version = "0.3.9" @@ -1000,6 +1038,31 @@ dependencies = [ "wasi", ] +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "half" version = "2.7.1" @@ -1029,6 +1092,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1051,6 +1123,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1065,14 +1143,34 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litrs" version = "0.4.2" @@ -1383,6 +1481,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -1447,6 +1555,25 @@ 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 2.11.0", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "pulldown-cmark" version = "0.11.3" @@ -1458,6 +1585,12 @@ dependencies = [ "unicase", ] +[[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.45" @@ -1467,6 +1600,18 @@ 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 = "r0" version = "1.0.0" @@ -1486,8 +1631,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1497,7 +1652,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[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 0.9.5", ] [[package]] @@ -1506,9 +1671,33 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] +[[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 0.9.5", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "riscv" version = "0.12.1" @@ -1556,7 +1745,20 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -1565,6 +1767,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[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 = "scopeguard" version = "1.2.0" @@ -1586,6 +1800,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "semver-parser" version = "0.7.0" @@ -1622,6 +1842,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1730,6 +1963,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.2", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1812,6 +2058,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.9.0" @@ -1824,6 +2076,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "usb-device" version = "0.3.2" @@ -1898,12 +2156,73 @@ dependencies = [ "vcell", ] +[[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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver 1.0.27", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1937,6 +2256,94 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "xtensa-lx" version = "0.9.0" @@ -1998,3 +2405,9 @@ dependencies = [ "quote", "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/donglora-cobs/Cargo.toml b/crates/donglora-cobs/Cargo.toml index fd78f909..4ef849f8 100644 --- a/crates/donglora-cobs/Cargo.toml +++ b/crates/donglora-cobs/Cargo.toml @@ -10,4 +10,5 @@ default = [] [dev-dependencies] corncobs = "0.1" +proptest = "1" rand = "0.8" diff --git a/crates/donglora-cobs/README.md b/crates/donglora-cobs/README.md index 4a45afe5..d876e5c7 100644 --- a/crates/donglora-cobs/README.md +++ b/crates/donglora-cobs/README.md @@ -21,11 +21,21 @@ benchmark-optimized for streaming applications. ## Testing ```sh +# Unit tests + property tests + interop cargo test -p donglora-cobs + +# Fuzz testing (requires cargo-fuzz: cargo install cargo-fuzz) +cd crates/donglora-cobs +cargo +nightly fuzz run fuzz_decode -- -max_total_time=60 +cargo +nightly fuzz run fuzz_roundtrip -- -max_total_time=60 ``` Tests include: - 18 canonical COBS vectors (Wikipedia / IEEE/ACM spec) - Round-trip verification for various payload sizes -- 8 interop tests cross-validating against `corncobs` (dev-dependency) +- 8 interop tests cross-validating against `corncobs` - 500 randomized round-trip payloads verified against `corncobs` +- **5 proptest properties** (~1280 random inputs): round-trip, interop + encode/decode both directions, length bounds, decode-never-panics +- **2 fuzz targets**: `fuzz_decode` (arbitrary bytes → must not panic) + and `fuzz_roundtrip` (encode→decode must match) diff --git a/crates/donglora-cobs/fuzz/Cargo.toml b/crates/donglora-cobs/fuzz/Cargo.toml new file mode 100644 index 00000000..4ca8ffac --- /dev/null +++ b/crates/donglora-cobs/fuzz/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "donglora-cobs-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +donglora-cobs = { path = ".." } + +[[bin]] +name = "fuzz_decode" +path = "fuzz_targets/fuzz_decode.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_roundtrip" +path = "fuzz_targets/fuzz_roundtrip.rs" +test = false +doc = false diff --git a/crates/donglora-cobs/fuzz/fuzz_targets/fuzz_decode.rs b/crates/donglora-cobs/fuzz/fuzz_targets/fuzz_decode.rs new file mode 100644 index 00000000..83e7e314 --- /dev/null +++ b/crates/donglora-cobs/fuzz/fuzz_targets/fuzz_decode.rs @@ -0,0 +1,9 @@ +//! Fuzz target: feed arbitrary bytes to decode(). Must never panic. +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let mut buf = [0u8; 2048]; + // Must not panic — may return None on invalid input. + let _ = donglora_cobs::decode(data, &mut buf); +}); diff --git a/crates/donglora-cobs/fuzz/fuzz_targets/fuzz_roundtrip.rs b/crates/donglora-cobs/fuzz/fuzz_targets/fuzz_roundtrip.rs new file mode 100644 index 00000000..6f0f45c2 --- /dev/null +++ b/crates/donglora-cobs/fuzz/fuzz_targets/fuzz_roundtrip.rs @@ -0,0 +1,30 @@ +//! Fuzz target: encode then decode arbitrary input. Output must match input. +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if data.len() > 1024 { + return; // skip huge inputs to keep fuzzing fast + } + + let max_enc = donglora_cobs::max_encoded_len(data.len()) + 1; + let mut enc_buf = vec![0u8; max_enc]; + + let enc_len = match donglora_cobs::encode(data, &mut enc_buf) { + Some(n) => n, + None => return, // encode failed (buffer too small shouldn't happen, but skip) + }; + let encoded = &enc_buf[..enc_len]; + + // Encoded output must never contain 0x00. + assert!(!encoded.contains(&0x00), "encoded contains sentinel"); + + let mut dec_buf = vec![0u8; data.len() + 1]; + let dec_len = donglora_cobs::decode(encoded, &mut dec_buf) + .expect("decode failed after successful encode"); + + assert_eq!( + &dec_buf[..dec_len], data, + "round-trip mismatch: encode→decode produced different output" + ); +}); diff --git a/crates/donglora-cobs/src/lib.rs b/crates/donglora-cobs/src/lib.rs index 0a9fbd22..26eaee35 100644 --- a/crates/donglora-cobs/src/lib.rs +++ b/crates/donglora-cobs/src/lib.rs @@ -486,3 +486,72 @@ mod tests { } } } + +// ── Property-based tests (proptest) ───────────────────────────── + +#[cfg(test)] +mod proptests { + extern crate alloc; + use alloc::vec; + use alloc::vec::Vec; + use super::*; + use proptest::prelude::*; + + // Round-trip: encode then decode must recover the original. + proptest! { + #[test] + fn roundtrip_any(data in proptest::collection::vec(any::(), 0..1024)) { + let mut enc_buf = vec![0u8; max_encoded_len(data.len()) + 1]; + let enc_len = encode(&data, &mut enc_buf).unwrap(); + let encoded = &enc_buf[..enc_len]; + + // Encoded output must never contain 0x00. + prop_assert!(!encoded.contains(&0x00)); + + let mut dec_buf = vec![0u8; data.len() + 1]; + let dec_len = decode(encoded, &mut dec_buf).unwrap(); + prop_assert_eq!(&dec_buf[..dec_len], &data[..]); + } + + // Our encode → corncobs decode must match. + #[test] + fn interop_our_encode_their_decode(data in proptest::collection::vec(any::(), 0..1024)) { + let mut enc_buf = vec![0u8; max_encoded_len(data.len()) + 2]; + let enc_len = encode(&data, &mut enc_buf).unwrap(); + // Append sentinel for corncobs. + enc_buf[enc_len] = 0x00; + + let mut dec_buf = vec![0u8; data.len() + 1]; + let dec_len = corncobs::decode_buf(&enc_buf[..enc_len + 1], &mut dec_buf).unwrap(); + prop_assert_eq!(&dec_buf[..dec_len], &data[..]); + } + + // corncobs encode → our decode must match. + #[test] + fn interop_their_encode_our_decode(data in proptest::collection::vec(any::(), 0..1024)) { + let mut enc_buf = vec![0u8; corncobs::max_encoded_len(data.len())]; + let enc_len = corncobs::encode_buf(&data, &mut enc_buf); + // Strip trailing sentinel for our decoder. + let data_end = enc_buf[..enc_len].iter().rposition(|&b| b != 0x00).map_or(0, |i| i + 1); + + let mut dec_buf = vec![0u8; data.len() + 1]; + let dec_len = decode(&enc_buf[..data_end], &mut dec_buf).unwrap(); + prop_assert_eq!(&dec_buf[..dec_len], &data[..]); + } + + // Encoded length is always within the theoretical maximum. + #[test] + fn encoded_length_bounded(data in proptest::collection::vec(any::(), 0..1024)) { + let mut enc_buf = vec![0u8; max_encoded_len(data.len()) + 1]; + let enc_len = encode(&data, &mut enc_buf).unwrap(); + prop_assert!(enc_len <= max_encoded_len(data.len())); + } + + // Decoding random garbage must never panic (returns None on invalid input). + #[test] + fn decode_never_panics(data in proptest::collection::vec(any::(), 0..512)) { + let mut dec_buf = vec![0u8; data.len() + 256]; + let _ = decode(&data, &mut dec_buf); // may return None, must not panic + } + } +}