diff --git a/.github/buildomat/jobs/bench.sh b/.github/buildomat/jobs/bench.sh new file mode 100644 index 0000000..d5cf10a --- /dev/null +++ b/.github/buildomat/jobs/bench.sh @@ -0,0 +1,18 @@ +#!/bin/bash +#: +#: name = "bench" +#: variety = "basic" +#: target = "helios-2.0" +#: rust_toolchain = "stable" +#: output_rules = [ ] +#: + +set -o errexit +set -o pipefail +set -o xtrace + +cargo --version +rustc --version + +banner bench +cargo bench diff --git a/.github/buildomat/jobs/build-and-test.sh b/.github/buildomat/jobs/build-and-test.sh index 4737918..e73f384 100644 --- a/.github/buildomat/jobs/build-and-test.sh +++ b/.github/buildomat/jobs/build-and-test.sh @@ -21,4 +21,5 @@ cargo fmt -- --check cargo clippy -- --deny warnings banner test -cargo test +cargo install cargo-nextest +cargo nextest run --release diff --git a/Cargo.lock b/Cargo.lock index caac35c..1205e6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,778 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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", + "wasip2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] [[package]] name = "poptrie" version = "0.1.0" +dependencies = [ + "criterion", + "poptrie-test-util", + "proptest", +] + +[[package]] +name = "poptrie-test-util" +version = "0.1.0" +dependencies = [ + "poptrie", + "proptest", +] + +[[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.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +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.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[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 = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +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 = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[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", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/Cargo.toml b/Cargo.toml index 5047484..b8c20ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "test-util"] + [package] name = "poptrie" version = "0.1.0" @@ -6,3 +9,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +[dev-dependencies] +criterion = "0.5" +proptest = "1" +poptrie-test-util = { path = "test-util", features = ["proptest"] } + +[[bench]] +name = "lookup" +harness = false diff --git a/README.md b/README.md index 3e3d89f..4bc9ff1 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,19 @@ Poptrie was created by Asai and Ohara in: This is a dependency free `no_std` implementation to facilitate use in OS kernels. -**This is a work in progress implementation.** +Performance analysis versus a naive LPM table is the following for a single +lookup. The naive lookup scales linearly and quickly exceeds the realm of +acceptable performance for data plane networking while the poptrie has near +constant scaling. + +``` +┌────────┬─────────┬────────┬─────────┐ +│ Routes │ Poptrie │ Naive │ Speedup │ +├────────┼─────────┼────────┼─────────┤ +│ 100 │ 5.9 ns │ 426 ns │ 72x │ +├────────┼─────────┼────────┼─────────┤ +│ 1,000 │ 5.9 ns │ 4.9 µs │ 830x │ +├────────┼─────────┼────────┼─────────┤ +│ 10,000 │ 10.3 ns │ 32 µs │ 3,100x │ +└────────┴─────────┴────────┴─────────┘ +```` diff --git a/benches/lookup.rs b/benches/lookup.rs new file mode 100644 index 0000000..1958563 --- /dev/null +++ b/benches/lookup.rs @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2026 Oxide Computer Company + +use criterion::{ + black_box, criterion_group, criterion_main, BenchmarkId, Criterion, +}; +use poptrie::Poptrie; +use poptrie_test_util::{generate_addrs, generate_table, longest_match_v4}; + +fn bench_lookup(c: &mut Criterion) { + let mut group = c.benchmark_group("lookup"); + + for size in [100, 500, 1000, 5000, 10000] { + let table = generate_table(size); + let pt = Poptrie::from(table.clone()); + let addrs = generate_addrs(1000); + + group.bench_with_input( + BenchmarkId::new("poptrie", size), + &size, + |b, _| { + b.iter(|| { + for addr in &addrs { + black_box(pt.match_v4(black_box(*addr))); + } + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("naive", size), + &size, + |b, _| { + b.iter(|| { + for addr in &addrs { + black_box(longest_match_v4( + &table, + black_box((*addr).to_be_bytes()), + )); + } + }) + }, + ); + } + + group.finish(); +} + +fn bench_construction(c: &mut Criterion) { + let mut group = c.benchmark_group("construction"); + + for size in [100, 500, 1000, 5000] { + let table = generate_table(size); + + group.bench_with_input( + BenchmarkId::new("poptrie", size), + &size, + |b, _| { + b.iter(|| { + black_box(Poptrie::from(table.clone())); + }) + }, + ); + } + + group.finish(); +} + +fn bench_single_lookup(c: &mut Criterion) { + let mut group = c.benchmark_group("single_lookup"); + + for size in [100, 1000, 10000] { + let table = generate_table(size); + let pt = Poptrie::from(table.clone()); + let addr = u32::from_be_bytes([192, 168, 1, 1]); + let addr_bytes = [192u8, 168, 1, 1]; + + group.bench_with_input( + BenchmarkId::new("poptrie", size), + &size, + |b, _| b.iter(|| black_box(pt.match_v4(black_box(addr)))), + ); + + group.bench_with_input( + BenchmarkId::new("naive", size), + &size, + |b, _| { + b.iter(|| { + black_box(longest_match_v4(&table, black_box(addr_bytes))) + }) + }, + ); + } + + group.finish(); +} + +criterion_group!( + benches, + bench_lookup, + bench_construction, + bench_single_lookup +); +criterion_main!(benches); diff --git a/src/lib.rs b/src/lib.rs index b00c357..60392de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -// Copyright 2023 Oxide Computer Company +// Copyright 2026 Oxide Computer Company #![no_std] @@ -279,9 +279,20 @@ //! extern crate alloc; + use alloc::collections::BTreeMap; use alloc::vec; use alloc::vec::Vec; +use util::mask_through; + +mod util; + +#[cfg(test)] +mod test; + +#[cfg(test)] +#[macro_use] +extern crate std; /// The poptrie data structure. #[derive(Debug)] @@ -296,18 +307,6 @@ pub struct Poptrie { pub default: Option>, } -// NOTE #[derive(Default)] see: -// broken https://github.com/rust-lang/rust/issues/26925 -impl Default for Poptrie { - fn default() -> Self { - Self { - interior: Vec::new(), - leaf: Vec::new(), - default: None, - } - } -} - /// An interior poptrie node. pub struct Interior { /// The bit vector that describes child internal nodes. @@ -325,31 +324,6 @@ pub struct Interior { pub leaf_offset: u64, } -impl core::fmt::Debug for Interior { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let mut islots = Vec::new(); - let mut lslots = Vec::new(); - for i in 0..63 { - if (self.iv & (1 << i)) != 0 { - islots.push(i); - } - } - for i in 0..63 { - if (self.lv & (1 << i)) != 0 { - lslots.push(i); - } - } - //NOTE casts here due to - // - https://github.com/rust-lang/rust-analyzer/issues/11847 - f.debug_struct("Interior") - .field("iv", &islots as &dyn core::fmt::Debug) - .field("lv", &lslots as &dyn core::fmt::Debug) - .field("interior_offset", &self.interior_offset) - .field("leaf_offset", &self.leaf_offset) - .finish() - } -} - /// A leaf poptrie node. #[derive(Debug)] pub struct Leaf { @@ -357,31 +331,9 @@ pub struct Leaf { pub data: T, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Ipv4RoutingTable(pub BTreeMap<([u8; 4], u8), T>); -// NOTE #[derive(Default)] see: -// broken https://github.com/rust-lang/rust/issues/26925 -impl Default for Ipv4RoutingTable { - fn default() -> Self { - Self(BTreeMap::new()) - } -} - -impl core::ops::Deref for Ipv4RoutingTable { - type Target = BTreeMap<([u8; 4], u8), T>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl core::ops::DerefMut for Ipv4RoutingTable { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - impl Ipv4RoutingTable { pub fn add(&mut self, dst: [u8; 4], len: u8, nexthop: T) { self.0.insert((dst, len), nexthop); @@ -396,31 +348,9 @@ impl From> for Poptrie { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Ipv6RoutingTable(pub BTreeMap<([u8; 16], u8), T>); -// NOTE #[derive(Default)] see: -// broken https://github.com/rust-lang/rust/issues/26925 -impl Default for Ipv6RoutingTable { - fn default() -> Self { - Self(BTreeMap::new()) - } -} - -impl core::ops::Deref for Ipv6RoutingTable { - type Target = BTreeMap<([u8; 16], u8), T>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl core::ops::DerefMut for Ipv6RoutingTable { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - impl Ipv6RoutingTable { pub fn add(&mut self, dst: [u8; 16], len: u8, nexthop: T) { self.0.insert((dst, len), nexthop); @@ -435,23 +365,6 @@ impl From> for Poptrie { } } -macro_rules! extract { - ($width:expr, $offset:expr, $v:expr, $bits:expr) => {{ - let shift = $bits.saturating_sub($width * ($offset + 1)); - let mask = 0b111111 << shift; - let res = ($v & mask) >> shift; - res as u8 - }}; -} - -pub fn extract_32(width: u8, offset: u8, v: u32) -> u8 { - extract!(width, offset, v, 32u8) -} - -pub fn extract_128(width: u8, offset: u8, v: u32) -> u8 { - extract!(width, offset, v, 128u8) -} - //TODO having this as a macro is terrible for debugging as we get no backtrace macro_rules! matcher { ($self:ident, $addr:tt, $bits:expr) => {{ @@ -468,9 +381,24 @@ macro_rules! matcher { #[cfg(test)] println!("{:#?}", $self.interior.get(i as usize)?); - while (v & (1 << n)) != 0 { + while (v & (1u64 << n)) != 0 { + // Check for stash at CURRENT node BEFORE descending. + // This handles the case where both an interior child AND a leaf + // exist at the same position (shorter prefix provides fallback). + { + let lv = $self.interior.get(i as usize)?.lv; + if (lv & (1u64 << n)) != 0 { + let base = $self.interior.get(i as usize)?.leaf_offset; + let arg = lv & mask_through(n); + let bc = arg.count_ones() as u64; + let leaf_i = base - lv.count_ones() as u64 + bc - 1; + result = Some($self.leaf.get(leaf_i as usize)?.data.clone()) + } + } + + // Now descend to the interior child let base = $self.interior.get(i as usize)?.interior_offset; - let arg = v & ((2 << n) - 1); + let arg = v & mask_through(n); let bc = arg.count_ones() as u64; i = base + bc - 1; v = $self.interior.get(i as usize)?.iv; @@ -483,21 +411,15 @@ macro_rules! matcher { #[cfg(test)] println!("{:#?}", $self.interior.get(i as usize)?); - - // check for stash any potentially suboptimal matches, longer - // prefix matches will overwrite these - let base = $self.interior.get(i as usize)?.leaf_offset; - let v = $self.interior.get(i as usize)?.lv; - if (v & (1 << n)) != 0 { - let i = base - 1; - result = Some($self.leaf.get(i as usize)?.data.clone()) - } } + // Final leaf lookup at the terminal node let base = $self.interior.get(i as usize)?.leaf_offset; let v = $self.interior.get(i as usize)?.lv; - if (v & (1 << n)) != 0 { - i = base - 1; + if (v & (1u64 << n)) != 0 { + let arg = v & mask_through(n); + let bc = arg.count_ones() as u64; + i = base - v.count_ones() as u64 + bc - 1; result = Some($self.leaf.get(i as usize)?.data.clone()) } @@ -513,75 +435,104 @@ macro_rules! construct { let mut ioff = 1; for depth in 0..$depth { let mut subforest = Vec::<(u8, $rt<$t>)>::new(); - let mut children = 0; - let mut siblings = 0; + // Tracks cumulative count of children from preceding sibling trees. + // This is used to compute interior_offset for each tree's children. + let mut child_offset = 0; for (_, $tree) in &forest { let mut iv = 0u64; let mut lv = 0u64; let mut subsubforest = Vec::<(u8, $rt<$t>)>::new(); - for (r, e) in &$tree.0 { + // Collect leaves with their bit positions for later sorting. + // Leaves must be pushed in bit-position order for the matcher's + // popcount-based indexing to work correctly. + let mut pending_leaves: Vec<(u8, $t)> = Vec::new(); + + // Sort routes by descending prefix length (more specific first). + // This ensures longer prefixes claim their slots before shorter + // prefixes, implementing proper longest-prefix-match semantics. + let mut routes: Vec<_> = $tree.0.iter().collect(); + routes.sort_by(|a, b| b.0.1.cmp(&a.0.1)); + + for ((addr, prefix_len), data) in routes { // default route case - if r.1 == 0 { - $self.default = Some(Leaf { data: e.clone() }); + if *prefix_len == 0 { + $self.default = Some(Leaf { data: data.clone() }); continue; } - let k = extract!(6, depth, $w::from_be_bytes(r.0), $bits); + let k = extract!(6, depth, $w::from_be_bytes(*addr), $bits); let consumed = core::cmp::min((depth + 1) * 6, $bits); - if r.1 <= consumed { - if ((1 << k) & iv) == 0 { - lv |= 1 << k; - $self.leaf.push(Leaf { data: e.clone() }); + if *prefix_len <= consumed { + // Only add leaf if no existing leaf at this position. + // More specific prefixes (processed first) take precedence. + // Note: We allow setting lv even when iv is set - this provides + // fallback matches when traversing through interior nodes + // doesn't find a more specific match (the "stash" logic). + if ((1u64 << k) & lv) == 0 { + lv |= 1u64 << k; + pending_leaves.push((k, data.clone())); } // If the prefix of the router entry is less than but not equal // to the consumed number of bits, we need to add those bits to // the bitvec. - if r.1 != consumed { + if *prefix_len != consumed { // Shift by the extra bits and add to the bitvec for this // internal node. - let extra = 1 << (consumed - r.1); + let extra = 1u64 << (consumed - *prefix_len); for i in 1..(extra) { - lv |= 1 << (k + i); - $self.leaf.push(Leaf { data: e.clone() }); + // Only add if slot not already claimed by more specific prefix + if ((1u64 << (k + i as u8)) & lv) == 0 { + lv |= 1u64 << (k + i as u8); + pending_leaves.push((k + i as u8, data.clone())); + } } } continue; } - iv |= 1 << k; + iv |= 1u64 << k; match subsubforest.iter_mut().find(|x| x.0 == k) { Some(ref mut entry) => { - entry.1.insert(*r, e.clone()); + entry.1.insert((*addr, *prefix_len), data.clone()); } None => { let mut tbl = $rt::<$t>::default(); - tbl.insert(*r, e.clone()); + tbl.insert((*addr, *prefix_len), data.clone()); subsubforest.push((k, tbl)); - if iv > 0 { - children += 1; - } } } } + // Sort leaves by bit position and push them in order. + // The matcher uses popcount to index into leaves, which requires + // leaves to be stored in bit-position order. + pending_leaves.sort_by(|a, b| a.0.cmp(&b.0)); + for (_, data) in pending_leaves { + $self.leaf.push(Leaf { data }); + } + if iv > 0 || lv > 0 { $self.interior.push(Interior { iv, lv, interior_offset: if iv > 0 { - ioff + siblings + ioff + child_offset } else { 0 }, leaf_offset: $self.leaf.len() as u64, }); - if iv > 0 { - siblings += 1; - } } + // Sort children by k value to maintain correct popcount-based indexing. + // The matcher uses popcount to find the child index, which assumes + // children are stored in order of their bit positions in iv. + subsubforest.sort_by(|a, b| a.0.cmp(&b.0)); + + // Add this tree's children count to the offset for subsequent trees + child_offset += subsubforest.len() as u64; subforest.extend_from_slice(&subsubforest); } - ioff += children; + ioff += subforest.len() as u64; forest = subforest; } }} @@ -614,433 +565,3 @@ impl Poptrie { matcher!(self, addr, 128u8) } } - -#[cfg(test)] -#[macro_use] -extern crate std; - -#[cfg(test)] -mod test { - use super::*; - - #[derive(Default, Copy, Clone, PartialEq)] - struct Ipv4(u32); - impl core::fmt::Debug for Ipv4 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let b = self.0.to_be_bytes(); - write!(f, "{}.{}.{}.{}", b[0], b[1], b[2], b[3]) - } - } - - impl Ipv4 { - fn new(v: [u8; 4]) -> Self { - Self(u32::from_be_bytes(v)) - } - } - - #[derive(Default, Copy, Clone, PartialEq)] - struct Ipv6(u128); - impl core::fmt::Debug for Ipv6 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let b = self.0.to_be_bytes(); - write!( - f, - "{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}", - b[0], b[1], b[2], b[3], - b[4], b[5], b[6], b[7], - b[8], b[9], b[10], b[11], - b[12], b[13], b[14], b[15], - ) - } - } - - impl Ipv6 { - fn new(v: [u8; 16]) -> Self { - Self(u128::from_be_bytes(v)) - } - } - - impl std::str::FromStr for Ipv6 { - type Err = std::net::AddrParseError; - fn from_str(s: &str) -> std::result::Result { - let addr: std::net::Ipv6Addr = s.parse()?; - Ok(Self::new(addr.octets())) - } - } - - #[test] - fn test_extract32() { - // Verify documentation examples - - // 1.0.0.0 - let v = u32::from_be_bytes([1, 0, 0, 0]); - let x = extract_32_all(v); - assert_eq!(x, [0, 16, 0, 0, 0, 0]); - - // 247.33.0.0 - let v = u32::from_be_bytes([247, 33, 0, 0]); - let x = extract_32_all(v); - assert_eq!(x, [61, 50, 4, 0, 0, 0]); - - // 247.33.12.0 - let v = u32::from_be_bytes([247, 33, 12, 0]); - let x = extract_32_all(v); - assert_eq!(x, [61, 50, 4, 12, 0, 0]); - - // 51.12.109.0 - let v = u32::from_be_bytes([51, 12, 109, 0]); - let x = extract_32_all(v); - assert_eq!(x, [12, 48, 49, 45, 0, 0]); - - // 77.18.0.0 - let v = u32::from_be_bytes([77, 18, 0, 0]); - let x = extract_32_all(v); - assert_eq!(x, [19, 17, 8, 0, 0, 0]); - - // 170.1.14.3 - let v = u32::from_be_bytes([170, 1, 14, 3]); - let x = extract_32_all(v); - assert_eq!(x, [42, 32, 4, 14, 0, 3]); - - // 0.0.0.0 - let v = u32::from_be_bytes([0, 0, 0, 0]); - let x = extract_32_all(v); - assert_eq!(x, [0, 0, 0, 0, 0, 0]); - } - - #[test] - fn test_construct_rec() { - let tbl = test_routing_table_with_default_route_v4(); - let pt = Poptrie::::from(tbl); - - #[allow(clippy::identity_op)] - let expected_root_bitvec = - 0u64 | 1 << 0 | 1 << 61 | 1 << 61 | 1 << 12 | 1 << 19 | 1 << 42; - - assert_eq!(expected_root_bitvec, pt.interior[0].iv); - assert_eq!(pt.leaf.len(), 27); - - println!("{:#?}", pt); - } - - #[test] - fn test_match_v4() { - let tbl = test_routing_table_v4(); - let pt = Poptrie::::from(tbl); - - // Test hits - let addr = Ipv4::new([1, 7, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([1, 254, 254, 254]))); - - let addr = Ipv4::new([247, 33, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([247, 33, 0, 1]))); - - let addr = Ipv4::new([247, 33, 12, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([247, 33, 12, 1]))); - - let addr = Ipv4::new([51, 12, 109, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([51, 12, 109, 10]))); - - let addr = Ipv4::new([77, 18, 4, 7]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([77, 18, 10, 1]))); - - let addr = Ipv4::new([170, 1, 14, 3]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([1, 7, 0, 1]))); - - // Test default route - let addr = Ipv4::new([4, 7, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, None); - - let tbl = test_routing_table_with_default_route_v4(); - let pt = Poptrie::::from(tbl); - - // Test default route - let addr = Ipv4::new([4, 7, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, Some(Ipv4::new([1, 2, 3, 4]))); - } - - #[test] - fn test_match_v4_multi() { - let tbl = test_routing_table_v4_mp(); - let pt = Poptrie::>::from(tbl); - - // Test hits - let addr = Ipv4::new([1, 7, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([1, 254, 254, 254]), // path 1 - Ipv4::new([1, 254, 254, 255]), // path 2 - ]) - ); - - let addr = Ipv4::new([247, 33, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([247, 33, 0, 1]), // path 1 - Ipv4::new([247, 33, 0, 2]), // path 2 - ]) - ); - - let addr = Ipv4::new([247, 33, 12, 1]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([247, 33, 12, 1]), // path 1 - Ipv4::new([247, 33, 12, 2]), // path 2 - ]) - ); - - let addr = Ipv4::new([51, 12, 109, 1]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([51, 12, 109, 10]), // path 1 - Ipv4::new([51, 12, 109, 11]), // path 2 - ],) - ); - - let addr = Ipv4::new([77, 18, 4, 7]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([77, 18, 10, 1]), // path 1 - Ipv4::new([77, 18, 10, 2]), // path 2 - ],) - ); - - let addr = Ipv4::new([170, 1, 14, 3]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([1, 7, 0, 1]), // path 1 - Ipv4::new([1, 7, 0, 2]), // path 2 - ],) - ); - - // Test default route - let addr = Ipv4::new([4, 7, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!(m, None); - - let tbl = test_routing_table_with_default_route_v4_mp(); - let pt = Poptrie::>::from(tbl); - - // Test default route - let addr = Ipv4::new([4, 7, 0, 1]); - let m = pt.match_v4(addr.0); - assert_eq!( - m, - Some(vec![ - Ipv4::new([1, 2, 3, 4]), // path 1 - Ipv4::new([1, 2, 3, 5]), // path 2 - ]) - ); - } - - #[test] - fn test_match_v6() { - let tbl = test_routing_table_v6(); - let pt = Poptrie::::from(tbl); - - // Test hits - let addr: Ipv6 = "1:7:0::1".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "1::ffff:ffff:ffff".parse().unwrap(); - assert_eq!(m, Some(gw)); - - let addr: Ipv6 = "247:33::1".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "247:33::1".parse().unwrap(); - assert_eq!(m, Some(gw)); - - let addr: Ipv6 = "247:33:12::1".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "247:33:12::1".parse().unwrap(); - assert_eq!(m, Some(gw)); - - let addr: Ipv6 = "51:12:109::1".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "51:12:109::10".parse().unwrap(); - assert_eq!(m, Some(gw)); - - let addr: Ipv6 = "77:18:4::7".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "77:18:10::1".parse().unwrap(); - assert_eq!(m, Some(gw)); - - let addr: Ipv6 = "170:1:14::3".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "1:7:0::1".parse().unwrap(); - assert_eq!(m, Some(gw)); - - // Test default route - let addr: Ipv6 = "4:7:0::1".parse().unwrap(); - let m = pt.match_v6(addr.0); - assert_eq!(m, None); - - let tbl = test_routing_table_with_default_route_v6(); - let pt = Poptrie::::from(tbl); - - let addr: Ipv6 = "4:7:0::1".parse().unwrap(); - let m = pt.match_v6(addr.0); - let gw: Ipv6 = "1:2:3::4".parse().unwrap(); - assert_eq!(m, Some(gw)); - } - - fn test_routing_table_v4() -> Ipv4RoutingTable { - let mut tbl = Ipv4RoutingTable::::default(); - tbl.add([1, 0, 0, 0], 8, Ipv4::new([1, 254, 254, 254])); - tbl.add([247, 33, 0, 0], 16, Ipv4::new([247, 33, 0, 1])); - tbl.add([247, 33, 12, 0], 24, Ipv4::new([247, 33, 12, 1])); - tbl.add([51, 12, 109, 0], 24, Ipv4::new([51, 12, 109, 10])); - tbl.add([77, 18, 0, 0], 16, Ipv4::new([77, 18, 10, 1])); - tbl.add([170, 1, 14, 3], 32, Ipv4::new([1, 7, 0, 1])); - tbl - } - - fn test_routing_table_v4_mp() -> Ipv4RoutingTable> { - let mut tbl = Ipv4RoutingTable::>::default(); - tbl.add( - [1, 0, 0, 0], - 8, - vec![ - Ipv4::new([1, 254, 254, 254]), // path 1 - Ipv4::new([1, 254, 254, 255]), // path 2 - ], - ); - - tbl.add( - [247, 33, 0, 0], - 16, - vec![ - Ipv4::new([247, 33, 0, 1]), // path 1 - Ipv4::new([247, 33, 0, 2]), // path 2 - ], - ); - - tbl.add( - [247, 33, 12, 0], - 24, - vec![ - Ipv4::new([247, 33, 12, 1]), // path 1 - Ipv4::new([247, 33, 12, 2]), // path 2 - ], - ); - - tbl.add( - [51, 12, 109, 0], - 24, - vec![ - Ipv4::new([51, 12, 109, 10]), // path 1 - Ipv4::new([51, 12, 109, 11]), // path 2 - ], - ); - - tbl.add( - [77, 18, 0, 0], - 16, - vec![ - Ipv4::new([77, 18, 10, 1]), // path 1 - Ipv4::new([77, 18, 10, 2]), // path 2 - ], - ); - - tbl.add( - [170, 1, 14, 3], - 32, - vec![ - Ipv4::new([1, 7, 0, 1]), // path 1 - Ipv4::new([1, 7, 0, 2]), // path 2 - ], - ); - - tbl - } - - fn test_routing_table_with_default_route_v4() -> Ipv4RoutingTable { - let mut tbl = test_routing_table_v4(); - tbl.add([0, 0, 0, 0], 0, Ipv4::new([1, 2, 3, 4])); - tbl - } - - fn test_routing_table_with_default_route_v4_mp( - ) -> Ipv4RoutingTable> { - let mut tbl = test_routing_table_v4_mp(); - tbl.add( - [0, 0, 0, 0], - 0, - vec![ - Ipv4::new([1, 2, 3, 4]), // path 1 - Ipv4::new([1, 2, 3, 5]), // path 2 - ], - ); - tbl - } - - fn test_routing_table_v6() -> Ipv6RoutingTable { - let mut tbl = Ipv6RoutingTable::::default(); - - let rt: std::net::Ipv6Addr = "1::".parse().unwrap(); - let gw: Ipv6 = "1::ffff:ffff:ffff".parse().unwrap(); - tbl.add(rt.octets(), 16, gw); - - let rt: std::net::Ipv6Addr = "247:33::".parse().unwrap(); - let gw: Ipv6 = "247:33::1".parse().unwrap(); - tbl.add(rt.octets(), 32, gw); - - let rt: std::net::Ipv6Addr = "247:33:12::".parse().unwrap(); - let gw: Ipv6 = "247:33:12::1".parse().unwrap(); - tbl.add(rt.octets(), 48, gw); - - let rt: std::net::Ipv6Addr = "51:12:109::".parse().unwrap(); - let gw: Ipv6 = "51:12:109::10".parse().unwrap(); - tbl.add(rt.octets(), 48, gw); - - let rt: std::net::Ipv6Addr = "77:18::".parse().unwrap(); - let gw: Ipv6 = "77:18:10::1".parse().unwrap(); - tbl.add(rt.octets(), 32, gw); - - let rt: std::net::Ipv6Addr = "170:1:14::3".parse().unwrap(); - let gw: Ipv6 = "1:7:0::1".parse().unwrap(); - tbl.add(rt.octets(), 128, gw); - - tbl - } - - fn test_routing_table_with_default_route_v6() -> Ipv6RoutingTable { - let mut tbl = test_routing_table_v6(); - - let rt: std::net::Ipv6Addr = "::".parse().unwrap(); - let gw: Ipv6 = "1:2:3::4".parse().unwrap(); - tbl.add(rt.octets(), 0, gw); - - tbl - } - - fn extract_32_all(v: u32) -> [u8; 6] { - [ - extract_32(6, 0, v), - extract_32(6, 1, v), - extract_32(6, 2, v), - extract_32(6, 3, v), - extract_32(6, 4, v), - extract_32(6, 5, v), - ] - } -} diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..e033b85 --- /dev/null +++ b/src/test.rs @@ -0,0 +1,618 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2026 Oxide Computer Company + +use super::*; + +#[derive(Default, Copy, Clone, PartialEq)] +struct Ipv4(u32); +impl core::fmt::Debug for Ipv4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let b = self.0.to_be_bytes(); + write!(f, "{}.{}.{}.{}", b[0], b[1], b[2], b[3]) + } +} + +impl Ipv4 { + fn new(v: [u8; 4]) -> Self { + Self(u32::from_be_bytes(v)) + } +} + +#[derive(Default, Copy, Clone, PartialEq)] +struct Ipv6(u128); +impl core::fmt::Debug for Ipv6 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let b = self.0.to_be_bytes(); + write!( + f, + "{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}", + b[0], b[1], b[2], b[3], + b[4], b[5], b[6], b[7], + b[8], b[9], b[10], b[11], + b[12], b[13], b[14], b[15], + ) + } +} + +impl Ipv6 { + fn new(v: [u8; 16]) -> Self { + Self(u128::from_be_bytes(v)) + } +} + +impl std::str::FromStr for Ipv6 { + type Err = std::net::AddrParseError; + fn from_str(s: &str) -> std::result::Result { + let addr: std::net::Ipv6Addr = s.parse()?; + Ok(Self::new(addr.octets())) + } +} + +fn extract_32(width: u8, offset: u8, v: u32) -> u8 { + extract!(width, offset, v, 32u8) +} + +#[test] +fn test_extract32() { + // Verify documentation examples + + // 1.0.0.0 + let v = u32::from_be_bytes([1, 0, 0, 0]); + let x = extract_32_all(v); + assert_eq!(x, [0, 16, 0, 0, 0, 0]); + + // 247.33.0.0 + let v = u32::from_be_bytes([247, 33, 0, 0]); + let x = extract_32_all(v); + assert_eq!(x, [61, 50, 4, 0, 0, 0]); + + // 247.33.12.0 + let v = u32::from_be_bytes([247, 33, 12, 0]); + let x = extract_32_all(v); + assert_eq!(x, [61, 50, 4, 12, 0, 0]); + + // 51.12.109.0 + let v = u32::from_be_bytes([51, 12, 109, 0]); + let x = extract_32_all(v); + assert_eq!(x, [12, 48, 49, 45, 0, 0]); + + // 77.18.0.0 + let v = u32::from_be_bytes([77, 18, 0, 0]); + let x = extract_32_all(v); + assert_eq!(x, [19, 17, 8, 0, 0, 0]); + + // 170.1.14.3 + let v = u32::from_be_bytes([170, 1, 14, 3]); + let x = extract_32_all(v); + assert_eq!(x, [42, 32, 4, 14, 0, 3]); + + // 0.0.0.0 + let v = u32::from_be_bytes([0, 0, 0, 0]); + let x = extract_32_all(v); + assert_eq!(x, [0, 0, 0, 0, 0, 0]); +} + +#[test] +fn test_construct_rec() { + let tbl = test_routing_table_with_default_route_v4(); + let pt = Poptrie::::from(tbl); + + #[allow(clippy::identity_op)] + let expected_root_bitvec = + 0u64 | 1 << 0 | 1 << 61 | 1 << 61 | 1 << 12 | 1 << 19 | 1 << 42; + + assert_eq!(expected_root_bitvec, pt.interior[0].iv); + assert_eq!(pt.leaf.len(), 27); + + println!("{:#?}", pt); +} + +#[test] +fn test_match_v4() { + let tbl = test_routing_table_v4(); + let pt = Poptrie::::from(tbl); + + // Test hits + let addr = Ipv4::new([1, 7, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([1, 254, 254, 254]))); + + let addr = Ipv4::new([247, 33, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([247, 33, 0, 1]))); + + let addr = Ipv4::new([247, 33, 12, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([247, 33, 12, 1]))); + + let addr = Ipv4::new([51, 12, 109, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([51, 12, 109, 10]))); + + let addr = Ipv4::new([77, 18, 4, 7]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([77, 18, 10, 1]))); + + let addr = Ipv4::new([170, 1, 14, 3]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([1, 7, 0, 1]))); + + // Test default route + let addr = Ipv4::new([4, 7, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, None); + + let tbl = test_routing_table_with_default_route_v4(); + let pt = Poptrie::::from(tbl); + + // Test default route + let addr = Ipv4::new([4, 7, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([1, 2, 3, 4]))); +} + +#[test] +fn test_match_v4_sus() { + let tbl = test_routing_table_sus_v4(); + let pt = Poptrie::::from(tbl); + + let addr = Ipv4::new([169, 254, 254, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, Some(Ipv4::new([1, 1, 1, 7]))); +} + +fn test_routing_table_sus_v4() -> Ipv4RoutingTable { + let mut tbl = Ipv4RoutingTable::::default(); + + tbl.add([169, 254, 0, 0], 31, Ipv4::new([1, 1, 1, 1])); + tbl.add([169, 254, 0, 2], 31, Ipv4::new([1, 1, 1, 2])); + tbl.add([169, 254, 0, 6], 31, Ipv4::new([1, 1, 1, 3])); + tbl.add([169, 254, 0, 32], 31, Ipv4::new([1, 1, 1, 4])); + tbl.add([169, 254, 0, 34], 31, Ipv4::new([1, 1, 1, 5])); + tbl.add([169, 254, 0, 38], 31, Ipv4::new([1, 1, 1, 6])); + tbl.add([169, 254, 254, 1], 32, Ipv4::new([1, 1, 1, 7])); + tbl.add([169, 254, 254, 2], 32, Ipv4::new([1, 1, 1, 8])); + tbl.add([169, 254, 254, 4], 32, Ipv4::new([1, 1, 1, 9])); + tbl.add([172, 20, 29, 0], 24, Ipv4::new([1, 1, 1, 10])); + tbl +} + +#[test] +fn test_match_v4_multi() { + let tbl = test_routing_table_v4_mp(); + let pt = Poptrie::>::from(tbl); + + // Test hits + let addr = Ipv4::new([1, 7, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([1, 254, 254, 254]), // path 1 + Ipv4::new([1, 254, 254, 255]), // path 2 + ]) + ); + + let addr = Ipv4::new([247, 33, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([247, 33, 0, 1]), // path 1 + Ipv4::new([247, 33, 0, 2]), // path 2 + ]) + ); + + let addr = Ipv4::new([247, 33, 12, 1]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([247, 33, 12, 1]), // path 1 + Ipv4::new([247, 33, 12, 2]), // path 2 + ]) + ); + + let addr = Ipv4::new([51, 12, 109, 1]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([51, 12, 109, 10]), // path 1 + Ipv4::new([51, 12, 109, 11]), // path 2 + ],) + ); + + let addr = Ipv4::new([77, 18, 4, 7]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([77, 18, 10, 1]), // path 1 + Ipv4::new([77, 18, 10, 2]), // path 2 + ],) + ); + + let addr = Ipv4::new([170, 1, 14, 3]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([1, 7, 0, 1]), // path 1 + Ipv4::new([1, 7, 0, 2]), // path 2 + ],) + ); + + // Test default route + let addr = Ipv4::new([4, 7, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!(m, None); + + let tbl = test_routing_table_with_default_route_v4_mp(); + let pt = Poptrie::>::from(tbl); + + // Test default route + let addr = Ipv4::new([4, 7, 0, 1]); + let m = pt.match_v4(addr.0); + assert_eq!( + m, + Some(vec![ + Ipv4::new([1, 2, 3, 4]), // path 1 + Ipv4::new([1, 2, 3, 5]), // path 2 + ]) + ); +} + +#[test] +fn test_match_v6() { + let tbl = test_routing_table_v6(); + let pt = Poptrie::::from(tbl); + + // Test hits + let addr: Ipv6 = "1:7:0::1".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "1::ffff:ffff:ffff".parse().unwrap(); + assert_eq!(m, Some(gw)); + + let addr: Ipv6 = "247:33::1".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "247:33::1".parse().unwrap(); + assert_eq!(m, Some(gw)); + + let addr: Ipv6 = "247:33:12::1".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "247:33:12::1".parse().unwrap(); + assert_eq!(m, Some(gw)); + + let addr: Ipv6 = "51:12:109::1".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "51:12:109::10".parse().unwrap(); + assert_eq!(m, Some(gw)); + + let addr: Ipv6 = "77:18:4::7".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "77:18:10::1".parse().unwrap(); + assert_eq!(m, Some(gw)); + + let addr: Ipv6 = "170:1:14::3".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "1:7:0::1".parse().unwrap(); + assert_eq!(m, Some(gw)); + + // Test default route + let addr: Ipv6 = "4:7:0::1".parse().unwrap(); + let m = pt.match_v6(addr.0); + assert_eq!(m, None); + + let tbl = test_routing_table_with_default_route_v6(); + let pt = Poptrie::::from(tbl); + + let addr: Ipv6 = "4:7:0::1".parse().unwrap(); + let m = pt.match_v6(addr.0); + let gw: Ipv6 = "1:2:3::4".parse().unwrap(); + assert_eq!(m, Some(gw)); +} + +fn test_routing_table_v4() -> Ipv4RoutingTable { + let mut tbl = Ipv4RoutingTable::::default(); + tbl.add([1, 0, 0, 0], 8, Ipv4::new([1, 254, 254, 254])); + tbl.add([247, 33, 0, 0], 16, Ipv4::new([247, 33, 0, 1])); + tbl.add([247, 33, 12, 0], 24, Ipv4::new([247, 33, 12, 1])); + tbl.add([51, 12, 109, 0], 24, Ipv4::new([51, 12, 109, 10])); + tbl.add([77, 18, 0, 0], 16, Ipv4::new([77, 18, 10, 1])); + tbl.add([170, 1, 14, 3], 32, Ipv4::new([1, 7, 0, 1])); + tbl +} + +fn test_routing_table_v4_mp() -> Ipv4RoutingTable> { + let mut tbl = Ipv4RoutingTable::>::default(); + tbl.add( + [1, 0, 0, 0], + 8, + vec![ + Ipv4::new([1, 254, 254, 254]), // path 1 + Ipv4::new([1, 254, 254, 255]), // path 2 + ], + ); + + tbl.add( + [247, 33, 0, 0], + 16, + vec![ + Ipv4::new([247, 33, 0, 1]), // path 1 + Ipv4::new([247, 33, 0, 2]), // path 2 + ], + ); + + tbl.add( + [247, 33, 12, 0], + 24, + vec![ + Ipv4::new([247, 33, 12, 1]), // path 1 + Ipv4::new([247, 33, 12, 2]), // path 2 + ], + ); + + tbl.add( + [51, 12, 109, 0], + 24, + vec![ + Ipv4::new([51, 12, 109, 10]), // path 1 + Ipv4::new([51, 12, 109, 11]), // path 2 + ], + ); + + tbl.add( + [77, 18, 0, 0], + 16, + vec![ + Ipv4::new([77, 18, 10, 1]), // path 1 + Ipv4::new([77, 18, 10, 2]), // path 2 + ], + ); + + tbl.add( + [170, 1, 14, 3], + 32, + vec![ + Ipv4::new([1, 7, 0, 1]), // path 1 + Ipv4::new([1, 7, 0, 2]), // path 2 + ], + ); + + tbl +} + +fn test_routing_table_with_default_route_v4() -> Ipv4RoutingTable { + let mut tbl = test_routing_table_v4(); + tbl.add([0, 0, 0, 0], 0, Ipv4::new([1, 2, 3, 4])); + tbl +} + +fn test_routing_table_with_default_route_v4_mp() -> Ipv4RoutingTable> +{ + let mut tbl = test_routing_table_v4_mp(); + tbl.add( + [0, 0, 0, 0], + 0, + vec![ + Ipv4::new([1, 2, 3, 4]), // path 1 + Ipv4::new([1, 2, 3, 5]), // path 2 + ], + ); + tbl +} + +fn test_routing_table_v6() -> Ipv6RoutingTable { + let mut tbl = Ipv6RoutingTable::::default(); + + let rt: std::net::Ipv6Addr = "1::".parse().unwrap(); + let gw: Ipv6 = "1::ffff:ffff:ffff".parse().unwrap(); + tbl.add(rt.octets(), 16, gw); + + let rt: std::net::Ipv6Addr = "247:33::".parse().unwrap(); + let gw: Ipv6 = "247:33::1".parse().unwrap(); + tbl.add(rt.octets(), 32, gw); + + let rt: std::net::Ipv6Addr = "247:33:12::".parse().unwrap(); + let gw: Ipv6 = "247:33:12::1".parse().unwrap(); + tbl.add(rt.octets(), 48, gw); + + let rt: std::net::Ipv6Addr = "51:12:109::".parse().unwrap(); + let gw: Ipv6 = "51:12:109::10".parse().unwrap(); + tbl.add(rt.octets(), 48, gw); + + let rt: std::net::Ipv6Addr = "77:18::".parse().unwrap(); + let gw: Ipv6 = "77:18:10::1".parse().unwrap(); + tbl.add(rt.octets(), 32, gw); + + let rt: std::net::Ipv6Addr = "170:1:14::3".parse().unwrap(); + let gw: Ipv6 = "1:7:0::1".parse().unwrap(); + tbl.add(rt.octets(), 128, gw); + + tbl +} + +fn test_routing_table_with_default_route_v6() -> Ipv6RoutingTable { + let mut tbl = test_routing_table_v6(); + + let rt: std::net::Ipv6Addr = "::".parse().unwrap(); + let gw: Ipv6 = "1:2:3::4".parse().unwrap(); + tbl.add(rt.octets(), 0, gw); + + tbl +} + +fn extract_32_all(v: u32) -> [u8; 6] { + [ + extract_32(6, 0, v), + extract_32(6, 1, v), + extract_32(6, 2, v), + extract_32(6, 3, v), + extract_32(6, 4, v), + extract_32(6, 5, v), + ] +} + +// Test case for underflow bug found by proptest +// Minimal failing input: routes [0.0.0.128/25, 0.0.0.0/8] +// (The proptest output showed [0.0.0.252/25] but that gets masked to [0.0.0.128/25]) +#[test] +fn test_underflow_bug() { + let mut tbl = Ipv4RoutingTable::::default(); + // Use properly masked addresses (host bits must be zero) + tbl.add([0, 0, 0, 128], 25, 1); // 0.0.0.128/25 + tbl.add([0, 0, 0, 0], 8, 2); // 0.0.0.0/8 + + let pt = Poptrie::from(tbl); + + // Debug output + println!("interior nodes: {}", pt.interior.len()); + println!("leaf nodes: {}", pt.leaf.len()); + for (i, interior) in pt.interior.iter().enumerate() { + println!( + " interior[{}]: ioff={}, loff={}", + i, interior.interior_offset, interior.leaf_offset + ); + println!("{:#?}", interior); + } + + // Looking up 0.0.0.0 should match the /8 route + let addr = u32::from_be_bytes([0, 0, 0, 0]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(2)); + + // Looking up 0.0.0.128 should match the /25 route + let addr = u32::from_be_bytes([0, 0, 0, 128]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(1)); +} + +// Test case for LPM bug found by proptest +// Minimal failing input: /1 and /2 prefixes at same address +// The more specific /2 should win over /1 +#[test] +fn test_lpm_overlapping_prefixes() { + let mut tbl = Ipv4RoutingTable::::default(); + tbl.add([0, 0, 0, 0], 1, 1); // /1 -> nexthop 1 + tbl.add([0, 0, 0, 0], 2, 0); // /2 -> nexthop 0 (more specific) + + let pt = Poptrie::from(tbl); + + // Debug output + println!("interior nodes: {}", pt.interior.len()); + println!("leaf nodes: {}", pt.leaf.len()); + for (i, interior) in pt.interior.iter().enumerate() { + println!( + " interior[{}]: ioff={}, loff={}", + i, interior.interior_offset, interior.leaf_offset + ); + println!("{:#?}", interior); + } + for (i, leaf) in pt.leaf.iter().enumerate() { + println!(" leaf[{}]: {}", i, leaf.data); + } + + // Looking up 0.0.0.0 should match the /2 route (more specific) + let addr = u32::from_be_bytes([0, 0, 0, 0]); + let result = pt.match_v4(addr); + assert_eq!( + result, + Some(0), + "Should match /2 (nexthop 0), not /1 (nexthop 1)" + ); +} + +// Test case from proptest: short prefix (/1) should match addresses +// in its range even when there's a more specific route elsewhere. +#[test] +fn test_short_prefix_bug() { + let mut tbl = Ipv4RoutingTable::::default(); + tbl.add([128, 0, 0, 0], 1, 1); // /1 -> nexthop 1 (covers 128.0.0.0 - 255.255.255.255) + tbl.add([228, 0, 0, 0], 7, 2); // /7 -> nexthop 2 + + let pt = Poptrie::from(tbl.clone()); + + // Debug output + println!("interior nodes: {}", pt.interior.len()); + println!("leaf nodes: {}", pt.leaf.len()); + for (i, interior) in pt.interior.iter().enumerate() { + println!( + " interior[{}]: iv={:#018x}, lv={:#018x}, ioff={}, loff={}", + i, + interior.iv, + interior.lv, + interior.interior_offset, + interior.leaf_offset + ); + } + for (i, leaf) in pt.leaf.iter().enumerate() { + println!(" leaf[{}]: {}", i, leaf.data); + } + + // Looking up 230.0.0.0 should match the /1 route + // 230 = 0xE6, first 6 bits = 57 + let addr = u32::from_be_bytes([230, 0, 0, 0]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(1), "Should match /1 (nexthop 1)"); + + // Looking up 228.0.0.0 should match the /7 route + let addr = u32::from_be_bytes([228, 0, 0, 0]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(2), "Should match /7 (nexthop 2)"); + + // Looking up 128.0.0.0 should match the /1 route + let addr = u32::from_be_bytes([128, 0, 0, 0]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(1), "Should match /1 (nexthop 1)"); +} + +// Test case from proptest minimal failing input: +// prefix_16 = [0, 0], third_bytes = {0}, fourth_bytes = [62] +// Routes: [0,0,0,0]/24, [0,0,0,62]/31, [0,0,0,62]/32 +#[test] +fn test_dense_route_bug() { + let mut tbl = Ipv4RoutingTable::::default(); + // Add routes with varying prefix lengths at same location + // Note: /24 masks to [0,0,0,0], /31 and /32 stay as [0,0,0,62] + tbl.add([0, 0, 0, 0], 24, 0); // nexthop 0 for /24 + tbl.add([0, 0, 0, 62], 31, 1); // nexthop 1 for /31 + tbl.add([0, 0, 0, 62], 32, 2); // nexthop 2 for /32 + + let pt = Poptrie::from(tbl); + + // Debug output + println!("interior nodes: {}", pt.interior.len()); + println!("leaf nodes: {}", pt.leaf.len()); + for (i, interior) in pt.interior.iter().enumerate() { + println!( + " interior[{}]: iv={:#018x}, lv={:#018x}, ioff={}, loff={}", + i, + interior.iv, + interior.lv, + interior.interior_offset, + interior.leaf_offset + ); + } + for (i, leaf) in pt.leaf.iter().enumerate() { + println!(" leaf[{}]: {}", i, leaf.data); + } + + // Looking up 0.0.0.62 should match the /32 route (most specific) + let addr = u32::from_be_bytes([0, 0, 0, 62]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(2), "Should match /32 (nexthop 2)"); + + // Looking up 0.0.0.63 should match the /31 route + let addr = u32::from_be_bytes([0, 0, 0, 63]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(1), "Should match /31 (nexthop 1)"); + + // Looking up 0.0.0.0 should match the /24 route + let addr = u32::from_be_bytes([0, 0, 0, 0]); + let result = pt.match_v4(addr); + assert_eq!(result, Some(0), "Should match /24 (nexthop 0)"); +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..167adab --- /dev/null +++ b/src/util.rs @@ -0,0 +1,115 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2026 Oxide Computer Company + +//! Display machinery + +use crate::{Interior, Ipv4RoutingTable, Ipv6RoutingTable, Poptrie}; +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::fmt::Debug; +use core::ops::{Deref, DerefMut}; + +#[macro_export] +macro_rules! extract { + ($width:expr, $offset:expr, $v:expr, $bits:expr) => {{ + let shift = $bits.saturating_sub($width * ($offset + 1)); + let mask = 0b111111 << shift; + let res = ($v & mask) >> shift; + res as u8 + }}; +} + +impl Debug for Interior { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut islots = Vec::new(); + let mut lslots = Vec::new(); + for i in 0..64 { + if (self.iv & (1u64 << i)) != 0 { + islots.push(i); + } + } + for i in 0..64 { + if (self.lv & (1u64 << i)) != 0 { + lslots.push(i); + } + } + //NOTE casts here due to + // - https://github.com/rust-lang/rust-analyzer/issues/11847 + f.debug_struct("Interior") + .field("iv", &islots as &dyn core::fmt::Debug) + .field("lv", &lslots as &dyn core::fmt::Debug) + .field("interior_offset", &self.interior_offset) + .field("leaf_offset", &self.leaf_offset) + .finish() + } +} + +// NOTE #[derive(Default)] is broken see: +// https://github.com/rust-lang/rust/issues/26925 +impl Default for Poptrie { + fn default() -> Self { + Self { + interior: Vec::new(), + leaf: Vec::new(), + default: None, + } + } +} + +// NOTE #[derive(Default)] is broken see: +// https://github.com/rust-lang/rust/issues/26925 +impl Default for Ipv4RoutingTable { + fn default() -> Self { + Self(BTreeMap::new()) + } +} + +// NOTE #[derive(Default)] is broken see: +// https://github.com/rust-lang/rust/issues/26925 +impl Default for Ipv6RoutingTable { + fn default() -> Self { + Self(BTreeMap::new()) + } +} + +/// Create a mask with bits 0 through n (inclusive) set. +/// For n=63, returns u64::MAX (all bits set). +#[inline] +pub fn mask_through(n: u8) -> u64 { + if n >= 63 { + u64::MAX + } else { + (1u64 << (n + 1)) - 1 + } +} + +impl Deref for Ipv4RoutingTable { + type Target = BTreeMap<([u8; 4], u8), T>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Ipv4RoutingTable { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for Ipv6RoutingTable { + type Target = BTreeMap<([u8; 16], u8), T>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Ipv6RoutingTable { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/test-util/Cargo.toml b/test-util/Cargo.toml new file mode 100644 index 0000000..5615472 --- /dev/null +++ b/test-util/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "poptrie-test-util" +version = "0.1.0" +edition = "2021" + +[dependencies] +poptrie = { path = ".." } + +[dependencies.proptest] +version = "1" +optional = true + +[features] +default = [] +proptest = ["dep:proptest"] diff --git a/test-util/src/lib.rs b/test-util/src/lib.rs new file mode 100644 index 0000000..c1d2532 --- /dev/null +++ b/test-util/src/lib.rs @@ -0,0 +1,184 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2026 Oxide Computer Company + +//! Test utilities for poptrie. +//! +//! This crate provides common testing utilities + +use poptrie::{Ipv4RoutingTable, Ipv6RoutingTable}; + +/// Apply a prefix mask to an IPv4 address. +/// +/// Returns the address with host bits (beyond prefix_len) zeroed out. +pub fn mask_v4(addr: [u8; 4], prefix_len: u8) -> [u8; 4] { + let addr_u32 = u32::from_be_bytes(addr); + let mask = if prefix_len == 0 { + 0 + } else { + !0u32 << (32 - prefix_len) + }; + (addr_u32 & mask).to_be_bytes() +} + +/// Apply a prefix mask to an IPv6 address. +/// +/// Returns the address with host bits (beyond prefix_len) zeroed out. +pub fn mask_v6(addr: [u8; 16], prefix_len: u8) -> [u8; 16] { + let addr_u128 = u128::from_be_bytes(addr); + let mask = if prefix_len == 0 { + 0 + } else { + !0u128 << (128 - prefix_len) + }; + (addr_u128 & mask).to_be_bytes() +} + +/// Check if addr is within the prefix defined by (prefix_addr, prefix_len). +pub fn addr_in_prefix_v4( + addr: [u8; 4], + prefix_addr: [u8; 4], + prefix_len: u8, +) -> bool { + mask_v4(addr, prefix_len) == mask_v4(prefix_addr, prefix_len) +} + +/// Check if addr is within the prefix defined by (prefix_addr, prefix_len). +pub fn addr_in_prefix_v6( + addr: [u8; 16], + prefix_addr: [u8; 16], + prefix_len: u8, +) -> bool { + mask_v6(addr, prefix_len) == mask_v6(prefix_addr, prefix_len) +} + +/// Find the longest matching prefix for an address in a routing table. +/// +/// This is a naive O(n) implementation used for verification against +/// the optimized poptrie implementation. +pub fn longest_match_v4( + table: &Ipv4RoutingTable, + addr: [u8; 4], +) -> Option { + let mut best_match: Option<(u8, T)> = None; + for ((prefix_addr, prefix_len), nexthop) in table.iter() { + if addr_in_prefix_v4(addr, *prefix_addr, *prefix_len) { + match &best_match { + None => best_match = Some((*prefix_len, nexthop.clone())), + Some((best_len, _)) if prefix_len > best_len => { + best_match = Some((*prefix_len, nexthop.clone())) + } + _ => {} + } + } + } + best_match.map(|(_, nh)| nh) +} + +/// Find the longest matching prefix for an address in a routing table. +/// +/// This is a naive O(n) implementation used for verification against +/// the optimized poptrie implementation. +pub fn longest_match_v6( + table: &Ipv6RoutingTable, + addr: [u8; 16], +) -> Option { + let mut best_match: Option<(u8, T)> = None; + for ((prefix_addr, prefix_len), nexthop) in table.iter() { + if addr_in_prefix_v6(addr, *prefix_addr, *prefix_len) { + match &best_match { + None => best_match = Some((*prefix_len, nexthop.clone())), + Some((best_len, _)) if prefix_len > best_len => { + best_match = Some((*prefix_len, nexthop.clone())) + } + _ => {} + } + } + } + best_match.map(|(_, nh)| nh) +} + +/// Generate a deterministic routing table with n routes. +/// +/// Produces varied prefixes across the address space with prefix lengths +/// ranging from /8 to /32. +pub fn generate_table(n: usize) -> Ipv4RoutingTable { + let mut table = Ipv4RoutingTable::default(); + for i in 0..n { + let a = ((i * 7) % 256) as u8; + let b = ((i * 13) % 256) as u8; + let c = ((i * 17) % 256) as u8; + let d = ((i * 23) % 256) as u8; + let prefix_len = (8 + (i % 25)) as u8; // /8 to /32 + let addr = mask_v4([a, b, c, d], prefix_len); + table.add(addr, prefix_len, i as u32); + } + table +} + +/// Generate deterministic lookup addresses. +/// +/// Produces n addresses spread across the address space in a deterministic +/// but pseudo-random pattern. +pub fn generate_addrs(n: usize) -> Vec { + (0..n) + .map(|i| { + let a = ((i * 31) % 256) as u8; + let b = ((i * 37) % 256) as u8; + let c = ((i * 41) % 256) as u8; + let d = ((i * 43) % 256) as u8; + u32::from_be_bytes([a, b, c, d]) + }) + .collect() +} + +#[cfg(feature = "proptest")] +pub mod strategies { + //! Proptest strategies for generating routing tables and addresses. + + use super::*; + use proptest::prelude::*; + + /// Strategy for generating a valid IPv4 route (address masked to prefix length). + pub fn ipv4_route_strategy() -> impl Strategy { + (any::<[u8; 4]>(), 0u8..=32, any::()) + .prop_map(|(addr, len, nexthop)| (mask_v4(addr, len), len, nexthop)) + } + + /// Strategy for generating a valid IPv6 route (address masked to prefix length). + pub fn ipv6_route_strategy() -> impl Strategy + { + (any::<[u8; 16]>(), 0u8..=128, any::()) + .prop_map(|(addr, len, nexthop)| (mask_v6(addr, len), len, nexthop)) + } + + /// Strategy for generating an IPv4 routing table with 1 to 8192 routes. + pub fn ipv4_table_strategy() -> impl Strategy> + { + prop::collection::vec(ipv4_route_strategy(), 1..8192).prop_map( + |routes| { + let mut table = Ipv4RoutingTable::default(); + for (addr, len, nexthop) in routes { + table.add(addr, len, nexthop); + } + table + }, + ) + } + + /// Strategy for generating an IPv6 routing table with 1 to 8192 routes. + pub fn ipv6_table_strategy() -> impl Strategy> + { + prop::collection::vec(ipv6_route_strategy(), 1..8192).prop_map( + |routes| { + let mut table = Ipv6RoutingTable::default(); + for (addr, len, nexthop) in routes { + table.add(addr, len, nexthop); + } + table + }, + ) + } +} diff --git a/tests/proptest.proptest-regressions b/tests/proptest.proptest-regressions new file mode 100644 index 0000000..30d648d --- /dev/null +++ b/tests/proptest.proptest-regressions @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 5f79a96f3c72f2c335513575ed6e275b205e285a1d487c3a552702337ffdabe5 # shrinks to table = Ipv4RoutingTable({([211, 240, 0, 0], 12): 0}) +cc e23633fadda1d66d843d5ea838ed32b91a876691b23c2c58c83969e9a5c092b8 # shrinks to prefix_16 = [0, 0], third_bytes = {0}, fourth_bytes = [2] +cc 01e853a36b17d7a86364d8df0c3d4eb0f5eea140cee0e110bdef5b7900fa9af9 # shrinks to base_addr = [0, 0, 0], last_bytes = [0, 0], prefix_lens = [25, 26] diff --git a/tests/proptest.rs b/tests/proptest.rs new file mode 100644 index 0000000..837475d --- /dev/null +++ b/tests/proptest.rs @@ -0,0 +1,240 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2026 Oxide Computer Company + +use poptrie::{Ipv4RoutingTable, Poptrie}; +use poptrie_test_util::{ + longest_match_v4, longest_match_v6, mask_v4, + strategies::{ipv4_table_strategy, ipv6_table_strategy}, +}; +use proptest::prelude::*; +use std::net::{Ipv4Addr, Ipv6Addr}; + +proptest! { + /// Test that looking up any address returns the same result as a naive + /// longest-prefix-match implementation + #[test] + fn poptrie_matches_naive_lpm_v4( + table in ipv4_table_strategy(), + lookup_addr in any::<[u8; 4]>() + ) { + let pt = Poptrie::from(table.clone()); + let addr_u32 = u32::from_be_bytes(lookup_addr); + let poptrie_result = pt.match_v4(addr_u32); + let naive_result = longest_match_v4(&table, lookup_addr); + + prop_assert_eq!(poptrie_result, naive_result, + "Mismatch for addr {:?}", Ipv4Addr::from(lookup_addr)); + } + + /// Test that looking up any address returns the same result as a naive + /// longest-prefix-match implementation + #[test] + fn poptrie_matches_naive_lpm_v6( + table in ipv6_table_strategy(), + lookup_addr in any::<[u8; 16]>() + ) { + let pt = Poptrie::from(table.clone()); + let addr_u128 = u128::from_be_bytes(lookup_addr); + + let poptrie_result = pt.match_v6(addr_u128); + let naive_result = longest_match_v6(&table, lookup_addr); + + prop_assert_eq!(poptrie_result, naive_result, + "Mismatch for addr {:?}", Ipv6Addr::from(lookup_addr)); + } + + /// Test that every inserted route can be matched by an address within its prefix + #[test] + fn inserted_routes_are_matchable_v4( + table in ipv4_table_strategy() + ) { + let pt = Poptrie::from(table.clone()); + + for ((prefix_addr, prefix_len), _nexthop) in table.iter() { + // Look up the prefix address itself + let addr_u32 = u32::from_be_bytes(*prefix_addr); + let result = pt.match_v4(addr_u32); + + // The result should exist and match either this route or a more specific one + prop_assert!(result.is_some(), + "No match for prefix {:?}/{}", Ipv4Addr::from(*prefix_addr), prefix_len); + + // Verify using naive LPM + let naive_result = longest_match_v4(&table, *prefix_addr); + prop_assert_eq!(result, naive_result); + } + } + + /// Test that every inserted route can be matched by an address within its prefix + #[test] + fn inserted_routes_are_matchable_v6( + table in ipv6_table_strategy() + ) { + let pt = Poptrie::from(table.clone()); + + for ((prefix_addr, prefix_len), _nexthop) in table.iter() { + // Look up the prefix address itself + let addr_u128 = u128::from_be_bytes(*prefix_addr); + let result = pt.match_v6(addr_u128); + + // The result should exist and match either this route or a more specific one + prop_assert!(result.is_some(), + "No match for prefix {:?}/{}", Ipv6Addr::from(*prefix_addr), prefix_len); + + // Verify using naive LPM + let naive_result = longest_match_v6(&table, *prefix_addr); + prop_assert_eq!(result, naive_result); + } + } + + /// Test with routes that have long prefix lengths (/31 and /32) which + /// exercise the edge cases at the final trie depth + #[test] + fn long_prefix_routes_v4( + base_addr in any::<[u8; 3]>(), + last_bytes in prop::collection::vec(any::(), 1..20), + prefix_lens in prop::collection::vec(24u8..=32, 1..20), + ) { + let mut table = Ipv4RoutingTable::default(); + + for (i, (last_byte, prefix_len)) in last_bytes.iter().zip(prefix_lens.iter()).enumerate() { + let addr = [base_addr[0], base_addr[1], base_addr[2], *last_byte]; + let masked = mask_v4(addr, *prefix_len); + table.add(masked, *prefix_len, i as u32); + } + + let pt = Poptrie::from(table.clone()); + + // Test lookups for each route + for ((prefix_addr, prefix_len), _expected) in table.iter() { + let addr_u32 = u32::from_be_bytes(*prefix_addr); + let poptrie_result = pt.match_v4(addr_u32); + let naive_result = longest_match_v4(&table, *prefix_addr); + prop_assert_eq!(poptrie_result, naive_result, + "Mismatch for {:?}/{}", Ipv4Addr::from(*prefix_addr), prefix_len); + } + } + + /// Test with dense routing tables where many routes share common prefixes + /// (similar structure to the bug-triggering test case) + #[test] + fn dense_routes_v4( + prefix_16 in any::<[u8; 2]>(), + third_bytes in prop::collection::hash_set(any::(), 1..10), + fourth_bytes in prop::collection::vec(any::(), 1..10), + ) { + let mut table = Ipv4RoutingTable::default(); + let mut nexthop = 0u32; + + // Add routes with varying third and fourth bytes + for third in &third_bytes { + for fourth in &fourth_bytes { + // Mix of /24, /31, and /32 routes + for prefix_len in [24u8, 31, 32] { + let addr = [prefix_16[0], prefix_16[1], *third, *fourth]; + let masked = mask_v4(addr, prefix_len); + table.add(masked, prefix_len, nexthop); + nexthop += 1; + } + } + } + + let pt = Poptrie::from(table.clone()); + + // Test random lookups + for third in &third_bytes { + for fourth in 0u8..=255 { + let addr = [prefix_16[0], prefix_16[1], *third, fourth]; + let addr_u32 = u32::from_be_bytes(addr); + let poptrie_result = pt.match_v4(addr_u32); + let naive_result = longest_match_v4(&table, addr); + prop_assert_eq!(poptrie_result, naive_result, + "Mismatch for {:?}", Ipv4Addr::from(addr)); + } + } + } + + /// Test with routes that create sibling subtrees at various depths + #[test] + fn sibling_subtrees_v4( + routes in prop::collection::vec( + (any::<[u8; 4]>(), 8u8..=32, any::()), + 2..30 + ) + ) { + let mut table = Ipv4RoutingTable::default(); + for (addr, len, nexthop) in &routes { + let masked = mask_v4(*addr, *len); + table.add(masked, *len, *nexthop); + } + + let pt = Poptrie::from(table.clone()); + + // Lookup each route's prefix address + for (addr, len, _) in &routes { + let masked = mask_v4(*addr, *len); + let addr_u32 = u32::from_be_bytes(masked); + let poptrie_result = pt.match_v4(addr_u32); + let naive_result = longest_match_v4(&table, masked); + prop_assert_eq!(poptrie_result, naive_result, + "Mismatch for {:?}/{}", Ipv4Addr::from(masked), len); + } + + // Also test with the original (unmasked) addresses + for (addr, _len, _) in &routes { + let addr_u32 = u32::from_be_bytes(*addr); + let poptrie_result = pt.match_v4(addr_u32); + let naive_result = longest_match_v4(&table, *addr); + prop_assert_eq!(poptrie_result, naive_result, + "Mismatch for {:?}", Ipv4Addr::from(*addr)); + } + } +} + +/// Specific regression test for the /31 and /32 bug pattern +#[test] +fn regression_31_32_pattern() { + // This pattern mimics the original failing test + let mut table = Ipv4RoutingTable::default(); + + // Multiple /31 routes with same first three bytes but different fourth byte + table.add([169, 254, 0, 0], 31, 1); + table.add([169, 254, 0, 2], 31, 2); + table.add([169, 254, 0, 6], 31, 3); + + // Multiple /32 routes with different fourth byte + table.add([169, 254, 254, 1], 32, 7); + table.add([169, 254, 254, 2], 32, 8); + table.add([169, 254, 254, 4], 32, 9); + + let pt = Poptrie::from(table.clone()); + + // Verify each /32 route + for (addr, expected) in [ + ([169, 254, 254, 1], 7), + ([169, 254, 254, 2], 8), + ([169, 254, 254, 4], 9), + ] { + let addr_u32 = u32::from_be_bytes(addr); + let result = pt.match_v4(addr_u32); + assert_eq!(result, Some(expected), "Failed for {:?}", addr); + } + + // Verify /31 routes + for (addr, _expected) in [ + ([169, 254, 0, 0], 1), + ([169, 254, 0, 1], 1), // Within /31 + ([169, 254, 0, 2], 2), + ([169, 254, 0, 3], 2), // Within /31 + ([169, 254, 0, 6], 3), + ([169, 254, 0, 7], 3), // Within /31 + ] { + let addr_u32 = u32::from_be_bytes(addr); + let result = pt.match_v4(addr_u32); + let naive = longest_match_v4(&table, addr); + assert_eq!(result, naive, "Failed for {:?}", addr); + } +}