diff --git a/.cargo/config.toml b/.cargo/config.toml index a5751bbd83..ba92d46cbe 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,7 +7,7 @@ xtask = "run --package xtask --" # version_detect is basically "if nightly { return true; }". This setting gets # overridden within xtask for Hubris programs, so this only affects host tools like # xtask. -rustflags = ["-Zallow-features=proc_macro_diagnostic,used_with_arg"] +rustflags = ["-Zallow-features=proc_macro_diagnostic,used_with_arg,proc_macro_span"] [unstable] bindeps = true diff --git a/Cargo.lock b/Cargo.lock index f1ffe1ec45..b094c9ff35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,6 +251,21 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[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" @@ -315,7 +330,7 @@ dependencies = [ "serde", "serde_json", "serde_with 3.6.1", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -355,7 +370,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -379,7 +394,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -393,7 +408,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -598,7 +613,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -718,7 +733,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -772,6 +787,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -779,7 +800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -849,7 +870,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -871,7 +892,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -931,7 +952,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -951,7 +972,7 @@ dependencies = [ "abi", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -1261,9 +1282,11 @@ dependencies = [ "drv-spi-api", "drv-stm32h7-spi", "drv-stm32xx-sys-api", + "fixedstr", "gnarle", "idol", "idol-runtime", + "microcbor", "num-derive 0.4.2", "num-traits", "pmbus", @@ -1534,8 +1557,8 @@ dependencies = [ "idol-runtime", "lpc55-pac", "num-traits", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "userlib", "zerocopy 0.8.27", "zerocopy-derive 0.8.27", @@ -2014,12 +2037,12 @@ dependencies = [ "abi", "counters", "derive-idol-err", - "getrandom", + "getrandom 0.2.3", "idol", "idol-runtime", "num", "num-traits", - "rand_core", + "rand_core 0.6.4", "userlib", "zerocopy 0.8.27", "zerocopy-derive 0.8.27", @@ -2467,7 +2490,7 @@ dependencies = [ "ringbuf", "serde", "stm32h7", - "syn 2.0.98", + "syn 2.0.106", "userlib", "zerocopy 0.8.27", ] @@ -2818,7 +2841,7 @@ dependencies = [ "generic-array", "group", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -2895,7 +2918,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -2928,9 +2951,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2946,13 +2969,19 @@ dependencies = [ "libc", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "ff" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2972,6 +3001,15 @@ dependencies = [ name = "fixedmap" version = "0.1.0" +[[package]] +name = "fixedstr" +version = "0.1.0" +dependencies = [ + "microcbor", + "minicbor 2.1.1", + "serde", +] + [[package]] name = "flagset" version = "0.4.3" @@ -3064,7 +3102,19 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -3138,10 +3188,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -3381,7 +3441,7 @@ dependencies = [ "ron", "serde", "serde_with 3.6.1", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -3513,7 +3573,7 @@ dependencies = [ "ron", "serde", "ssmarshal", - "syn 2.0.98", + "syn 2.0.106", "unwrap-lite", "zerocopy 0.8.27", "zerocopy-derive 0.8.27", @@ -3587,9 +3647,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.140" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libm" @@ -3609,6 +3669,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "lock_api" version = "0.4.5" @@ -3855,6 +3921,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "microcbor" +version = "0.1.0" +dependencies = [ + "fixedstr", + "microcbor-derive", + "minicbor 2.1.1", + "proptest", + "proptest-derive", +] + +[[package]] +name = "microcbor-derive" +version = "0.1.0" +dependencies = [ + "microcbor", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "minibar" version = "0.1.0" @@ -3875,7 +3962,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0452a60c1863c1f50b5f77cd295e8d2786849f35883f0b9e18e7e6e1b5691b0" dependencies = [ - "minicbor-derive", + "minicbor-derive 0.15.3", ] [[package]] @@ -3883,6 +3970,10 @@ name = "minicbor" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f182275033b808ede9427884caa8e05fa7db930801759524ca7925bd8aa7a82" +dependencies = [ + "half", + "minicbor-derive 0.18.2", +] [[package]] name = "minicbor-derive" @@ -3892,7 +3983,18 @@ checksum = "bd2209fff77f705b00c737016a48e73733d7fbccb8b007194db148f03561fb70" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", +] + +[[package]] +name = "minicbor-derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17290c95158a760027059fe3f511970d6857e47ff5008f9e09bffe3d3e1c6af" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -4009,7 +4111,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.4", "serde", "smallvec 1.10.0", "zeroize", @@ -4049,7 +4151,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4197,7 +4299,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "rand_core", + "rand_core 0.6.4", "sha2", ] @@ -4269,8 +4371,8 @@ version = "0.1.0" dependencies = [ "anyhow", "phash", - "rand", - "rand_chacha", + "rand 0.8.4", + "rand_chacha 0.3.1", ] [[package]] @@ -4376,7 +4478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4427,6 +4529,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.9.4", + "lazy_static", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.6", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "psc" version = "0.1.0" @@ -4440,6 +4573,12 @@ dependencies = [ "stm32h7", ] +[[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.35" @@ -4449,6 +4588,12 @@ 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 = "r0" version = "0.2.2" @@ -4467,11 +4612,21 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "rand_hc", ] +[[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.3", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4479,7 +4634,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.3", ] [[package]] @@ -4488,7 +4653,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.3", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] @@ -4497,7 +4671,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core", + "rand_core 0.6.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.3", ] [[package]] @@ -4523,7 +4706,7 @@ checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.6.26", ] [[package]] @@ -4532,6 +4715,12 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "rfc6979" version = "0.4.0" @@ -4590,7 +4779,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "signature", @@ -4643,19 +4832,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" dependencies = [ "bitflags 1.3.2", - "errno 0.3.9", + "errno 0.3.14", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.45.0", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.4", + "errno 0.3.14", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.5" @@ -4817,7 +5031,7 @@ checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4910,7 +5124,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4954,7 +5168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -5218,7 +5432,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -5240,9 +5454,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -5290,7 +5504,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", "toml 0.9.6", ] @@ -5721,7 +5935,7 @@ dependencies = [ "serde", "smoltcp", "stm32h7", - "syn 2.0.98", + "syn 2.0.106", "task-jefe-api", "task-net-api", "task-packrat-api", @@ -5810,11 +6024,13 @@ dependencies = [ "host-sp-messages", "idol", "idol-runtime", + "microcbor", "minicbor 2.1.1", "minicbor-serde 0.6.0", "num-traits", "oxide-barcode", "serde", + "static_assertions", "userlib", "zerocopy 0.8.27", "zerocopy-derive 0.8.27", @@ -6181,6 +6397,18 @@ dependencies = [ "zerocopy-derive 0.8.27", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix 0.38.44", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -6403,7 +6631,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -6591,6 +6819,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[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.8" @@ -6726,6 +6960,15 @@ dependencies = [ "zerocopy-derive 0.8.27", ] +[[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.3.2" @@ -6743,6 +6986,24 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[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.92" @@ -6764,7 +7025,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -6786,7 +7047,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7053,6 +7314,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "wyz" version = "0.5.1" @@ -7161,7 +7428,7 @@ checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -7172,7 +7439,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -7183,7 +7450,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -7203,7 +7470,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9a739c2308..7d1829049d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,8 @@ paste = { version = "1", default-features = false } path-slash = { version = "0.1.3", default-features = false } prettyplease = { version = "0.2.29", default-features = false } proc-macro2 = { version = "1", default-features = false } +proptest = { version = "1.8.0" } +proptest-derive = { version = "0.6.0" } quote = { version = "1", default-features = false } rand = { version = "0.8", default-features = false } rand_chacha = { version = "0.3", default-features = false } diff --git a/drv/gimlet-seq-server/Cargo.toml b/drv/gimlet-seq-server/Cargo.toml index 1e74953402..bef21f5754 100644 --- a/drv/gimlet-seq-server/Cargo.toml +++ b/drv/gimlet-seq-server/Cargo.toml @@ -21,6 +21,8 @@ task-jefe-api = { path = "../../task/jefe-api" } task-packrat-api = { path = "../../task/packrat-api", features = ["serde"] } userlib = { path = "../../sys/userlib", features = ["panic-messages"] } static-cell = { path = "../../lib/static-cell" } +microcbor = { path = "../../lib/microcbor" } +fixedstr = { path = "../../lib/fixedstr", features = ["microcbor"] } cfg-if = { workspace = true } cortex-m = { workspace = true } diff --git a/drv/gimlet-seq-server/src/main.rs b/drv/gimlet-seq-server/src/main.rs index cd92457fcc..a603ddaa1c 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -208,7 +208,37 @@ struct ServerImpl { } const TIMER_INTERVAL: u32 = 10; -const EREPORT_BUF_LEN: usize = 256; +const EREPORT_BUF_LEN: usize = microcbor::max_cbor_len_for!( + task_packrat_api::Ereport +); + +#[derive(microcbor::Encode)] +pub enum EreportClass { + #[cbor(rename = "hw.pwr.pmbus.alert")] + PmbusAlert, +} + +#[derive(microcbor::EncodeFields)] +pub(crate) enum EreportKind { + PmbusAlert { + refdes: fixedstr::FixedStr<{ crate::i2c_config::MAX_COMPONENT_ID_LEN }>, + rail: &'static fixedstr::FixedStr<10>, + time: u64, + pwr_good: Option, + pmbus_status: PmbusStatus, + }, +} + +#[derive(Copy, Clone, Default, microcbor::Encode)] +pub(crate) struct PmbusStatus { + word: Option, + input: Option, + iout: Option, + vout: Option, + temp: Option, + cml: Option, + mfr: Option, +} impl ServerImpl { fn init( diff --git a/drv/gimlet-seq-server/src/vcore.rs b/drv/gimlet-seq-server/src/vcore.rs index 056c8081da..011ef40c4d 100644 --- a/drv/gimlet-seq-server/src/vcore.rs +++ b/drv/gimlet-seq-server/src/vcore.rs @@ -31,8 +31,8 @@ use crate::gpio_irq_pins::VCORE_TO_SP_ALERT_L; use drv_i2c_api::{I2cDevice, ResponseCode}; use drv_i2c_devices::raa229618::Raa229618; use drv_stm32xx_sys_api as sys_api; +use fixedstr::FixedStr; use ringbuf::*; -use serde::Serialize; use sys_api::IrqControl; use task_packrat_api as packrat_api; use userlib::{sys_get_timer, units}; @@ -139,7 +139,10 @@ impl VCore { Ok(()) } - pub fn handle_notification(&self, ereport_buf: &mut [u8]) { + pub fn handle_notification( + &self, + ereport_buf: &mut [u8; crate::EREPORT_BUF_LEN], + ) { let now = sys_get_timer().now; let asserted = self.sys.gpio_read(VCORE_TO_SP_ALERT_L) == 0; @@ -162,7 +165,11 @@ impl VCore { let _ = self.sys.gpio_irq_control(self.mask(), IrqControl::Enable); } - fn read_pmbus_status(&self, now: u64, ereport_buf: &mut [u8]) { + fn read_pmbus_status( + &self, + now: u64, + ereport_buf: &mut [u8; crate::EREPORT_BUF_LEN], + ) { use pmbus::commands::raa229618::STATUS_WORD; // Read PMBus status registers and prepare an ereport. @@ -257,7 +264,7 @@ impl VCore { .map(|s| s.0); ringbuf_entry!(Trace::StatusMfrSpecific(status_mfr_specific)); - let status = PmbusStatus { + let status = super::PmbusStatus { word: status_word.map(|s| s.0).ok(), input: status_input.ok(), vout: status_vout.ok(), @@ -266,27 +273,27 @@ impl VCore { cml: status_cml.ok(), mfr: status_mfr_specific.ok(), }; - let ereport = Ereport { - k: "hw.pwr.pmbus.alert", - v: 0, - refdes: self.device.i2c_device().component_id(), - rail: "VDD_VCORE", - time: now, - pwr_good, - pmbus_status: status, + + static RAIL: FixedStr<10> = FixedStr::from_str("VDD_VCORE"); + let ereport = packrat_api::Ereport { + class: crate::EreportClass::PmbusAlert, + version: 0, + report: crate::EreportKind::PmbusAlert { + refdes: FixedStr::from_str( + self.device.i2c_device().component_id(), + ), + rail: &RAIL, + time: now, + pwr_good, + pmbus_status: status, + }, }; - match self - .packrat - .serialize_ereport(&ereport, &mut ereport_buf[..]) - { + match self.packrat.encode_ereport(&ereport, &mut ereport_buf[..]) { Ok(len) => ringbuf_entry!(Trace::EreportSent(len)), - Err(task_packrat_api::EreportSerializeError::Packrat { - len, - err, - }) => { + Err(task_packrat_api::EreportEncodeError::Packrat { len, err }) => { ringbuf_entry!(Trace::EreportLost(len, err)) } - Err(task_packrat_api::EreportSerializeError::Serialize(_)) => { + Err(task_packrat_api::EreportEncodeError::Encoder(_)) => { ringbuf_entry!(Trace::EreportTooBig) } } @@ -325,25 +332,3 @@ impl VCore { } } } - -#[derive(Copy, Clone, Default, Serialize)] -struct PmbusStatus { - word: Option, - input: Option, - iout: Option, - vout: Option, - temp: Option, - cml: Option, - mfr: Option, -} - -#[derive(Serialize)] -struct Ereport { - k: &'static str, - v: usize, - refdes: &'static str, - rail: &'static str, - time: u64, - pwr_good: Option, - pmbus_status: PmbusStatus, -} diff --git a/lib/fixedstr/Cargo.toml b/lib/fixedstr/Cargo.toml new file mode 100644 index 0000000000..a202a041ae --- /dev/null +++ b/lib/fixedstr/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fixedstr" +version = "0.1.0" +edition = "2024" + + +[features] +microcbor = ["dep:minicbor", "dep:microcbor"] +minicbor = ["dep:minicbor"] + +[dependencies] +microcbor = { path = "../microcbor", optional = true } +minicbor = { workspace = true, optional = true } +serde = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/lib/fixedstr/src/lib.rs b/lib/fixedstr/src/lib.rs new file mode 100644 index 0000000000..7fad301f46 --- /dev/null +++ b/lib/fixedstr/src/lib.rs @@ -0,0 +1,201 @@ +// 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/. + +//! A minimalist fixed-size string type. +//! +//! # Why Not `heapless::String`? +//! +//! The [`heapless::String`] type is also a fixed-length array-backed string +//! type. At a glance, it seems very similar. `FixedStr` is actually somewhat +//! different from `heapless::String`. +//! +//! The `heapless` type provides an API similar to that of +//! `alloc::string::String`, with the ability to push characters/`&str`s at +//! runtime and to mutate the contents of the string. It's designed mainly for +//! uses where you want a mutable string, but cannot allocate it on the heap. +//! +//! Meanwhile, `FixedStr` is mainly intended for use with _immutable_ strings. +//! Unlike `heapless::String`, `FixedStr` does *not* (currently) provide APIs +//! for mutating the contents of the string after it's constructed.[^1] Instead, +//! it has `const fn` [`FixedStr::from_str`], [`FixedStr::try_from_str`], and +//! [`FixedStr::try_from_utf8`] methods, so that a `FixedStr` can be constructed +//! from string or byte literals in a `const` or `static` initializer. While +//! `heapless::String` has a `const fn new`, that function constructs an *empty* +//! string, and the functions that actually push characters to the string are +//! not `const`. +//! +//! [^1]: Because I was too lazy to implement them. +#![no_std] + +use core::ops::Deref; + +#[derive(Copy, Clone)] +pub struct FixedStr { + buf: [u8; MAX], + len: usize, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct StringTooLong; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum FromUtf8Error { + TooLong, + InvalidUtf8(core::str::Utf8Error), +} + +impl FixedStr { + pub const fn try_from_str(s: &str) -> Result { + let mut buf = [0; MAX]; + let bytes = s.as_bytes(); + let len = bytes.len(); + if len > MAX { + return Err(StringTooLong); + } + + // do this instead of `copy_from_slice` so we can be a const fn :/ + let mut idx = 0; + while idx < len { + buf[idx] = bytes[idx]; + idx += 1; + } + Ok(Self { buf, len }) + } + + pub const fn from_str(s: &str) -> Self { + match Self::try_from_str(s) { + Ok(s) => s, + Err(_) => panic!(), + } + } + + pub const fn try_from_utf8(bytes: &[u8]) -> Result { + let s = match core::str::from_utf8(bytes) { + Ok(s) => s, + Err(e) => return Err(FromUtf8Error::InvalidUtf8(e)), + }; + match Self::try_from_str(s) { + Ok(s) => Ok(s), + Err(StringTooLong) => Err(FromUtf8Error::TooLong), + } + } + + pub fn as_str(&self) -> &str { + unsafe { + // Safety: we know the buffer up to `self.len` contains valid UTF-8 + // because we only allow this type to be constructed from a `&str`. + core::str::from_utf8_unchecked(self.as_bytes()) + } + } + + pub fn as_bytes(&self) -> &[u8] { + &self.buf[..self.len] + } + + /// Converts this `FixedStr` into a byte array. + /// + /// The array may be zero-padded if the string is shorter than the maximum + /// length of the `FixedStr`. + pub const fn into_array(self) -> [u8; MAX] { + self.buf + } + + pub const fn len(&self) -> usize { + self.len + } + + pub const fn is_empty(&self) -> bool { + self.len == 0 + } +} + +impl Deref for FixedStr { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl AsRef for FixedStr { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for FixedStr { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl PartialEq for FixedStr +where + T: AsRef, +{ + fn eq(&self, other: &T) -> bool { + self.as_str() == other.as_ref() + } +} + +impl core::fmt::Display for FixedStr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(self.as_str(), f) + } +} + +impl core::fmt::Debug for FixedStr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(self.as_str(), f) + } +} + +#[cfg(feature = "microcbor")] +impl microcbor::StaticCborLen for FixedStr { + const MAX_CBOR_LEN: usize = LEN + usize::MAX_CBOR_LEN; +} + +#[cfg(any(feature = "minicbor", feature = "microcbor"))] +impl minicbor::encode::Encode for FixedStr { + fn encode( + &self, + e: &mut minicbor::encode::Encoder, + _: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.str(self.as_str())?; + Ok(()) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for FixedStr { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[cfg(feature = "serde")] +impl<'de, const MAX: usize> serde::Deserialize<'de> for FixedStr { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ExpectedLen(usize); + impl serde::de::Expected for ExpectedLen { + fn fmt( + &self, + f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + write!(f, "a string of length <= {} bytes", self.0) + } + } + let s = <&'de str>::deserialize(deserializer)?; + Self::try_from_str(s).map_err(|_: StringTooLong| { + serde::de::Error::invalid_length(s.len(), &ExpectedLen(MAX)) + }) + } +} diff --git a/lib/microcbor-derive/Cargo.toml b/lib/microcbor-derive/Cargo.toml new file mode 100644 index 0000000000..3e792a273b --- /dev/null +++ b/lib/microcbor-derive/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "microcbor-derive" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.78" +quote = "1.0.35" +syn = { version = "2.0.106", features = ["extra-traits", "parsing"] } + +[dev-dependencies] +microcbor = { path = "../microcbor" } + +[lints] +workspace = true diff --git a/lib/microcbor-derive/src/lib.rs b/lib/microcbor-derive/src/lib.rs new file mode 100644 index 0000000000..e8a1f7ef1c --- /dev/null +++ b/lib/microcbor-derive/src/lib.rs @@ -0,0 +1,1096 @@ +// 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/. + +extern crate proc_macro; +use proc_macro::TokenStream; +use quote::{ToTokens, format_ident, quote}; +use syn::{ + Attribute, DataEnum, DataStruct, DeriveInput, Generics, Ident, LitStr, + parse_macro_input, +}; + +/// Derives an implementation of the [`Encode`] and `StaticCborLen` traits for the +/// annotated `struct` or `enum` type. +/// +/// All fields of the deriving type must implement the [`Encode`] and +/// `StaticCborLen` traits, with the following exceptions: +/// +/// - If the field is annotated with the `#[cbor(skip)]` attribute, +/// it need not implement any traits, as it is skipped. +/// - If the field is annotated with the `#[cbor(flatten)]` attribute, +/// it must instead implement the [`EncodeFields`] trait. +/// +/// Because fields must implement `StaticCborLen`, the maximum length in bytes +/// of the encoded representation can be computed at compile-time. +/// +/// # Encoding +/// +/// The generated CBOR is encoded as follows: +/// +/// - Structs with named fields, and struct-like enum variants, are encoded +/// as CBOR maps of strings to values. The keys in the encoded map are the +/// string representations of the Rust identifier names of the encoded +/// fields, unless overridden by the `#[cbor(rename = "..")]` attribute. +/// - Structs with unnamed fields ("tuple structs") and enum variants with +/// unnamed fields are encoded as CBOR arrays of the values of those +/// fields, in declaration order. +/// - If a tuple struct or tuple-like enum variant has only a single field, +/// it is encoded "transparently", i.e. as the CBOR value of that field, +/// rather than as a single-element array. +/// - Unit enum variants are encoded as strings. By default, the string +/// representation is the Rust identifier name of the variant, unless +/// overridden by the `#[cbor(rename = "...")]` attribute. +/// +/// Someday, I may add a way to encode enum variants as their `repr` +/// values, but I haven't done that yet. +/// +/// ## Enum Variant ID Encoding +/// +/// The `#[cbor(variant_id = "...")]` attribute may be placed on an enum type to +/// encode its variants with a variant ID field, similar to [`serde`'s +/// "internally tagged" enum representations][serde-tagged]. +/// +/// **Note**: We use the (somewhat unfortunate) terminology "variant ID" rather +/// than "tag", because in CBOR, the term "tag" refers to a [completely +/// different thing][cbor-tags]. +/// +/// If the `#[cbor(variant_id = "id_field_name")]` attribute is present, any +/// variant of the enum will additionally encode a key-value pair where the +/// key is the provided variant ID field name, and the value is the variant's +/// name (or the value of a `#[cbor(rename = "...")]` attribute on that +/// variant if one is present). +/// +/// Otherwise, struct-like variants are encoded as a map of their field names to +/// field values, and unit variants are encoded as the variant's name (or the +/// value of a `#[cbor(rename = "...")]` attribute on that variant if one is +/// present). +/// +/// When the enum derives `#[microcbor::Encode]`, it will be encoded as a map +/// with the variant ID key-value pair added (in addition to any other fields +/// defined by the enum) variant). If the variant has no other fields, the map +/// will contain only the variant ID key-value pair. +/// +/// When the enum derives `#[microcbor::EncodeFields]`, the variant ID field will be +/// added to the parent map into which the encoded fields are flattened. If the +/// enum has no other fields, only one additional key-value pair will be added. +/// +/// For example: +/// +/// ```rust +/// # use microcbor_derive::*; +/// #[derive(Encode, EncodeFields)] +/// #[cbor(variant_id = "type")] +/// enum MyEnum { +/// // will encode as { "type": "Variant1" } +/// Variant1, +/// // will encode as { "type": "Variant2", "a": 1, "b": 2 } +/// Variant2 { a: u64, b: u64 }, +/// // will encode as { "type": "my_cool_variant", "c": 1, "d": 2 } +/// #[cbor(rename = "my_cool_variant")] +/// Variant3 { c: u64, d: u64 }, +/// // will encode as { "type": "my_cool_unit_variant"} +/// #[cbor(rename = "my_cool_unit_variant")] +/// Variant4, +/// } +///``` +/// +/// #### Usage Notes +/// +/// **Note**: The variant ID representation is not supported for tuple-like enum +/// variants with unnamed fields. For example, the following will not compile: +/// +/// ```rust,compile_fail +/// # use microcbor_derive::*; +/// #[derive(Encode, EncodeFields)] +/// #[cbor(variant_id = "type")] +/// enum MyEnum { +/// Variant1, +/// Variant2 { a: u64, b: u64 }, +/// Variant3 (u64, u8), // <-- this will not compile +/// } +/// ``` +/// +/// **Note**: The name of the variant ID field may not be the same as the field +/// name of a struct-like variant (unless it is renamed using the `#[cbor(rename +/// = "...")]` attribute). This would generate multiple fields in the encoded +/// map with the same key. +/// +/// For example, the following will not compile: +/// +/// ```rust,compile_fail +/// # use microcbor_derive::*; +/// #[derive(Encode, EncodeFields)] +/// #[cbor(variant_id = "id")] +/// enum MyEnum { +/// Variant1, +/// Variant2 { a: u64, b: u64 }, +/// Variant3 { id: u8 }, // <-- this will not compile +/// } +/// ``` +/// +/// Similarly, if a field is renamed using the `#[cbor(rename = "...")]` +/// attribute, the name it is renamed to cannot collide with the name of the +/// variant ID field. For example, this also won't compile: +/// +/// ```rust,compile_fail +/// # use microcbor_derive::*; +/// #[derive(Encode, EncodeFields)] +/// #[cbor(variant_id = "type")] +/// enum MyEnum { +/// Variant1, +/// Variant2 { +/// #[cbor(rename = "type")] // <-- this will not compile +/// type_: u8, +/// }, +/// Variant4 { a: u64, b: u64 }, +/// } +/// ``` +/// +/// However, if a field's Rust identifier collides with the name of the +/// variant ID field, the `#[cbor(rename = "...")]` attribute can be used to +/// prevent the collision by changing the encoded name of the field to +/// something else. For example: +/// +/// ```rust +/// # use microcbor_derive::*; +/// #[derive(Encode, EncodeFields)] +/// #[cbor(variant_id = "id")] +/// enum MyEnum { +/// Variant1, +/// Variant2 { +/// #[cbor(rename = "type_id")] // <-- this prevents the collision +/// id: u8, +/// }, +/// Variant4 { a: u64, b: u64 }, +/// } +/// ``` +/// +/// # Helper Attributes +/// +/// This derive macro supports a `#[cbor(...)]` attribute, which may be placed +/// on fields or variants of a deriving type to modify how they are encoded. +/// +/// ## Enum Type Definition Attributes +/// +/// The following `#[cbor(...)]` attributes are may be placed on the *definition* +/// of an enum type: +/// +/// - `#[cbor(variant_id = "..")]`: Uses the [variant ID enum +/// representation](#enum-variant-id-encoding) with the specified field name +/// name when encoding this enum. Note that this attribute may *not* be used +/// on enums which have tuple-like (unnamed fields) variants. +/// +/// ## Field Attributes +/// +/// The following `#[cbor(..)]` attributes are supported on fields of structs +/// and enum variants: +/// +/// - `#[cbor(skip)]`: Completely skip encoding this field. If a field is +/// skipped, it will not be included in the encoded CBOR output. +/// +/// - `#[cbor(skip_if_nil)]`: Skip encoding this field if it would encode a +/// CBOR `nil` value. +/// +/// This attribute will cause the generated `Encode` implementation to call +/// the value's `Encode::is_nil` method to determine if the field would emit +/// a `nil` value. If it returns `true`, the field will not be encoded at +/// all. +/// +/// - `#[cbor(flatten)]`: Flatten this field into the CBOR map generated for +/// the enclosing type, rather than as a nested CBOR map. +/// +/// This attribute may only be placed on fields which are of types that +/// implement the [`EncodeFields`] trait. [`EncodeFields`] may be derived +/// for any struct or enum type which has named fields. +/// +/// Only structs and enum variants whose fields are named may use the +/// `#[cbor(flatten)]` attribute on their fields. Using `#[cbor(flatten)]` +/// on fields of a tuple struct or tuple-like enum variant will result in a +/// compile error. An enum type which has both struct-like and tuple-like +/// variants *may* use `#[cbor(flatten)]`, but only within its struct-like +/// variants. +/// +/// - `#[cbor(rename = "...")]`: Use a different name for this field when +/// encoding it as CBOR. +/// +/// This attribute will cause the field to be encoded with the string +/// provided in the `rename` attribute as its key, rather than the Rust +/// field name. This attribute may, of course, only be used on structs +/// and enum variants with named fields. +/// +/// ## Variant Attributes +/// +/// The following `#[cbor(..)]` attributes may be placed on variants of +/// an enum type: +/// +/// - `#[cbor(rename = "...")]`: Use a different name for this variant when +/// encoding it as CBOR. +/// +/// Enum variants without fields are encoded as strings. By default, the +/// Rust identifier is used as the encoded representation of a unit +/// variant. If the variant is annotated with the `#[cbor(rename = "...")]` +/// attribute, the provided string constant will be used as the encoded +/// representation of the variant, instead. +/// +/// This attribute may only be placed on unit variants, unless the enum type +/// uses the [variant-ID representation](#enum-variant-id-encoding). +/// +/// [serde-tagged]: https://serde.rs/enum-representations.html#internally-tagged +/// [cbor-tags]: https://www.rfc-editor.org/rfc/rfc8949.html#name-tagging-of-items +#[proc_macro_derive(Encode, attributes(cbor))] +pub fn derive_encode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match gen_encode_impl(input) { + Ok(tokens) => tokens, + Err(err) => err.to_compile_error().into(), + } +} + +/// Derives an implementation of the [`EncodeFields`] trait for the annotated +/// `struct` or `enum` type. +/// +/// Deriving `EncodeFields` allows the implementing type to be annotated with +/// `#[cbor(flatten)]` when nested within another type that derives `Encode` or +/// `EncodeFields`. +/// +/// Types that derive `EncodeFields` may only have named fields. If the deriving +/// type is an `enum`, all variants must have named fields; attempting to derive +/// `EncodeFields` on an enum that has both named (struct-like) variants and +/// unnamed (tuple-like) variants will result in a compilation error. +/// +/// The same type may derive both `Encode` and `EncodeFields` to be able to be +/// encoded both as its own map and flattened into existing maps. +/// +/// # Helper Attributes +/// +/// All [the attributes](macro@Encode#helper-attributes) recognized by +/// `#[derive(Encode)]` may also be placed on the fields of a type that derives +/// `EncodeFields`. +#[proc_macro_derive(EncodeFields, attributes(cbor))] +pub fn derive_encode_fields(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match gen_encode_fields_impl(input) { + Ok(tokens) => tokens, + Err(err) => err.to_compile_error().into(), + } +} + +fn gen_encode_impl(input: DeriveInput) -> Result { + match &input.data { + syn::Data::Enum(data) => gen_enum_encode_impl( + input.attrs, + input.ident, + input.generics, + data.clone(), + ) + .map(|tokens| tokens.to_token_stream().into()), + syn::Data::Struct(data) => { + gen_encode_struct_impl(input.ident, input.generics, data.clone()) + .map(|tokens| tokens.to_token_stream().into()) + } + _ => Err(syn::Error::new_spanned( + input, + "`StaticCborLen` can only be derived for `struct` and `enum` \ + types", + )), + } +} + +const HELPER_ATTR: &str = "cbor"; +const RENAME_ATTR: &str = "rename"; +const VARIANT_ID_ATTR: &str = "variant_id"; + +fn gen_enum_encode_impl( + attrs: Vec, + ident: Ident, + generics: Generics, + data: DataEnum, +) -> Result { + // TODO(eliza): support top-level attribute for using the enum's repr + // instead of its name + let EnumDefAttrs { + variant_id_field_name, + } = EnumDefAttrs::parse(&attrs)?; + let mut variant_patterns = Vec::new(); + let mut variant_lens = Vec::new(); + let mut all_where_bounds = Vec::new(); + + for variant in data.variants { + let EnumVariantAttrs { rename } = + EnumVariantAttrs::parse(&variant.attrs)?; + + let variant_name = &variant.ident; + match variant.fields { + syn::Fields::Unit => { + let name = rename.unwrap_or_else(|| { + LitStr::new(&variant_name.to_string(), variant_name.span()) + }); + + match variant_id_field_name { + None => { + variant_patterns.push(quote! { + #ident::#variant_name => { + __microcbor_renamed_encoder.str(#name)?; + } + }); + variant_lens.push(quote! { + if ::microcbor::str_cbor_len(#name) > max { + max = ::microcbor::str_cbor_len(#name); + } + }); + } + Some(ref field_name) => { + // Since there's only ever one field, we can easily use the + // length-prefixed representation and save a byte. + variant_patterns.push(quote! { + #ident::#variant_name => { + __microcbor_renamed_encoder + .map(1)? + .str(#field_name)? + .str(#name)?; + } + }); + variant_lens.push(quote! { + #[allow(non_snake_case)] + let #variant_name = { + // Major type map + length 1 is always encoded as + // one byte. + let mut len = 1; + len += ::microcbor::str_cbor_len(#field_name); + len += ::microcbor::str_cbor_len(#name); + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + } + } + } + syn::Fields::Named(ref fields) => { + let mut field_gen = FieldGenerator::new(FieldType::Named); + if let Some(ref field_name) = variant_id_field_name { + field_gen.set_variant_id_name(field_name); + } + for field in &fields.named { + field_gen.add_field(field)?; + } + field_gen.gen_variant_id_if_needed(variant_name, rename); + let FieldGenerator { + field_patterns, + field_len_exprs, + field_encode_exprs, + where_bounds, + .. + } = field_gen; + all_where_bounds.extend(where_bounds); + let match_pattern = quote! { + #ident::#variant_name { #(#field_patterns,)* } + }; + variant_patterns.push(quote! { + #match_pattern => { + __microcbor_renamed_encoder.begin_map()?; + #(#field_encode_exprs)* + __microcbor_renamed_encoder.end()?; + } + }); + variant_lens.push(quote! { + #[allow(non_snake_case)] + let #variant_name = { + let mut len = 2; // map begin and end bytes + #(#field_len_exprs)* + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + } + syn::Fields::Unnamed(fields) => { + let mut field_gen = FieldGenerator::new(FieldType::Unnamed); + if let Some(ref field_name) = variant_id_field_name { + field_gen.set_variant_id_name(field_name); + } + for field in &fields.unnamed { + field_gen.add_field(field)?; + } + field_gen.gen_variant_id_if_needed(variant_name, rename); + let FieldGenerator { + field_patterns, + field_len_exprs, + field_encode_exprs, + where_bounds, + .. + } = field_gen; + all_where_bounds.extend(where_bounds); + let match_pattern = quote! { + #ident::#variant_name( #(#field_patterns,)* ) + }; + // If exactly one field was generated, encode just that field. + if let ([len_expr], [encode_expr]) = + (&field_len_exprs[..], &field_encode_exprs[..]) + { + variant_patterns.push(quote! { + #match_pattern => { + #encode_expr + } + }); + variant_lens.push(quote! { + #[allow(non_snake_case)] + let #variant_name = { + // it's a lil goofy that we still do it this way, + // but the len expressions are generated as + // `len += ..` + let mut len = 0; + #len_expr + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + } else { + // TODO: Since we don't flatten anything into the array + // generated for unnamed fields, we could use the + // determinate length encoding and save a byte... + variant_patterns.push(quote! { + #match_pattern => { + __microcbor_renamed_encoder.begin_array()?; + #(#field_encode_exprs)* + __microcbor_renamed_encoder.end()?; + } + }); + variant_lens.push(quote! { + #[allow(non_snake_case)] + let #variant_name = { + let mut len = 2; // array begin and end bytes + #(#field_len_exprs)* + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + } + } + } + } + let (impl_generics, tygenerics, prev_where_clause) = + generics.split_for_impl(); + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::microcbor::StaticCborLen + for #ident #tygenerics + #prev_where_clause + where #(#all_where_bounds,)* + { + const MAX_CBOR_LEN: usize = { + let mut max = 0; + #(#variant_lens;)* + max + }; + } + + #[automatically_derived] + impl #impl_generics ::microcbor::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#all_where_bounds,)* + { + fn encode( + &self, + __microcbor_renamed_encoder: &mut ::microcbor::encode::Encoder, + __microcbor_renamed_ctx: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + match self { + #(#variant_patterns,)* + } + Ok(()) + } + } + }) +} + +fn gen_encode_struct_impl( + ident: Ident, + generics: Generics, + data: DataStruct, +) -> Result { + let field_type = match data.fields { + syn::Fields::Named(_) => FieldType::Named, + syn::Fields::Unnamed(_) => FieldType::Unnamed, + syn::Fields::Unit => { + return Err(syn::Error::new_spanned( + &data.fields, + "`#[derive(microcbor::Encode)]` is not supported on unit \ + structs", + )); + } + }; + let mut field_gen = FieldGenerator::new(field_type); + for field in &data.fields { + field_gen.add_field(field)?; + } + let (impl_generics, tygenerics, prev_where_clause) = + generics.split_for_impl(); + + let FieldGenerator { + where_bounds, + field_encode_exprs, + field_len_exprs, + field_patterns, + .. + } = field_gen; + + match (field_type, &field_encode_exprs[..], &field_len_exprs[..]) { + (FieldType::Named, encode_exprs, len_exprs) => Ok(quote! { + #[automatically_derived] + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_CBOR_LEN: usize = { + let mut len = 2; // map begin and end bytes + #(#len_exprs)* + len + }; + } + + #[automatically_derived] + impl #impl_generics ::microcbor::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + __microcbor_renamed_encoder: &mut ::microcbor::encode::Encoder, + __microcbor_renamed_ctx: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + + let Self { #(#field_patterns,)* } = self; + __microcbor_renamed_encoder.begin_map()?; + #(#encode_exprs)* + __microcbor_renamed_encoder.end()?; + Ok(()) + } + } + }), + // If there's exactly one non-skipped field, encode transparently as a + // single value. + (FieldType::Unnamed, [encode_expr], [len_expr]) => Ok(quote! { + #[automatically_derived] + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_CBOR_LEN: usize = { + let mut len = 0; + #len_expr + len + }; + } + + #[automatically_derived] + impl #impl_generics ::microcbor::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + __microcbor_renamed_encoder: &mut ::microcbor::encode::Encoder, + __microcbor_renamed_ctx: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + let Self( #(#field_patterns,)* ) = self; + #encode_expr + Ok(()) + } + } + }), + (FieldType::Unnamed, encode_exprs, len_exprs) => Ok(quote! { + #[automatically_derived] + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_CBOR_LEN: usize = { + let mut len = 2; // array begin and end bytes + #(#len_exprs)* + len + }; + } + + #[automatically_derived] + impl #impl_generics ::microcbor::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + __microcbor_renamed_encoder: &mut ::microcbor::encode::Encoder, + __microcbor_renamed_ctx: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + let Self( #(#field_patterns,)* ) = self; + // TODO: Since we don't flatten anything into the array + // generated for unnamed fields, we could use the + // determinate length encoding and save a byte... + __microcbor_renamed_encoder.begin_array()?; + #(#encode_exprs)* + __microcbor_renamed_encoder.end()?; + Ok(()) + } + } + }), + } +} + +fn gen_encode_fields_impl( + input: DeriveInput, +) -> Result { + match &input.data { + syn::Data::Enum(data) => gen_encode_fields_enum_impl( + input.attrs, + input.ident, + input.generics, + data.clone(), + ) + .map(|tokens| tokens.to_token_stream().into()), + syn::Data::Struct(data) => gen_encode_fields_struct_impl( + input.ident, + input.generics, + data.clone(), + ) + .map(|tokens| tokens.to_token_stream().into()), + _ => Err(syn::Error::new_spanned( + input, + "`microcbor::EncodeFields` can only be derived for `struct` and \ + `enum` types", + )), + } +} + +fn gen_encode_fields_struct_impl( + ident: Ident, + generics: Generics, + data: DataStruct, +) -> Result { + let syn::Fields::Named(ref fields) = data.fields else { + return Err(syn::Error::new_spanned( + &data.fields, + "`microcbor::EncodeFields` may only be derived for structs with \ + named fields", + )); + }; + let mut field_gen = FieldGenerator::new(FieldType::Named); + for field in &fields.named { + field_gen.add_field(field)?; + } + let (impl_generics, tygenerics, prev_where_clause) = + generics.split_for_impl(); + + let FieldGenerator { + where_bounds, + field_encode_exprs, + field_len_exprs, + field_patterns, + .. + } = field_gen; + + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::microcbor::EncodeFields<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_FIELDS_LEN: usize = { + let mut len = 0; + #(#field_len_exprs)* + len + }; + + fn encode_fields( + &self, + __microcbor_renamed_encoder: &mut ::microcbor::encode::Encoder, + __microcbor_renamed_ctx: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + let Self { #(#field_patterns,)* } = self; + #(#field_encode_exprs)* + Ok(()) + } + } + }) +} + +fn gen_encode_fields_enum_impl( + attrs: Vec, + ident: Ident, + generics: Generics, + data: DataEnum, +) -> Result { + let EnumDefAttrs { + variant_id_field_name, + } = EnumDefAttrs::parse(&attrs)?; + let mut variant_patterns = Vec::new(); + let mut variant_lens = Vec::new(); + let mut all_where_bounds = Vec::new(); + for variant in data.variants { + let variant_name = &variant.ident; + let EnumVariantAttrs { rename } = + EnumVariantAttrs::parse(&variant.attrs)?; + + let mut field_gen = FieldGenerator::new(FieldType::Named); + if let Some(ref field_name) = variant_id_field_name { + field_gen.set_variant_id_name(field_name); + } + match variant.fields { + syn::Fields::Named(ref fields) => { + for field in &fields.named { + field_gen.add_field(field)?; + } + } + syn::Fields::Unnamed(_) => { + return Err(syn::Error::new_spanned( + &variant, + "`microcbor::EncodeFields` cannot be derived for an `enum` \ + type with unnamed (tuple-like) variants", + )); + } + syn::Fields::Unit if variant_id_field_name.is_some() => {} + syn::Fields::Unit => { + return Err(syn::Error::new_spanned( + &variant, + format!( + "`microcbor::EncodeFields` may only be derived for an \ + `enum` type with unit variants if the enum has the \ + `#[cbor({VARIANT_ID_ATTR} = \"...\")]` attribute", + ), + )); + } + }; + + field_gen.gen_variant_id_if_needed(&variant.ident, rename); + + let FieldGenerator { + field_patterns, + field_len_exprs, + field_encode_exprs, + where_bounds, + .. + } = field_gen; + all_where_bounds.extend(where_bounds); + let match_pattern = quote! { + #ident::#variant_name { #(#field_patterns,)* } + }; + variant_lens.push(quote! { + #[allow(non_snake_case)] + let #variant_name = { + // no map begin and end bytes, as we are flattening + // the fields into a higher-level map. + let mut len = 0; + #(#field_len_exprs)* + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + variant_patterns.push(quote! { + #match_pattern => { + #(#field_encode_exprs)* + } + }); + } + let (impl_generics, tygenerics, prev_where_clause) = + generics.split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::microcbor::EncodeFields<()> + for #ident #tygenerics + #prev_where_clause + where #(#all_where_bounds,)* + { + const MAX_FIELDS_LEN: usize = { + let mut max = 0; + #(#variant_lens;)* + max + }; + + fn encode_fields( + &self, + __microcbor_renamed_encoder: &mut ::microcbor::encode::Encoder, + __microcbor_renamed_ctx: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + match self { + #(#variant_patterns,)* + } + Ok(()) + } + } + }) +} + +struct FieldGenerator<'a> { + field_patterns: Vec, + field_len_exprs: Vec, + field_encode_exprs: Vec, + where_bounds: Vec, + any_skipped: bool, + field_type: FieldType, + variant_id_field: Option<&'a LitStr>, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum FieldType { + Named, + Unnamed, +} + +impl<'v> FieldGenerator<'v> { + fn new(field_type: FieldType) -> Self { + Self { + field_patterns: Vec::new(), + field_len_exprs: Vec::new(), + field_encode_exprs: Vec::new(), + where_bounds: Vec::new(), + any_skipped: false, + field_type, + variant_id_field: None, + } + } + + fn set_variant_id_name(&mut self, variant_id_name: &'v LitStr) { + self.variant_id_field = Some(variant_id_name); + } + + fn gen_variant_id_if_needed( + &mut self, + variant_name: &Ident, + rename: Option, + ) { + let Some(field_name) = self.variant_id_field.as_ref() else { + return; + }; + let variant_id = rename.unwrap_or_else(|| { + LitStr::new(&variant_name.to_string(), variant_name.span()) + }); + + self.field_len_exprs.push(quote! { + len += ::microcbor::str_cbor_len(#field_name); + len += ::microcbor::str_cbor_len(#variant_id); + }); + self.field_encode_exprs.push(quote! { + __microcbor_renamed_encoder + .str(#field_name)? + .str(#variant_id)?; + }); + } + + fn add_field(&mut self, field: &syn::Field) -> Result<(), syn::Error> { + let mut field_name = None; + let mut skipped = false; + let mut flattened = false; + let mut skipped_if_nil = false; + for attr in &field.attrs { + if attr.path().is_ident(HELPER_ATTR) { + attr.meta.require_list()?.parse_nested_meta(|meta| { + if meta.path.is_ident(RENAME_ATTR) { + if field.ident.is_none() { + return Err(meta.error(format!( + "`#[cbor({RENAME_ATTR} = \"...\")]` is only \ + supported on named fields", + ))); + } + field_name = Some(meta.value()?.parse::()?); + Ok(()) + } else if meta.path.is_ident("skip") { + skipped = true; + Ok(()) + } else if meta.path.is_ident("skip_if_nil") { + skipped_if_nil = true; + Ok(()) + } else if meta.path.is_ident("flatten") { + if self.field_type == FieldType::Unnamed { + return Err(meta.error( + "`#[cbor(flatten)]` is only supported on \ + structs and enum variants with named fields", + )); + } + flattened = true; + Ok(()) + } else { + Err(meta.error(format!( + "expected `{RENAME_ATTR}`, `skip`, `skip_if_nil`, or \ + `flatten` attribute", + ))) + } + })?; + } + } + if skipped { + self.any_skipped = true; + } + + let (field_ident, encode_name, name_len) = + match (self.field_type, skipped) { + (FieldType::Unnamed, true) => { + self.field_patterns.push(quote! { _ }); + return Ok(()); + } + (FieldType::Named, true) => { + let field_ident = field.ident.as_ref().unwrap(); + self.field_patterns.push(quote! { #field_ident: _ }); + return Ok(()); + } + (FieldType::Named, false) => { + let field_ident = field.ident.as_ref().expect( + "if we are generating named fields, there should \ + be an ident for each field", + ); + let field_name = field_name.unwrap_or_else(|| { + LitStr::new( + &field_ident.to_string(), + field_ident.span(), + ) + }); + if let Some(variant_id_name) = self.variant_id_field + && variant_id_name == &field_name + { + return Err(syn::Error::new( + field_name.span(), + format!( + "variant ID `#[cbor(variant_id = \"{}\")]` \ + collides with the name of a field", + variant_id_name.value() + ), + )); + } + self.field_patterns.push(quote! { #field_ident }); + let encode_name = quote! { + __microcbor_renamed_encoder.str(#field_name)?; + }; + let name_len = quote! { + len += ::microcbor::str_cbor_len(#field_name); + }; + (field_ident.clone(), encode_name, name_len) + } + (FieldType::Unnamed, false) => { + let num = self.field_patterns.len(); + let field_ident = format_ident!("__field_{num}"); + self.field_patterns.push(quote! { #field_ident }); + let encode_name = quote! {}; + let name_len = quote! {}; + + (field_ident, encode_name, name_len) + } + }; + + // TODO(eliza): if we allow more complex ways of encoding fields as + // different CBOR types, this will have to handle that... + let field_type = &field.ty; + if flattened { + self.where_bounds.push(quote! { + #field_type: ::microcbor::EncodeFields<()> + }); + self.field_len_exprs.push(quote! { + len += <#field_type as ::microcbor::EncodeFields<()>>::MAX_FIELDS_LEN; + }); + self.field_encode_exprs.push(quote! { + ::microcbor::EncodeFields::<()>::encode_fields( + #field_ident, + __microcbor_renamed_encoder, + __microcbor_renamed_ctx, + )?; + }); + } else { + self.where_bounds.push(quote! { + #field_type: ::microcbor::StaticCborLen + }); + self.field_len_exprs.push(quote! { + #name_len + len += <#field_type as ::microcbor::StaticCborLen>::MAX_CBOR_LEN; + }); + self.field_encode_exprs.push(if skipped_if_nil { + quote! { + if !::microcbor::Encode::<()>::is_nil(#field_ident) { + #encode_name + ::microcbor::Encode::<()>::encode( + #field_ident, + __microcbor_renamed_encoder, + __microcbor_renamed_ctx, + )?; + } + } + } else { + quote! { + #encode_name + ::microcbor::Encode::<()>::encode( + #field_ident, + __microcbor_renamed_encoder, + __microcbor_renamed_ctx, + )?; + } + }); + } + + Ok(()) + } +} + +struct EnumDefAttrs { + /// Are we asked to generate a variant-ID field? + variant_id_field_name: Option, +} + +impl EnumDefAttrs { + fn parse(attrs: &[Attribute]) -> Result { + let mut variant_id_field_name = None; + for attr in attrs { + if attr.path().is_ident(HELPER_ATTR) { + attr.meta.require_list()?.parse_nested_meta(|meta| { + if meta.path.is_ident(VARIANT_ID_ATTR) { + variant_id_field_name = + Some(meta.value()?.parse::()?); + Ok(()) + } else { + Err(meta.error(format!( + "expected `{VARIANT_ID_ATTR}` attribute" + ))) + } + })?; + }; + } + Ok(Self { + variant_id_field_name, + }) + } +} + +struct EnumVariantAttrs { + rename: Option, +} + +impl EnumVariantAttrs { + fn parse(attrs: &[Attribute]) -> Result { + let mut rename = None; + for attr in attrs { + if attr.path().is_ident(HELPER_ATTR) { + attr.meta.require_list()?.parse_nested_meta(|meta| { + if meta.path.is_ident(RENAME_ATTR) { + rename = Some(meta.value()?.parse::()?); + Ok(()) + } else { + Err(meta.error(format!( + "expected `{RENAME_ATTR}` attribute" + ))) + } + })?; + }; + } + Ok(Self { rename }) + } +} diff --git a/lib/microcbor/Cargo.toml b/lib/microcbor/Cargo.toml new file mode 100644 index 0000000000..e4ccb302e8 --- /dev/null +++ b/lib/microcbor/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "microcbor" +version = "0.1.0" +edition = "2024" + +[dependencies] +minicbor.workspace = true +microcbor-derive = { path = "../microcbor-derive" } + +[lints] +workspace = true + +[dev-dependencies] +minicbor = { workspace = true, features = ["alloc", "half"]} +fixedstr = { path = "../fixedstr", features = ["microcbor"] } +proptest = { workspace = true } +proptest-derive = { workspace = true } diff --git a/lib/microcbor/examples/test.rs b/lib/microcbor/examples/test.rs new file mode 100644 index 0000000000..e9dac9a3bb --- /dev/null +++ b/lib/microcbor/examples/test.rs @@ -0,0 +1,201 @@ +// 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/. + +use microcbor::{Encode, EncodeFields, StaticCborLen}; + +#[derive(Debug, Encode)] +pub enum TestEnum { + Variant1, + #[cbor(rename = "hw.foo.cool-ereport-class")] + Variant2, +} + +#[derive(Debug, Encode, EncodeFields)] +struct TestStruct { + #[cbor(rename = "a")] + field1: u32, + field2: TestEnum, +} + +#[derive(Debug, Encode)] +struct TestStruct2 { + #[cbor(skip_if_nil)] + field6: Option, + #[cbor(flatten)] + inner: D, +} + +#[derive(Debug, Encode, EncodeFields)] +enum TestEnum2 { + Flattened { + #[cbor(flatten)] + flattened: D, + bar: u32, + }, + Nested { + nested: D, + quux: f32, + }, +} + +#[derive(Debug, Encode)] +enum TestEnum3 { + Named { + field1: u32, + tuple_struct: TestTupleStruct1, + }, + Unnamed(u32, TestTupleStruct2), + UnnamedSingle(f64), +} + +#[derive(Debug, Encode)] +struct TestTupleStruct1(u32, u64); + +#[derive(Debug, Encode)] +struct TestTupleStruct2(u64); + +#[derive(Debug, Encode, EncodeFields)] +#[cbor(variant_id = "my_cool_variant_id")] +enum TestVariantIdEnum { + #[cbor(rename = "renamed_fields_variant")] + RenamedFieldsVariant { + a: u32, + b: u64, + }, + FieldsVariant { + c: u64, + d: bool, + }, + #[cbor(rename = "renamed_unit_variant")] + RenamedUnitVariant, + UnitVariant, +} + +fn main() { + const MAX_LEN: usize = microcbor::max_cbor_len_for![ + TestEnum, + TestStruct2, + TestEnum2, + TestStruct2>, + TestEnum3, + TestVariantIdEnum, + TestStruct2, + ]; + let mut buf = [0u8; MAX_LEN]; + + test_one_type(TestEnum::Variant2, &mut buf); + test_one_type( + TestStruct { + field1: 32, + field2: TestEnum::Variant1, + }, + &mut buf, + ); + test_one_type( + TestStruct2 { + field6: None, + inner: TestStruct { + field1: 4, + field2: TestEnum::Variant1, + }, + }, + &mut buf, + ); + test_one_type( + TestEnum2::Nested { + nested: TestStruct { + field1: 8, + field2: TestEnum::Variant2, + }, + quux: 10.6, + }, + &mut buf, + ); + test_one_type( + TestEnum2::Flattened { + flattened: TestStruct { + field1: 8, + field2: TestEnum::Variant2, + }, + bar: 10, + }, + &mut buf, + ); + + test_one_type( + TestStruct2 { + field6: Some(true), + inner: TestEnum2::Nested { + nested: TestStruct { + field1: 16, + field2: TestEnum::Variant1, + }, + quux: 87.666, + }, + }, + &mut buf, + ); + + test_one_type( + TestEnum3::Named { + field1: 69, + tuple_struct: TestTupleStruct1(1, 2), + }, + &mut buf, + ); + + test_one_type( + TestEnum3::Unnamed(420, TestTupleStruct2(0xc0ffee)), + &mut buf, + ); + + test_one_type(TestEnum3::UnnamedSingle(42069.0), &mut buf); + + test_one_type(TestVariantIdEnum::RenamedUnitVariant, &mut buf); + + test_one_type( + TestVariantIdEnum::RenamedFieldsVariant { a: 1, b: 2 }, + &mut buf, + ); + test_one_type( + TestVariantIdEnum::FieldsVariant { c: 1, d: false }, + &mut buf, + ); + test_one_type(TestVariantIdEnum::UnitVariant, &mut buf); + test_one_type( + TestStruct2 { + field6: None, + inner: TestVariantIdEnum::RenamedUnitVariant, + }, + &mut buf, + ); + + test_one_type( + TestStruct2 { + field6: Some(false), + inner: TestVariantIdEnum::RenamedFieldsVariant { a: 1, b: 2 }, + }, + &mut buf, + ); +} + +fn test_one_type(input: T, buf: &mut [u8]) { + println!( + "{}::MAX_CBOR_LEN = {}", + std::any::type_name::(), + T::MAX_CBOR_LEN + ); + + println!("value = {input:?}"); + let cursor = minicbor::encode::write::Cursor::new(buf); + let mut encoder = minicbor::encode::Encoder::new(cursor); + encoder.encode(&input).unwrap(); + let cursor = encoder.into_writer(); + let len: usize = cursor.position(); + let buf = &cursor.into_inner()[..len]; + println!("value.encode.len() = {len}"); + + println!("CBOR = {}", minicbor::display(buf)); + println!(); +} diff --git a/lib/microcbor/src/lib.rs b/lib/microcbor/src/lib.rs new file mode 100644 index 0000000000..5211c3729f --- /dev/null +++ b/lib/microcbor/src/lib.rs @@ -0,0 +1,333 @@ +// 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/. + +//! CBOR encoding traits with statically known maximum lengths. +//! +//! This crate provides traits and derive macros for encoding Rust types as +//! CBOR, with the maximum encoded length of the CBOR data determined at compile +//! time. This allows for the maximum buffer size needed to encode the data to +//! be determined at compile-time, allowing static allocation of encoding +//! buffers without the possibility of encoding failures due to insufficient +//! buffer space. +//! +//! When encoding ereports in Hubris, the CBOR messages are generally simple and +//! consist of fixed-size data. However, if buffer sizes for encoding are just +//! chosen arbitrarily by the programmer, it is possible that subsequent changes +//! to the ereport messages will increase the encoded size beyond the chosen +//! buffer size, leading to encoding failures and data loss. Thus, this crate. +//! +//! This crate provides the [`StaticCborLen`] trait for types that can be +//! encoded as CBOR with a known maximum encoded length. In addition, it +//! provides [`#[derive(Encode)`](macro@Encode) and +//! [`#[derive(EncodeFields)`](macro@EncodeFields) derive attributes for +//! deriving implementations of the [`Encode`] and [`StaticCborLen`] traits. +//! +//! ## Wait, Why Not `#[derive(serde::Serialize)]`? +//! +//! Well, the obvious one is that there's no way to know how many bytes a given +//! type's `Serialize` implementation will produce at compile-time. +//! +//! Another limitation, though, is that `serde`'s `#[serde(flatten)]` attribute +//! requires `liballoc`, as the flattened fields are temporarily stored on the +//! heap while encoding. This means that `#[serde(flatten)]` cannot be used to +//! compose nested structs in Hubris ereport messages. By introducing a separate +//! [`EncodeFields`] trait that encodes the fields of a type into a "parent" +//! struct or struct-like enum variant, we avoid this limitation and allow +//! composition of ereport messages. +//! +//! ## Okay, What About `#[derive(minicbor::Encode)]`? +//! +//! `minicbor` also has a first-party derive macro crate, [`minicbor-derive`]. +//! Would that be suitable for our purposes? Unfortunately, no. +//! `minicbor-derive` takes a very opinionated approach to CBOR encoding, with +//! the intention of providing forward- and backward-compatibility using a +//! technique similar to the one used by Protocol Buffers`. When using +//! `minicbor-derive`, fields must be annotated with index numbers (like those +//! of protobuf), and fields are serialized with those index numbers as their +//! keys, rather than their actual Rust identifiers. +//! +//! While this scheme is useful in some situations, it doesn't satisfy the goals +//! of the ereport subsystem. The whole reason we are using CBOR in the first +//! place is that we would like to allow decoding key-value data without having +//! to know the structure of the data in advance. `minicbor-derive`'s scheme +//! requires both sides of the exchange to know what field names those index +//! numbers map to at *some* version of the protocol, even if newer versions are +//! backwards compatible. So, it's unsuitable for our purposes. +//! +//! `minicbor-derive` also lacks a way to determine the maximum needed buffer +//! length to encode a value at compile time, which is this crate's primary +//! reason to exist. +//! +//! [`minicbor-derive`]: https://docs.rs/minicbor-derive +#![no_std] +use encode::{Encoder, Write}; +#[doc(inline)] +pub use microcbor_derive::{Encode, EncodeFields}; +pub use minicbor::encode::{self, Encode}; + +/// A CBOR-encodable value with a statically-known maximum length. +/// +/// A type implementing this trait must implement the [`Encode`]`<()>` trait. In +/// addition, it defines a `MAX_CBOR_LEN` constant that specifies the maximum +/// number of bytes that its [`Encode`] implementation will produce. +pub trait StaticCborLen: Encode<()> { + /// The maximum length of the CBOR-encoded representation of this value. + /// + /// The value is free to encode fewer than this many bytes, but may not + /// encode more. + const MAX_CBOR_LEN: usize; +} + +/// For a list of types implementing [`StaticCborLen`], returns the maximum length +/// of their CBOR-encoded representations. +/// +/// This macro may be used to calculate the maximum buffer size necessary to +/// encode any of a set of types implementing [`StaticCborLen`]. +/// +/// For example: +/// +/// ```rust +/// use microcbor::StaticCborLen; +/// +/// #[derive(microcbor::Encode)] +/// pub struct MyGreatEreport { +/// foo: u32, +/// bar: Option, +/// } +/// +/// #[derive(microcbor::Encode)] +/// pub enum AnotherEreport { +/// A { hello: bool, world: f64 }, +/// B(usize), +/// } +/// +/// const EREPORT_BUF_LEN: usize = microcbor::max_cbor_len_for![ +/// MyGreatEreport, +/// AnotherEreport, +/// ]; +/// +/// fn main() { +/// let mut ereport_buf = [0; EREPORT_BUF_LEN]; +/// // ... +/// # drop(ereport_buf); +/// +/// assert_eq!( +/// ereport_buf.len(), +/// core::cmp::max( +/// MyGreatEreport::MAX_CBOR_LEN, +/// AnotherEreport::MAX_CBOR_LEN, +/// ), +/// ); +/// } +/// ``` +#[macro_export] +macro_rules! max_cbor_len_for { + ($($T:ty),+$(,)?) => { + { + let mut len = 0; + $( + if <$T as $crate::StaticCborLen>::MAX_CBOR_LEN > len { + len = <$T as $crate::StaticCborLen>::MAX_CBOR_LEN; + } + )+ + len + } + }; +} + +/// Encode the named fields of a type into an *existing* CBOR map as name-value +/// pairs. +/// +/// This is used when a type is included as a field in a "parent" type that +/// derives [`Encode`], and the field in the parent type is annotated with +/// `#[cbor(flatten)]`. When that attribute is present, the fields of the +/// type are encoded as name-value pairs in the parent type's CBOR map, rather +/// than creating a new nested map for the new type being encoded. +/// +/// This type may be derived by struct types with named fields, and by enum +/// types where all variants have named fields. +/// +/// The [implementation of `EncodeFields` for `Option`][option-impl] will +/// encode the fields of the inner value if it is `Some`, or encode nothing if +/// it is `None`. This way, `#[cbor(flatten)]` may be used with values which are +/// not always present. +/// +/// [option-impl]: #impl-EncodeFields-for-Option +pub trait EncodeFields { + const MAX_FIELDS_LEN: usize; + + fn encode_fields( + &self, + e: &mut Encoder, + _: &mut C, + ) -> Result<(), encode::Error>; +} + +impl EncodeFields for &T +where + T: EncodeFields, +{ + const MAX_FIELDS_LEN: usize = T::MAX_FIELDS_LEN; + + fn encode_fields( + &self, + e: &mut Encoder, + c: &mut C, + ) -> Result<(), encode::Error> { + T::encode_fields(self, e, c) + } +} + +/// When an `Option` is used as a `#[cbor(flatten)]` field in a type deriving +/// [`Encode`] or [`EncodeFields`], and `T` implements [`EncodeFields`], the +/// `Option` will encode the fields of the inner value if it is `Some`, or +/// encode nothing if it is `None`. This way, `#[cbor(flatten)]` may be used +/// with values which are not always present. +impl EncodeFields for Option +where + T: EncodeFields, +{ + const MAX_FIELDS_LEN: usize = T::MAX_FIELDS_LEN; + + fn encode_fields( + &self, + e: &mut Encoder, + c: &mut C, + ) -> Result<(), encode::Error> { + match self { + Some(value) => value.encode_fields(e, c), + None => Ok(()), + } + } +} + +macro_rules! impl_static_cbor_len { + ($($T:ty = $len:expr),*$(,)?) => { + $( + impl StaticCborLen for $T { + const MAX_CBOR_LEN: usize = $len; + } + )* + }; +} + +impl_static_cbor_len! { + // A u8 may require up to 2 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#513 + u8 = 2, + + // A u16 may require up to 3 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#519-523 + u16 = 3, + + // A u32 may require up to 5 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#529-534 + u32 = 5, + + // A u64 may require up to 9 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#539-546 + u64 = 9, + + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#580 + f32 = 5, + + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#586 + f64 = 9, + + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#501 + bool = 1, +} + +impl StaticCborLen for usize { + #[cfg(target_pointer_width = "32")] + const MAX_CBOR_LEN: usize = u32::MAX_CBOR_LEN; + + #[cfg(not(target_pointer_width = "32"))] + const MAX_CBOR_LEN: usize = u64::MAX_CBOR_LEN; +} + +impl StaticCborLen for Option { + const MAX_CBOR_LEN: usize = if T::MAX_CBOR_LEN > 1 { + T::MAX_CBOR_LEN + 1 + } else { + 1 // always need 1 byte to encode the null, even if T is 0-sized... + }; +} + +impl StaticCborLen for [T; LEN] { + const MAX_CBOR_LEN: usize = usize_cbor_len(LEN) + (LEN * T::MAX_CBOR_LEN); +} + +impl StaticCborLen for &T { + const MAX_CBOR_LEN: usize = T::MAX_CBOR_LEN; +} + +/// Returns the CBOR-encoded length (in bytes) of a `&str` value. +/// +/// Unlike the [`minicbor::CborLen`] implementation for `&str`, this is a `const +/// fn`, and can therefore be used when the particular `&str` value to be +/// encoded is known at compile-time. This can be used when a string constant is +/// used in a type that's encoded as CBOR. +pub const fn str_cbor_len(s: &str) -> usize { + usize_cbor_len(s.len()) + s.len() +} + +/// Returns the CBOR-encoded length (in bytes) of a [`usize`] value. +/// +/// Unlike the [`minicbor::CborLen`] implementation for [`usize`], this is a +/// `const fn`, and can therefore be used when the particular `usize` value to +/// be encoded is known at compile-time. For instance, this can be used to +/// calculate the number of bytes required to encode the maximum length of a +/// CBOR array, string, or map. +#[cfg(target_pointer_width = "32")] +pub const fn usize_cbor_len(u: usize) -> usize { + u32_cbor_len(u as u32) +} + +/// Returns the CBOR-encoded length (in bytes) of a [`usize`] value. +/// +/// Unlike the [`minicbor::CborLen`] implementation for [`usize`], this is a +/// `const fn`, and can therefore be used when the particular `usize` value to +/// be encoded is known at compile-time. For instance, this can be used to +/// calculate the number of bytes required to encode the maximum length of a +/// CBOR array, string, or map. +#[cfg(target_pointer_width = "64")] +pub const fn usize_cbor_len(u: usize) -> usize { + u64_cbor_len(u as u64) +} + +/// Returns the CBOR-encoded length (in bytes) of a [`u32`] value. +/// +/// Unlike the [`minicbor::CborLen`] implementation for [`u32`], this is a +/// `const fn`, and can therefore be used when the particular `u32` value to be +/// encoded is known at compile-time. For instance, this can be used to +/// calculate the number of bytes required to encode the maximum length of a +/// CBOR array, string, or map. +pub const fn u32_cbor_len(u: u32) -> usize { + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#529-534 + match u { + 0..=0x17 => 1, + 0x18..=0xff => 2, + 0x100..=0xffff => 3, + _ => 5, + } +} + +/// Returns the CBOR-encoded length (in bytes) of a [`u64`] value. +/// +/// Unlike the [`minicbor::CborLen`] implementation for [`u64`], this is a +/// `const fn`, and can therefore be used when the particular `u64` value to be +/// encoded is known at compile-time. For instance, this can be used to +/// calculate the number of bytes required to encode the maximum length of a +/// CBOR array, string, or map. +pub const fn u64_cbor_len(u: u64) -> usize { + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#539-546 + match u { + 0..=0x17 => 1, + 0x18..=0xff => 2, + 0x100..=0xffff => 3, + 0x1_0000..=0xffff_ffff => 5, + _ => 9, + } +} diff --git a/lib/microcbor/tests/max_len.rs b/lib/microcbor/tests/max_len.rs new file mode 100644 index 0000000000..e06641f4e7 --- /dev/null +++ b/lib/microcbor/tests/max_len.rs @@ -0,0 +1,151 @@ +// 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/. + +use microcbor::{Encode, EncodeFields, StaticCborLen}; +use proptest::test_runner::TestCaseError; +use proptest_derive::Arbitrary; + +#[derive(Debug, Encode, Arbitrary)] +pub enum TestEnum { + Variant1, + #[cbor(rename = "hw.foo.cool-cbor-class")] + Variant2, + #[cbor(rename = "hw.bar.baz.fault.computer_on_fire")] + Variant3, +} + +#[derive(Debug, Encode, EncodeFields, Arbitrary)] +struct TestStruct { + #[cbor(rename = "a")] + field1: u32, + field2: TestEnum, +} + +#[derive(Debug, Encode, EncodeFields, Arbitrary)] +struct TestStruct2 { + #[cbor(skip_if_nil)] + field6: Option, + #[cbor(flatten)] + inner: D, +} + +#[derive(Debug, Encode, EncodeFields, Arbitrary)] +enum TestEnum2 { + Flattened { + #[cbor(flatten)] + flattened: D, + bar: u32, + }, + Nested { + nested: D, + quux: f32, + }, +} + +#[derive(Debug, Encode, Arbitrary)] +enum TestEnum3 { + Named { + field1: u32, + tuple_struct: TestTupleStruct1, + }, + Unnamed(u32, TestTupleStruct2), + UnnamedSingle(f64), +} + +#[derive(Debug, Encode, Arbitrary)] +struct TestTupleStruct1(u32, u64); + +#[derive(Debug, Encode, Arbitrary)] +struct TestTupleStruct2(u64); + +#[derive(Debug, Encode, Arbitrary)] +struct TestStructWithArrays { + bytes: [u8; 10], + enums: [TestEnum3; 4], + blargh: usize, +} + +#[derive(Debug, Encode, EncodeFields, Arbitrary)] +#[cbor(variant_id = "my_cool_tag")] +enum TestVariantIdEnum { + #[cbor(rename = "renamed_fields_variant")] + RenamedFieldsVariant { + a: u32, + b: u64, + }, + FieldsVariant { + c: u64, + d: bool, + }, + #[cbor(rename = "renamed_unit_variant")] + RenamedUnitVariant, + UnitVariant, +} + +#[track_caller] +fn assert_max_len( + input: &T, +) -> Result<(), TestCaseError> { + let max_size = T::MAX_CBOR_LEN; + let encoded = match minicbor::to_vec(input) { + Ok(bytes) => bytes, + Err(err) => { + return Err(TestCaseError::fail(format!( + "input did not encode: {err}\ninput = {input:#?}" + ))); + } + }; + proptest::prop_assert!( + encoded.len() <= max_size, + "encoded representation's length of {}B exceeded alleged max size \ + of {max_size}B\n\ + input: {input:#?}\n\ + encoded: {}", + encoded.len(), + minicbor::display(&encoded), + ); + Ok(()) +} + +proptest::proptest! { + #[test] + fn flattened_enum_variant(input: TestEnum2>) { + assert_max_len(&input)?; + } + + #[test] + fn enum_tuple_structs(input: TestEnum3) { + assert_max_len(&input)?; + } + + #[test] + fn unflattened_struct(input: TestStruct) { + assert_max_len(&input)?; + } + + #[test] + fn array(input: [TestStruct; 10]) { + assert_max_len(&input)?; + } + + #[test] + fn struct_with_arrays(input: TestStructWithArrays) { + assert_max_len(&input)?; + } + + #[test] + fn variant_id_enum(input: TestVariantIdEnum) { + assert_max_len(&input)?; + } + + #[test] + fn variant_id_enum_nested_or_flattened(input: TestEnum2) { + assert_max_len(&input)?; + } + + #[test] + fn variant_id_enum_array(input: [TestVariantIdEnum; 10]) { + assert_max_len(&input)?; + } +} diff --git a/task/packrat-api/Cargo.toml b/task/packrat-api/Cargo.toml index 5ab2f1c535..c32ed26232 100644 --- a/task/packrat-api/Cargo.toml +++ b/task/packrat-api/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [features] # Enable Serde support for the ereport API. serde = ["dep:serde", "dep:minicbor", "dep:minicbor-serde"] +microcbor = ["dep:microcbor", "dep:static_assertions", "dep:minicbor"] +default = ["microcbor"] [dependencies] counters = { path = "../../lib/counters" } @@ -13,6 +15,7 @@ derive-idol-err.path = "../../lib/derive-idol-err" host-sp-messages.path = "../../lib/host-sp-messages" oxide-barcode.path = "../../lib/oxide-barcode" userlib.path = "../../sys/userlib" +microcbor = { path = "../../lib/microcbor", optional = true } gateway-ereport-messages.workspace = true idol-runtime.workspace = true @@ -22,6 +25,7 @@ num-traits.workspace = true serde = { workspace = true, optional = true } zerocopy.workspace = true zerocopy-derive.workspace = true +static_assertions = { workspace = true, optional = true } # This section is here to discourage RLS/rust-analyzer from doing test builds, # since test builds don't work for cross compilation. diff --git a/task/packrat-api/src/lib.rs b/task/packrat-api/src/lib.rs index 1e49803de9..e120a6c6e5 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -14,6 +14,8 @@ use zerocopy::{ pub use gateway_ereport_messages as ereport_messages; pub use host_sp_messages::HostStartupOptions; +#[cfg(feature = "microcbor")] +use microcbor::StaticCborLen; pub use oxide_barcode::OxideIdentity; /// Represents a range of allocated MAC addresses, per RFD 320 @@ -85,11 +87,37 @@ pub enum EreportSerializeError { ), } -#[cfg(feature = "serde")] +/// Errors returned by [`Packrat::encode_ereport`]. +#[derive(counters::Count)] +#[cfg(feature = "microcbor")] +pub enum EreportEncodeError { + /// The IPC to deliver the serialized ereport failed. + Packrat { + len: usize, + #[count(children)] + err: EreportWriteError, + }, + /// Encoding the ereport failed. + Encoder(microcbor::encode::Error), +} + +/// Wrapper type defining common ereport fields. +#[cfg(feature = "microcbor")] +#[derive(Clone, microcbor::Encode)] +pub struct Ereport { + #[cbor(rename = "k")] + pub class: C, + #[cbor(rename = "v")] + pub version: u32, + #[cbor(flatten)] + pub report: D, +} + impl Packrat { /// Deliver an ereport for a value that implements [`serde::Serialize`]. The /// provided `buf` is used to serialize the value before sending it to /// Packrat. + #[cfg(feature = "serde")] pub fn serialize_ereport( &self, ereport: &impl serde::Serialize, @@ -117,6 +145,28 @@ impl Packrat { Ok(len) } + + // TODO(eliza): I really want this to be able to statically check that the + // buffer is >= E::MAX_CBOR_LEN but unfortunately that isn't currently + // possible due to https://github.com/rust-lang/rust/issues/132980... + #[cfg(feature = "microcbor")] + pub fn encode_ereport( + &self, + ereport: &E, + buf: &mut [u8], + ) -> Result { + let cursor = microcbor::encode::write::Cursor::new(buf); + let mut encoder = microcbor::encode::Encoder::new(cursor); + ereport + .encode(&mut encoder, &mut ()) + .map_err(EreportEncodeError::Encoder)?; + let cursor = encoder.into_writer(); + let len = cursor.position(); + let buf = cursor.into_inner(); + self.deliver_ereport(&buf[..len]) + .map_err(|err| EreportEncodeError::Packrat { len, err })?; + Ok(len) + } } include!(concat!(env!("OUT_DIR"), "/client_stub.rs"));