From e99b20887824c57b8a84f6b11fa45e8b87745d9e Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Sep 2025 14:39:48 -0700 Subject: [PATCH 01/30] whoa i think this actually might work --- Cargo.lock | 79 +++++--- lib/ereport-derive/Cargo.toml | 15 ++ lib/ereport-derive/src/lib.rs | 263 ++++++++++++++++++++++++++ lib/ereport/Cargo.toml | 12 ++ lib/ereport/examples/test-expanded.rs | 121 ++++++++++++ lib/ereport/examples/test.rs | 37 ++++ lib/ereport/src/lib.rs | 133 +++++++++++++ 7 files changed, 629 insertions(+), 31 deletions(-) create mode 100644 lib/ereport-derive/Cargo.toml create mode 100644 lib/ereport-derive/src/lib.rs create mode 100644 lib/ereport/Cargo.toml create mode 100644 lib/ereport/examples/test-expanded.rs create mode 100644 lib/ereport/examples/test.rs create mode 100644 lib/ereport/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index daa79217d..e374d2d31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,7 +315,7 @@ dependencies = [ "serde", "serde_json", "serde_with 3.6.1", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -355,7 +355,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -379,7 +379,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -393,7 +393,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -598,7 +598,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -718,7 +718,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -849,7 +849,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -871,7 +871,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -931,7 +931,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -951,7 +951,7 @@ dependencies = [ "abi", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -2467,7 +2467,7 @@ dependencies = [ "ringbuf", "serde", "stm32h7", - "syn 2.0.98", + "syn 2.0.106", "userlib", "zerocopy 0.8.26", ] @@ -2895,7 +2895,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -2915,6 +2915,23 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "ereport" +version = "0.1.0" +dependencies = [ + "ereport-derive", + "minicbor 2.1.1", +] + +[[package]] +name = "ereport-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "errno" version = "0.2.8" @@ -3381,7 +3398,7 @@ dependencies = [ "ron", "serde", "serde_with 3.6.1", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -3513,7 +3530,7 @@ dependencies = [ "ron", "serde", "ssmarshal", - "syn 2.0.98", + "syn 2.0.106", "unwrap-lite", "zerocopy 0.8.26", "zerocopy-derive 0.8.26", @@ -3892,7 +3909,7 @@ checksum = "bd2209fff77f705b00c737016a48e73733d7fbccb8b007194db148f03561fb70" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4049,7 +4066,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4375,7 +4392,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]] @@ -4816,7 +4833,7 @@ checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -4909,7 +4926,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -5218,7 +5235,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -5240,9 +5257,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 +5307,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.106", "toml 0.9.6", ] @@ -5721,7 +5738,7 @@ dependencies = [ "serde", "smoltcp", "stm32h7", - "syn 2.0.98", + "syn 2.0.106", "task-jefe-api", "task-net-api", "task-packrat-api", @@ -6403,7 +6420,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -6764,7 +6781,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -6786,7 +6803,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7161,7 +7178,7 @@ checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -7172,7 +7189,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -7183,7 +7200,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -7203,7 +7220,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] diff --git a/lib/ereport-derive/Cargo.toml b/lib/ereport-derive/Cargo.toml new file mode 100644 index 000000000..4b780f8ea --- /dev/null +++ b/lib/ereport-derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ereport-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"] } + +[lints] +workspace = true diff --git a/lib/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs new file mode 100644 index 000000000..fd362ce83 --- /dev/null +++ b/lib/ereport-derive/src/lib.rs @@ -0,0 +1,263 @@ +// 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, quote}; +use syn::spanned::Spanned; +use syn::{ + Attribute, DataEnum, DataStruct, DeriveInput, Generics, Ident, LitStr, + Visibility, parse::Parse, parse_macro_input, +}; + +/// Derives an implementation of the `EreportData` trait for the annotated +/// `struct` or `enum` type. +#[proc_macro_derive(EreportData, attributes(ereport))] +pub fn derive_ereport_data(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match gen_ereport_data_impl(input) { + Ok(tokens) => tokens, + Err(err) => err.to_compile_error().into(), + } +} + +fn gen_ereport_data_impl( + input: DeriveInput, +) -> Result { + match &input.data { + syn::Data::Enum(data) => gen_enum_impl( + input.attrs, + input.vis, + input.ident, + input.generics, + data.clone(), + ) + .map(|tokens| tokens.to_token_stream().into()), + syn::Data::Struct(data) => gen_struct_impl( + input.attrs, + input.vis, + input.ident, + input.generics, + data.clone(), + ) + .map(|tokens| tokens.to_token_stream().into()), + _ => Err(syn::Error::new_spanned( + input, + "`EreportData` can only be derived for `struct` and `enum` types", + )), + } +} + +fn gen_enum_impl( + _attrs: Vec, + _vis: Visibility, + ident: Ident, + generics: Generics, + data: DataEnum, +) -> Result { + let mut variant_patterns = Vec::new(); + let mut variant_lens = Vec::new(); + // TODO(eliza): support top-level attribute for using the enum's repr instead of its name + for variant in data.variants { + let mut name = None; + for attr in &variant.attrs { + if attr.path().is_ident("ereport") { + attr.meta.require_list()?.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + name = Some(meta.value()?.parse::()?); + Ok(()) + } else { + Err(meta.error("expected `rename` attribute")) + } + })?; + }; + } + let name = name.unwrap_or_else(|| { + LitStr::new(&variant.ident.to_string(), variant.ident.span()) + }); + if !matches!(variant.fields, syn::Fields::Unit) { + return Err(syn::Error::new_spanned( + variant, + "`#[derive(EreportData)]` only supports unit variants for now", + )); + } + let variant_name = &variant.ident; + variant_patterns.push(quote! { + #ident::#variant_name => { e.str(#name)?; } + }); + variant_lens.push(quote! { + if ::ereport::str_cbor_len(#name) > max { + max = ::ereport::str_cbor_len(#name); + } + }); + } + + Ok(quote! { + #[automatically_derived] + impl #generics ::ereport::EreportData for #ident #generics { + const MAX_CBOR_LEN: usize = { + let mut max = 0; + #(#variant_lens;)* + max + }; + } + + impl ::ereport::encode::Encode for #ident #generics { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + _: &mut C, + ) -> Result<(), ::ereport::encode::Error> { + match self { + #(#variant_patterns,)* + } + Ok(()) + } + } + }) +} + +fn gen_struct_impl( + _attrs: Vec, + _vis: Visibility, + ident: Ident, + generics: Generics, + data: DataStruct, +) -> Result { + let mut field_len_exprs = Vec::new(); + let mut field_encode_exprs = Vec::new(); + let mut where_bounds = Vec::new(); + // let mut data_where_bounds = Vec::new(); + for field in &data.fields { + 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("ereport") { + attr.meta.require_list()?.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + 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") { + flattened = true; + Ok(()) + } else { + Err(meta.error( + "expected `rename`, `skip`, `skip_if_nil`, or `flatten` attribute", + )) + } + })?; + } + } + if skipped { + continue; + } + + let field_ident = &field.ident.as_ref().ok_or_else(|| { + syn::Error::new_spanned( + field, + "#[derive(EreportData)] doesn't support tuple structs yet", + ) + })?; + let field_name = field_name.unwrap_or_else(|| { + LitStr::new(&field_ident.to_string(), field_ident.span()) + }); + + // 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 { + where_bounds.push(quote! { + #field_type: ::ereport::EncodeFields<()> + }); + field_len_exprs.push(quote! { + len += <#field_type as ::ereport::EncodeFields<()>>::MAX_FIELDS_LEN; + }); + field_encode_exprs.push(quote! { + ::ereport::EncodeFields::<()>::encode_fields(&self.#field_ident, e, c)?; + }); + } else { + field_len_exprs.push(quote! { + len += ::ereport::str_cbor_len(#field_name); + len += <#field_type as ::ereport::EreportData>::MAX_CBOR_LEN; + }); + field_encode_exprs.push(if skipped_if_nil { + quote! { + if !::ereport::Encode::<()>::is_nil(&self.#field_ident) { + e.str(#field_name)?; + ::ereport::Encode::<()>::encode(&self.#field_ident, e, c)?; + } + } + } else { + quote! { + e.str(#field_name)?; + ::ereport::Encode::<()>::encode(&self.#field_ident, e, c)?; + } + }); + where_bounds.push(quote! { + #field_type: ::ereport::EreportData + }); + } + } + let (impl_generics, tygenerics, prev_where_clause) = + generics.split_for_impl(); + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::ereport::EreportData for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_CBOR_LEN: usize = + 2 // map begin and end bytes + + >::MAX_FIELDS_LEN; + } + + #[automatically_derived] + impl #impl_generics ::ereport::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + e.begin_map()?; + >::encode_fields(self, e, c)?; + e.end()?; + Ok(()) + } + } + + #[automatically_derived] + impl #impl_generics ::ereport::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, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + #(#field_encode_exprs;)* + Ok(()) + } + } + + }) +} diff --git a/lib/ereport/Cargo.toml b/lib/ereport/Cargo.toml new file mode 100644 index 000000000..8846d1a6e --- /dev/null +++ b/lib/ereport/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ereport" +version = "0.1.0" +edition = "2024" + +[dependencies] +minicbor.workspace = true +# static-assertions.workspace = true +ereport-derive = { path = "../ereport-derive" } + +[lints] +workspace = true diff --git a/lib/ereport/examples/test-expanded.rs b/lib/ereport/examples/test-expanded.rs new file mode 100644 index 000000000..42034d13d --- /dev/null +++ b/lib/ereport/examples/test-expanded.rs @@ -0,0 +1,121 @@ +#![feature(prelude_import)] +#[prelude_import] +use std::prelude::rust_2024::*; +#[macro_use] +extern crate std; +use ereport::EreportData; +pub enum TestEnum { + Variant1, + #[ereport(rename = "hw.foo.cool-ereport-class")] + Variant2, +} +#[automatically_derived] +impl ::core::fmt::Debug for TestEnum { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str( + f, + match self { + TestEnum::Variant1 => "Variant1", + TestEnum::Variant2 => "Variant2", + }, + ) + } +} +#[automatically_derived] +impl ::ereport::EreportData for TestEnum { + const MAX_CBOR_LEN: usize = { + let mut max = 0; + if ::ereport::str_cbor_len("Variant1") > max { + max = ::ereport::str_cbor_len("Variant1"); + } + if ::ereport::str_cbor_len("hw.foo.cool-ereport-class") > max { + max = ::ereport::str_cbor_len("hw.foo.cool-ereport-class"); + } + max + }; +} +impl ::ereport::encode::Encode for TestEnum { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + _: &mut C, + ) -> Result<(), ::ereport::encode::Error> { + match self { + TestEnum::Variant1 => e.str("Variant1")?, + TestEnum::Variant2 => e.str("hw.foo.cool-ereport-class")?, + } + Ok(()) + } +} +struct TestStruct { + #[ereport(rename = "a")] + field1: u32, + field2: TestEnum, +} +#[automatically_derived] +impl ::ereport::EreportData for TestStruct +where + u32: ::ereport::EreportData, + TestEnum: ::ereport::EreportData, +{ + const MAX_CBOR_LEN: usize = + 2 + >::MAX_FIELDS_LEN; +} +#[automatically_derived] +impl ::ereport::encode::Encode for TestStruct +where + u32: ::ereport::EreportData + ::ereport::Encode, + TestEnum: ::ereport::EreportData + ::ereport::Encode, +{ + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut C, + ) -> Result<(), ::ereport::encode::Error> { + e.begin_map()?; + >::encode_fields(self, e, c)?; + e.end()?; + Ok(()) + } +} +#[automatically_derived] +impl ::ereport::EncodeFields for TestStruct +where + u32: ::ereport::EreportData + ::ereport::Encode, + TestEnum: ::ereport::EreportData + ::ereport::Encode, +{ + const MAX_FIELDS_LEN: usize = { + let mut len = 0; + len += ::ereport::str_cbor_len("a"); + len += ::MAX_CBOR_LEN; + len += ::ereport::str_cbor_len("field2"); + len += ::MAX_CBOR_LEN; + len + }; + fn encode_fields( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut C, + ) -> Result<(), ::ereport::encode::Error> { + e.str("a")?; + ::ereport::Encode::::encode(&self.field1, e, c)?; + e.str("field2")?; + ::ereport::Encode::::encode(&self.field2, e, c)?; + Ok(()) + } +} +fn main() { + // { + // ::std::io::_print(format_args!( + // "TestEnum::MAX_CBOR_LEN = {0}\n", + // TestEnum::MAX_CBOR_LEN + // )); + // }; + // { + // ::std::io::_print(format_args!( + // "TestStruct::MAX_CBOR_LEN = {0}\n", + // TestStruct::MAX_CBOR_LEN + // )); + // }; +} diff --git a/lib/ereport/examples/test.rs b/lib/ereport/examples/test.rs new file mode 100644 index 000000000..23d1db4c8 --- /dev/null +++ b/lib/ereport/examples/test.rs @@ -0,0 +1,37 @@ +// 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 ereport::EreportData; + +#[derive(Debug, EreportData)] +pub enum TestEnum { + Variant1, + #[ereport(rename = "hw.foo.cool-ereport-class")] + Variant2, +} + +#[derive(EreportData)] +struct TestStruct { + #[ereport(rename = "a")] + field1: u32, + field2: TestEnum, +} + +#[derive(EreportData)] +struct TestStruct2 { + #[ereport(skip_if_nil)] + field6: Option, + #[ereport(flatten)] + inner: D, +} + +fn main() { + println!("TestEnum::MAX_CBOR_LEN = {}", TestEnum::MAX_CBOR_LEN); + println!("TestStruct::MAX_CBOR_LEN = {}", TestStruct::MAX_CBOR_LEN); + + println!( + "TestStruct2::::MAX_CBOR_LEN = {}", + TestStruct2::::MAX_CBOR_LEN + ); +} diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs new file mode 100644 index 000000000..abc643239 --- /dev/null +++ b/lib/ereport/src/lib.rs @@ -0,0 +1,133 @@ +// 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/. + +//! Hubris ereport traits. +#![no_std] + +use encode::{Encoder, Write}; +pub use ereport_derive::EreportData; +pub use minicbor::encode::{self, Encode}; + +pub trait EreportData: 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; +} + +pub trait EncodeFields { + const MAX_FIELDS_LEN: usize; + + fn encode_fields( + &self, + e: &mut Encoder, + _: &mut C, + ) -> Result<(), encode::Error>; +} + +pub struct FixedStr { + buf: [u8; LEN], + len: usize, +} + +impl FixedStr { + pub const fn from_str(s: &str) -> Self { + todo!() + } + + pub fn as_str(&self) -> &str { + unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +impl Encode for FixedStr { + fn encode( + &self, + e: &mut Encoder, + _: &mut C, + ) -> Result<(), encode::Error> { + e.str(self.as_str())?; + Ok(()) + } +} + +impl EreportData for FixedStr { + const MAX_CBOR_LEN: usize = LEN + usize::MAX_CBOR_LEN; +} + +impl EreportData for u8 { + // A u8 may require up to 2 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#513 + const MAX_CBOR_LEN: usize = 2; +} + +impl EreportData for u16 { + // A u16 may require up to 3 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#519-523 + const MAX_CBOR_LEN: usize = 3; +} + +impl EreportData for u32 { + // A u32 may require up to 5 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#529-534 + const MAX_CBOR_LEN: usize = 5; +} + +impl EreportData for u64 { + // A u64 may require up to 9 bytes, see: + // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#539-546 + const MAX_CBOR_LEN: usize = 9; +} + +impl EreportData 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; +} + +pub const fn str_cbor_len(s: &str) -> usize { + usize_cbor_len(s.len()) + s.len() +} + +#[cfg(target_pointer_width = "32")] +pub const fn usize_cbor_len(u: usize) -> usize { + u32_cbor_len(u as u32) +} + +#[cfg(target_pointer_width = "64")] +pub const fn usize_cbor_len(u: usize) -> usize { + u64_cbor_len(u as u64) +} + +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, + } +} + +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, + } +} From a8a6c643f9ca589db0811740f6f9b8e2225dd8a4 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Sep 2025 15:25:38 -0700 Subject: [PATCH 02/30] more worky --- lib/ereport/src/lib.rs | 47 ++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index abc643239..20b60941c 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -65,28 +65,41 @@ impl EreportData for FixedStr { const MAX_CBOR_LEN: usize = LEN + usize::MAX_CBOR_LEN; } -impl EreportData for u8 { +macro_rules! impl_ereport_data { + ($($T:ty = $len:expr),*$(,)?) => { + $( + impl EreportData for $T { + const MAX_CBOR_LEN: usize = $len; + } + )* + }; +} + +impl_ereport_data! { // A u8 may require up to 2 bytes, see: // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#513 - const MAX_CBOR_LEN: usize = 2; -} + u8 = 2, -impl EreportData for u16 { // A u16 may require up to 3 bytes, see: // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#519-523 - const MAX_CBOR_LEN: usize = 3; -} + u16 = 3, -impl EreportData for u32 { // A u32 may require up to 5 bytes, see: // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#529-534 - const MAX_CBOR_LEN: usize = 5; -} + u32 = 5, -impl EreportData for u64 { // A u64 may require up to 9 bytes, see: // https://docs.rs/minicbor/2.1.1/src/minicbor/encode.rs.html#539-546 - const MAX_CBOR_LEN: usize = 9; + 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 EreportData for usize { @@ -97,6 +110,18 @@ impl EreportData for usize { const MAX_CBOR_LEN: usize = u64::MAX_CBOR_LEN; } +impl EreportData 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 EreportData for &T { + const MAX_CBOR_LEN: usize = T::MAX_CBOR_LEN; +} + pub const fn str_cbor_len(s: &str) -> usize { usize_cbor_len(s.len()) + s.len() } From c51c5bf7f03c7903051d6821b0b69129308a66f8 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Sep 2025 15:36:25 -0700 Subject: [PATCH 03/30] reticulating --- lib/ereport/Cargo.toml | 1 - lib/ereport/src/lib.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/ereport/Cargo.toml b/lib/ereport/Cargo.toml index 8846d1a6e..231f19522 100644 --- a/lib/ereport/Cargo.toml +++ b/lib/ereport/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] minicbor.workspace = true -# static-assertions.workspace = true ereport-derive = { path = "../ereport-derive" } [lints] diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 20b60941c..145d94a4a 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -9,6 +9,17 @@ use encode::{Encoder, Write}; pub use ereport_derive::EreportData; pub use minicbor::encode::{self, Encode}; +/// Wrapper type defining common ereport fields. +#[derive(Clone, EreportData)] +pub struct Ereport { + #[ereport(rename = "k")] + pub class: C, + #[ereport(rename = "v")] + pub version: u32, + #[ereport(flatten)] + pub report: D, +} + pub trait EreportData: Encode<()> { /// The maximum length of the CBOR-encoded representation of this value. /// @@ -17,6 +28,21 @@ pub trait EreportData: Encode<()> { const MAX_CBOR_LEN: usize; } +#[macro_export] +macro_rules! max_cbor_len_of { + ($($T:ty),+) => { + { + let mut len = 0; + $( + if <$T as $crate::EreportData>::MAX_CBOR_LEN > len { + len = <$T as $crate::EreportData>::MAX_CBOR_LEN; + } + )+ + len + } + }; +} + pub trait EncodeFields { const MAX_FIELDS_LEN: usize; From fb1abc6b8f2b3dc28deb5b57dafa2ff7407e936e Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Sep 2025 15:39:23 -0700 Subject: [PATCH 04/30] oh that's not compiley --- lib/ereport/src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 145d94a4a..27fea8732 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -9,16 +9,16 @@ use encode::{Encoder, Write}; pub use ereport_derive::EreportData; pub use minicbor::encode::{self, Encode}; -/// Wrapper type defining common ereport fields. -#[derive(Clone, EreportData)] -pub struct Ereport { - #[ereport(rename = "k")] - pub class: C, - #[ereport(rename = "v")] - pub version: u32, - #[ereport(flatten)] - pub report: D, -} +// /// Wrapper type defining common ereport fields. +// #[derive(Clone, EreportData)] +// pub struct Ereport { +// #[ereport(rename = "k")] +// pub class: C, +// #[ereport(rename = "v")] +// pub version: u32, +// #[ereport(flatten)] +// pub report: D, +// } pub trait EreportData: Encode<()> { /// The maximum length of the CBOR-encoded representation of this value. From c60aa0c725925175d8a660e3fc15bbba9ed402a7 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Sep 2025 15:50:40 -0700 Subject: [PATCH 05/30] finish fixedstr --- Cargo.lock | 1 + lib/ereport/src/lib.rs | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e374d2d31..6118bd712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1261,6 +1261,7 @@ dependencies = [ "drv-spi-api", "drv-stm32h7-spi", "drv-stm32xx-sys-api", + "ereport", "gnarle", "idol", "idol-runtime", diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 27fea8732..3b1877ca0 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -60,11 +60,28 @@ pub struct FixedStr { impl FixedStr { pub const fn from_str(s: &str) -> Self { - todo!() + let mut buf = [0; LEN]; + let bytes = s.as_bytes(); + let len = bytes.len(); + if len > LEN { + panic!(); + } + + // 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; + } + Self { buf, len } } pub fn as_str(&self) -> &str { - unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) } + 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.buf[..self.len]) + } } pub fn len(&self) -> usize { From c2ede04b3223b26bcfcd11e2c06cb9d8e5d4a7cc Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 23 Sep 2025 16:43:46 -0700 Subject: [PATCH 06/30] fix warnings --- Cargo.lock | 1 - lib/ereport-derive/src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6118bd712..e374d2d31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1261,7 +1261,6 @@ dependencies = [ "drv-spi-api", "drv-stm32h7-spi", "drv-stm32xx-sys-api", - "ereport", "gnarle", "idol", "idol-runtime", diff --git a/lib/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs index fd362ce83..d4678d492 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/ereport-derive/src/lib.rs @@ -8,7 +8,7 @@ use quote::{ToTokens, quote}; use syn::spanned::Spanned; use syn::{ Attribute, DataEnum, DataStruct, DeriveInput, Generics, Ident, LitStr, - Visibility, parse::Parse, parse_macro_input, + Visibility, parse_macro_input, }; /// Derives an implementation of the `EreportData` trait for the annotated From b7bdc71c2cdf8ddf5d55a6fe5c94e52662fe16f8 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 24 Sep 2025 10:59:30 -0700 Subject: [PATCH 07/30] make fixedstr a crate --- Cargo.lock | 9 ++ lib/ereport/Cargo.toml | 1 + lib/ereport/src/lib.rs | 54 +----------- lib/fixedstr/Cargo.toml | 11 +++ lib/fixedstr/src/lib.rs | 183 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 52 deletions(-) create mode 100644 lib/fixedstr/Cargo.toml create mode 100644 lib/fixedstr/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e374d2d31..2cfb55a67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2920,6 +2920,7 @@ name = "ereport" version = "0.1.0" dependencies = [ "ereport-derive", + "fixedstr", "minicbor 2.1.1", ] @@ -2989,6 +2990,14 @@ dependencies = [ name = "fixedmap" version = "0.1.0" +[[package]] +name = "fixedstr" +version = "0.1.0" +dependencies = [ + "minicbor 2.1.1", + "serde", +] + [[package]] name = "flagset" version = "0.4.3" diff --git a/lib/ereport/Cargo.toml b/lib/ereport/Cargo.toml index 231f19522..d6cf8a9e7 100644 --- a/lib/ereport/Cargo.toml +++ b/lib/ereport/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] minicbor.workspace = true ereport-derive = { path = "../ereport-derive" } +fixedstr = { path = "../fixedstr", optional = true } [lints] workspace = true diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 3b1877ca0..dd89a7c38 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -53,58 +53,8 @@ pub trait EncodeFields { ) -> Result<(), encode::Error>; } -pub struct FixedStr { - buf: [u8; LEN], - len: usize, -} - -impl FixedStr { - pub const fn from_str(s: &str) -> Self { - let mut buf = [0; LEN]; - let bytes = s.as_bytes(); - let len = bytes.len(); - if len > LEN { - panic!(); - } - - // 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; - } - Self { buf, len } - } - - 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.buf[..self.len]) - } - } - - pub fn len(&self) -> usize { - self.len - } - - pub fn is_empty(&self) -> bool { - self.len == 0 - } -} - -impl Encode for FixedStr { - fn encode( - &self, - e: &mut Encoder, - _: &mut C, - ) -> Result<(), encode::Error> { - e.str(self.as_str())?; - Ok(()) - } -} - -impl EreportData for FixedStr { +#[cfg(feature = "fixedstr")] +impl EreportData for fixedstr::FixedStr { const MAX_CBOR_LEN: usize = LEN + usize::MAX_CBOR_LEN; } diff --git a/lib/fixedstr/Cargo.toml b/lib/fixedstr/Cargo.toml new file mode 100644 index 000000000..4b0ba8e52 --- /dev/null +++ b/lib/fixedstr/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fixedstr" +version = "0.1.0" +edition = "2024" + +[dependencies] +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 000000000..467ec844c --- /dev/null +++ b/lib/fixedstr/src/lib.rs @@ -0,0 +1,183 @@ +// 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`. +//! +#![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(_) => 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() + } +} + +#[cfg(feature = "minicbor")] +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 {}", self.0) + } + } + let s = <&'de str>::deserialize(deserializer)?; + Self::try_from_str(s).map_err(|_| { + serde::de::Error::invalid_length(s.len(), &ExpectedLen(MAX)) + }) + } +} From 2c3b7e802f193adc50aa87480f00a8c554731b61 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 24 Sep 2025 11:30:59 -0700 Subject: [PATCH 08/30] wip --- Cargo.lock | 3 +++ drv/gimlet-seq-server/Cargo.toml | 1 + drv/gimlet-seq-server/src/main.rs | 6 +++++ lib/ereport/Cargo.toml | 2 +- lib/ereport/src/lib.rs | 11 -------- task/packrat-api/Cargo.toml | 4 +++ task/packrat-api/src/lib.rs | 44 ++++++++++++++++++++++++++++++- 7 files changed, 58 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2cfb55a67..7fecd3723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1261,6 +1261,7 @@ dependencies = [ "drv-spi-api", "drv-stm32h7-spi", "drv-stm32xx-sys-api", + "ereport", "gnarle", "idol", "idol-runtime", @@ -5832,6 +5833,7 @@ dependencies = [ "anyhow", "counters", "derive-idol-err", + "ereport", "gateway-ereport-messages", "host-sp-messages", "idol", @@ -5841,6 +5843,7 @@ dependencies = [ "num-traits", "oxide-barcode", "serde", + "static_assertions", "userlib", "zerocopy 0.8.26", "zerocopy-derive 0.8.26", diff --git a/drv/gimlet-seq-server/Cargo.toml b/drv/gimlet-seq-server/Cargo.toml index 1e7495340..7e44998c2 100644 --- a/drv/gimlet-seq-server/Cargo.toml +++ b/drv/gimlet-seq-server/Cargo.toml @@ -21,6 +21,7 @@ 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" } +ereport = { path = "../../lib/ereport", features = ["fixedstr"] } 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 8b56233a7..36b719408 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -209,6 +209,12 @@ struct ServerImpl { const TIMER_INTERVAL: u32 = 10; const EREPORT_BUF_LEN: usize = 256; +#[derive(EreportData)] +pub enum EreportClass { + #[ereport(rename = "hw.pwr.pmbus.alert")] + PmbusAlert, +} + impl ServerImpl { fn init( sys: &sys_api::Sys, diff --git a/lib/ereport/Cargo.toml b/lib/ereport/Cargo.toml index d6cf8a9e7..9832c8040 100644 --- a/lib/ereport/Cargo.toml +++ b/lib/ereport/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] minicbor.workspace = true ereport-derive = { path = "../ereport-derive" } -fixedstr = { path = "../fixedstr", optional = true } +fixedstr = { path = "../fixedstr", optional = true, features = ["minicbor"] } [lints] workspace = true diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index dd89a7c38..bda67d028 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -9,17 +9,6 @@ use encode::{Encoder, Write}; pub use ereport_derive::EreportData; pub use minicbor::encode::{self, Encode}; -// /// Wrapper type defining common ereport fields. -// #[derive(Clone, EreportData)] -// pub struct Ereport { -// #[ereport(rename = "k")] -// pub class: C, -// #[ereport(rename = "v")] -// pub version: u32, -// #[ereport(flatten)] -// pub report: D, -// } - pub trait EreportData: Encode<()> { /// The maximum length of the CBOR-encoded representation of this value. /// diff --git a/task/packrat-api/Cargo.toml b/task/packrat-api/Cargo.toml index 5ab2f1c53..a97507fd7 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"] +ereport = ["dep:ereport", "dep:static_assertions", "dep:minicbor"] +default = ["ereport"] [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" +ereport = { path = "../../lib/ereport", 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 90db9c593..c7a8c712e 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -12,6 +12,8 @@ use zerocopy::{ FromBytes, Immutable, IntoBytes, KnownLayout, LittleEndian, U16, }; +#[cfg(feature = "ereport")] +use ereport::EreportData; pub use gateway_ereport_messages as ereport_messages; pub use host_sp_messages::HostStartupOptions; pub use oxide_barcode::VpdIdentity; @@ -85,11 +87,23 @@ pub enum EreportSerializeError { ), } -#[cfg(feature = "serde")] +/// Wrapper type defining common ereport fields. +#[cfg(feature = "ereport")] +#[derive(Clone, EreportData)] +pub struct Ereport { + #[ereport(rename = "k")] + pub class: C, + #[ereport(rename = "v")] + pub version: u32, + #[ereport(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 +131,34 @@ impl Packrat { Ok(len) } + + #[cfg(feature = "ereport")] + pub fn deliver_ereport_data( + &self, + ereport: &E, + ) -> Result { + // XXX(eliza): i don't love that this puts the buffer on the stack; I + // wanted to make it take a buffer as an argument `&mut [u8; LEN]` and + // then `static_assertions::const_assert!(LEN >= E::MAX_CBOR_LEN)`, but + // this doesn't work as the outer const generic parameters for the + // buffer length and the ereport max CBOR length cannot be accessed in a + // `const` expression inside the function.` + let mut buf = [0u8; E::MAX_CBOR_LEN]; + let c = minicbor::encode::write::Cursor::new(&mut buf[..]); + let mut e = minicbor::encode::Encoder::new(c); + match ereport.encode(&mut e, &mut ()) { + Ok(()) => (), + Err(_) => unreachable!(), + } + let writer = e.into_writer(); + let len = writer.position(); + let buf = writer.into_inner(); + + // Now, try to send that to Packrat. + self.deliver_ereport(&buf[..len])?; + + Ok(len) + } } include!(concat!(env!("OUT_DIR"), "/client_stub.rs")); From 3c4d208fb3c04f20503a502b791972bf2ac6071c Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 26 Sep 2025 11:35:36 -0700 Subject: [PATCH 09/30] support enums with fields --- lib/ereport-derive/src/lib.rs | 258 ++++++++++++++++++++++++++++------ lib/ereport/Cargo.toml | 4 + lib/ereport/examples/test.rs | 98 ++++++++++++- lib/ereport/src/lib.rs | 2 +- lib/fixedstr/src/lib.rs | 12 ++ task/packrat-api/src/lib.rs | 28 ---- 6 files changed, 326 insertions(+), 76 deletions(-) diff --git a/lib/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs index d4678d492..6000a36a7 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/ereport-derive/src/lib.rs @@ -5,7 +5,6 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::{ToTokens, quote}; -use syn::spanned::Spanned; use syn::{ Attribute, DataEnum, DataStruct, DeriveInput, Generics, Ident, LitStr, Visibility, parse_macro_input, @@ -58,6 +57,8 @@ fn gen_enum_impl( ) -> Result { let mut variant_patterns = Vec::new(); let mut variant_lens = Vec::new(); + let mut flattened = Some((Vec::new(), Vec::new())); + let mut all_where_bounds = Vec::new(); // TODO(eliza): support top-level attribute for using the enum's repr instead of its name for variant in data.variants { let mut name = None; @@ -76,26 +77,140 @@ fn gen_enum_impl( let name = name.unwrap_or_else(|| { LitStr::new(&variant.ident.to_string(), variant.ident.span()) }); - if !matches!(variant.fields, syn::Fields::Unit) { - return Err(syn::Error::new_spanned( - variant, - "`#[derive(EreportData)]` only supports unit variants for now", - )); - } + let variant_name = &variant.ident; - variant_patterns.push(quote! { - #ident::#variant_name => { e.str(#name)?; } - }); - variant_lens.push(quote! { - if ::ereport::str_cbor_len(#name) > max { - max = ::ereport::str_cbor_len(#name); + match variant.fields { + syn::Fields::Unit => { + // If there's a unit variant, we cannot generate an + // `EncodeField` impl for flattening this type. + flattened = None; + variant_patterns.push(quote! { + #ident::#variant_name => { e.str(#name)?; } + }); + variant_lens.push(quote! { + if ::ereport::str_cbor_len(#name) > max { + max = ::ereport::str_cbor_len(#name); + } + }); } - }); + syn::Fields::Named(ref fields) => { + let mut field_gen = FieldGenerator::for_variant(); + for field in &fields.named { + field_gen.add_field(field)?; + } + let FieldGenerator { + field_idents, + field_len_exprs, + field_encode_exprs, + where_bounds, + any_skipped, + .. + } = field_gen; + all_where_bounds.extend(where_bounds); + let ignore_pattern = if any_skipped { + vec![quote!(..)] + } else { + vec![] + }; + let match_pattern = quote! { + #ident::#variant_name { #(#field_idents,)* #(#ignore_pattern)*} + }; + variant_patterns.push(quote! { + #match_pattern => { + e.begin_map()?; + #(#field_encode_exprs)* + e.end()?; + } + }); + variant_lens.push(quote! { + #[allow(non_camel_case_names)] + let #variant_name = { + let mut len = 2; // map begin and end bytes + #(#field_len_exprs;)* + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + // If we are still able to generate a flattened impl, add to that. + if let Some(( + ref mut flattened_lens, + ref mut flattened_patterns, + )) = flattened + { + flattened_lens.push(quote! { + #[allow(non_camel_case_names)] + let #variant_name = { + let mut len = 0; // no map begin and end bytes, as we are flattening + #(#field_len_exprs;)* + len + }; + if #variant_name > max { + max = #variant_name; + } + }); + flattened_patterns.push(quote! { + #match_pattern => { + #(#field_encode_exprs)* + } + }); + } + } + _ => { + return Err(syn::Error::new_spanned( + variant, + "`#[derive(EreportData)]` only supports unit and named fields variants for now", + )); + } + } } + let (impl_generics, tygenerics, prev_where_clause) = + generics.split_for_impl(); + + // If all variants of this enum contain multiple named fields (and can + // therefore be flattened into an enclosing struct), generate an + // `EncodeFields` impl. + let maybe_fields_impl = + if let Some((flattened_lens, flattened_encode_patterns)) = flattened { + quote! { + #[automatically_derived] + impl #impl_generics ::ereport::EncodeFields<()> + for #ident #tygenerics + #prev_where_clause + where #(#all_where_bounds,)* + { + const MAX_FIELDS_LEN: usize = { + let mut max = 0; + #(#flattened_lens;)* + max + }; + + fn encode_fields( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + match self { + #(#flattened_encode_patterns,)* + } + Ok(()) + } + } + } + } else { + quote! {} + }; Ok(quote! { + #maybe_fields_impl + #[automatically_derived] - impl #generics ::ereport::EreportData for #ident #generics { + impl #impl_generics ::ereport::EreportData + for #ident #tygenerics + #prev_where_clause + where #(#all_where_bounds,)* + { const MAX_CBOR_LEN: usize = { let mut max = 0; #(#variant_lens;)* @@ -103,11 +218,16 @@ fn gen_enum_impl( }; } - impl ::ereport::encode::Encode for #ident #generics { + #[automatically_derived] + impl #impl_generics ::ereport::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#all_where_bounds,)* + { fn encode( &self, e: &mut ::ereport::encode::Encoder, - _: &mut C, + c: &mut (), ) -> Result<(), ::ereport::encode::Error> { match self { #(#variant_patterns,)* @@ -118,18 +238,48 @@ fn gen_enum_impl( }) } -fn gen_struct_impl( - _attrs: Vec, - _vis: Visibility, - ident: Ident, - generics: Generics, - data: DataStruct, -) -> Result { - let mut field_len_exprs = Vec::new(); - let mut field_encode_exprs = Vec::new(); - let mut where_bounds = Vec::new(); - // let mut data_where_bounds = Vec::new(); - for field in &data.fields { +#[derive(Default)] +struct FieldGenerator<'fields> { + // XXX(eliza): This really ought to be an `Option`, since there's always + // either one token stream, or none. But, `quote!`'s repetition handles + // `Vec`s nicer than `Option`s, since we would have to separately create an + // `Iterator` over the option for every time the expression is interpolated. + // Sigh. + self_expr: Vec, + field_idents: Vec<&'fields syn::Ident>, + field_len_exprs: Vec, + field_encode_exprs: Vec, + where_bounds: Vec, + any_skipped: bool, +} + +impl<'fields> FieldGenerator<'fields> { + fn for_struct() -> Self { + Self { + self_expr: vec![quote! { &self. }], + field_idents: Vec::new(), + field_len_exprs: Vec::new(), + field_encode_exprs: Vec::new(), + where_bounds: Vec::new(), + any_skipped: false, + } + } + + fn for_variant() -> Self { + Self { + self_expr: vec![], + field_idents: Vec::new(), + field_len_exprs: Vec::new(), + field_encode_exprs: Vec::new(), + where_bounds: Vec::new(), + any_skipped: false, + } + } + + fn add_field( + &mut self, + field: &'fields syn::Field, + ) -> Result<(), syn::Error> { let mut field_name = None; let mut skipped = false; let mut flattened = false; @@ -158,10 +308,11 @@ fn gen_struct_impl( } } if skipped { - continue; + self.any_skipped = true; + return Ok(()); } - let field_ident = &field.ident.as_ref().ok_or_else(|| { + let field_ident = field.ident.as_ref().ok_or_else(|| { syn::Error::new_spanned( field, "#[derive(EreportData)] doesn't support tuple structs yet", @@ -170,44 +321,69 @@ fn gen_struct_impl( let field_name = field_name.unwrap_or_else(|| { LitStr::new(&field_ident.to_string(), field_ident.span()) }); + self.field_idents.push(field_ident); // 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; + let self_expr = &self.self_expr; if flattened { - where_bounds.push(quote! { + self.where_bounds.push(quote! { #field_type: ::ereport::EncodeFields<()> }); - field_len_exprs.push(quote! { + self.field_len_exprs.push(quote! { len += <#field_type as ::ereport::EncodeFields<()>>::MAX_FIELDS_LEN; }); - field_encode_exprs.push(quote! { - ::ereport::EncodeFields::<()>::encode_fields(&self.#field_ident, e, c)?; + self.field_encode_exprs.push(quote! { + ::ereport::EncodeFields::<()>::encode_fields(#(#self_expr)*#field_ident, e, c)?; }); } else { - field_len_exprs.push(quote! { + self.field_len_exprs.push(quote! { len += ::ereport::str_cbor_len(#field_name); len += <#field_type as ::ereport::EreportData>::MAX_CBOR_LEN; }); - field_encode_exprs.push(if skipped_if_nil { + self.field_encode_exprs.push(if skipped_if_nil { quote! { - if !::ereport::Encode::<()>::is_nil(&self.#field_ident) { + if !::ereport::Encode::<()>::is_nil(#(#self_expr)*#field_ident) { e.str(#field_name)?; - ::ereport::Encode::<()>::encode(&self.#field_ident, e, c)?; + ::ereport::Encode::<()>::encode(#(#self_expr)*#field_ident, e, c)?; } } } else { quote! { e.str(#field_name)?; - ::ereport::Encode::<()>::encode(&self.#field_ident, e, c)?; + ::ereport::Encode::<()>::encode(#(#self_expr)*#field_ident, e, c)?; } }); - where_bounds.push(quote! { + self.where_bounds.push(quote! { #field_type: ::ereport::EreportData }); } + + Ok(()) + } +} + +fn gen_struct_impl( + _attrs: Vec, + _vis: Visibility, + ident: Ident, + generics: Generics, + data: DataStruct, +) -> Result { + let mut field_gen = FieldGenerator::for_struct(); + // let mut data_where_bounds = Vec::new(); + 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_gen; Ok(quote! { #[automatically_derived] impl #impl_generics ::ereport::EreportData for #ident #tygenerics diff --git a/lib/ereport/Cargo.toml b/lib/ereport/Cargo.toml index 9832c8040..041f49491 100644 --- a/lib/ereport/Cargo.toml +++ b/lib/ereport/Cargo.toml @@ -10,3 +10,7 @@ fixedstr = { path = "../fixedstr", optional = true, features = ["minicbor"] } [lints] workspace = true + +[dev-dependencies] +minicbor = { workspace = true, features = ["alloc", "half"]} +fixedstr = { path = "../fixedstr", features = ["minicbor"] } diff --git a/lib/ereport/examples/test.rs b/lib/ereport/examples/test.rs index 23d1db4c8..909be0d31 100644 --- a/lib/ereport/examples/test.rs +++ b/lib/ereport/examples/test.rs @@ -11,14 +11,14 @@ pub enum TestEnum { Variant2, } -#[derive(EreportData)] +#[derive(Debug, EreportData)] struct TestStruct { #[ereport(rename = "a")] field1: u32, field2: TestEnum, } -#[derive(EreportData)] +#[derive(Debug, EreportData)] struct TestStruct2 { #[ereport(skip_if_nil)] field6: Option, @@ -26,12 +26,98 @@ struct TestStruct2 { inner: D, } +#[derive(Debug, EreportData)] +enum TestEnum2 { + Flattened { + #[ereport(flatten)] + flattened: D, + bar: u32, + }, + Nested { + nested: D, + quux: f32, + }, +} + fn main() { - println!("TestEnum::MAX_CBOR_LEN = {}", TestEnum::MAX_CBOR_LEN); - println!("TestStruct::MAX_CBOR_LEN = {}", TestStruct::MAX_CBOR_LEN); + const MAX_LEN: usize = ereport::max_cbor_len_for! { + TestEnum, + TestStruct2, + TestEnum2, + 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, + ); +} + +fn test_one_type(input: T, buf: &mut [u8]) { println!( - "TestStruct2::::MAX_CBOR_LEN = {}", - TestStruct2::::MAX_CBOR_LEN + "{}::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/ereport/src/lib.rs b/lib/ereport/src/lib.rs index bda67d028..e3459c167 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -18,7 +18,7 @@ pub trait EreportData: Encode<()> { } #[macro_export] -macro_rules! max_cbor_len_of { +macro_rules! max_cbor_len_for { ($($T:ty),+) => { { let mut len = 0; diff --git a/lib/fixedstr/src/lib.rs b/lib/fixedstr/src/lib.rs index 467ec844c..0a267f5a0 100644 --- a/lib/fixedstr/src/lib.rs +++ b/lib/fixedstr/src/lib.rs @@ -138,6 +138,18 @@ where } } +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 = "minicbor")] impl minicbor::encode::Encode for FixedStr { fn encode( diff --git a/task/packrat-api/src/lib.rs b/task/packrat-api/src/lib.rs index c7a8c712e..596b571cc 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -131,34 +131,6 @@ impl Packrat { Ok(len) } - - #[cfg(feature = "ereport")] - pub fn deliver_ereport_data( - &self, - ereport: &E, - ) -> Result { - // XXX(eliza): i don't love that this puts the buffer on the stack; I - // wanted to make it take a buffer as an argument `&mut [u8; LEN]` and - // then `static_assertions::const_assert!(LEN >= E::MAX_CBOR_LEN)`, but - // this doesn't work as the outer const generic parameters for the - // buffer length and the ereport max CBOR length cannot be accessed in a - // `const` expression inside the function.` - let mut buf = [0u8; E::MAX_CBOR_LEN]; - let c = minicbor::encode::write::Cursor::new(&mut buf[..]); - let mut e = minicbor::encode::Encoder::new(c); - match ereport.encode(&mut e, &mut ()) { - Ok(()) => (), - Err(_) => unreachable!(), - } - let writer = e.into_writer(); - let len = writer.position(); - let buf = writer.into_inner(); - - // Now, try to send that to Packrat. - self.deliver_ereport(&buf[..len])?; - - Ok(len) - } } include!(concat!(env!("OUT_DIR"), "/client_stub.rs")); From 68231f7b8759831e83723c529808ed09804c2e4f Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 26 Sep 2025 11:36:19 -0700 Subject: [PATCH 10/30] reorganize derive --- lib/ereport-derive/src/lib.rs | 150 +++++++++++++++++----------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/lib/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs index 6000a36a7..183d59f3f 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/ereport-derive/src/lib.rs @@ -238,6 +238,81 @@ fn gen_enum_impl( }) } +fn gen_struct_impl( + _attrs: Vec, + _vis: Visibility, + ident: Ident, + generics: Generics, + data: DataStruct, +) -> Result { + let mut field_gen = FieldGenerator::for_struct(); + // let mut data_where_bounds = Vec::new(); + 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_gen; + Ok(quote! { + #[automatically_derived] + impl #impl_generics ::ereport::EreportData for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_CBOR_LEN: usize = + 2 // map begin and end bytes + + >::MAX_FIELDS_LEN; + } + + #[automatically_derived] + impl #impl_generics ::ereport::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + e.begin_map()?; + >::encode_fields(self, e, c)?; + e.end()?; + Ok(()) + } + } + + #[automatically_derived] + impl #impl_generics ::ereport::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, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + #(#field_encode_exprs;)* + Ok(()) + } + } + + }) +} + #[derive(Default)] struct FieldGenerator<'fields> { // XXX(eliza): This really ought to be an `Option`, since there's always @@ -362,78 +437,3 @@ impl<'fields> FieldGenerator<'fields> { Ok(()) } } - -fn gen_struct_impl( - _attrs: Vec, - _vis: Visibility, - ident: Ident, - generics: Generics, - data: DataStruct, -) -> Result { - let mut field_gen = FieldGenerator::for_struct(); - // let mut data_where_bounds = Vec::new(); - 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_gen; - Ok(quote! { - #[automatically_derived] - impl #impl_generics ::ereport::EreportData for #ident #tygenerics - #prev_where_clause - where #(#where_bounds,)* - { - const MAX_CBOR_LEN: usize = - 2 // map begin and end bytes - + >::MAX_FIELDS_LEN; - } - - #[automatically_derived] - impl #impl_generics ::ereport::encode::Encode<()> - for #ident #tygenerics - #prev_where_clause - where #(#where_bounds,)* - { - fn encode( - &self, - e: &mut ::ereport::encode::Encoder, - c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { - e.begin_map()?; - >::encode_fields(self, e, c)?; - e.end()?; - Ok(()) - } - } - - #[automatically_derived] - impl #impl_generics ::ereport::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, - e: &mut ::ereport::encode::Encoder, - c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { - #(#field_encode_exprs;)* - Ok(()) - } - } - - }) -} From e2e510621801ec7635026a3a915d57fad020288a Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 26 Sep 2025 11:53:45 -0700 Subject: [PATCH 11/30] worky demo in gimlet-seq --- Cargo.lock | 34 ++++++++++++++- drv/gimlet-seq-server/Cargo.toml | 1 + drv/gimlet-seq-server/src/main.rs | 29 ++++++++++++- drv/gimlet-seq-server/src/vcore.rs | 70 ++++++++++++------------------ lib/ereport-derive/src/lib.rs | 4 +- task/packrat-api/src/lib.rs | 36 +++++++++++++++ 6 files changed, 127 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fecd3723..3c258afa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,6 +772,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" @@ -1262,6 +1268,7 @@ dependencies = [ "drv-stm32h7-spi", "drv-stm32xx-sys-api", "ereport", + "fixedstr", "gnarle", "idol", "idol-runtime", @@ -3169,6 +3176,16 @@ dependencies = [ "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" @@ -3902,7 +3919,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]] @@ -3910,6 +3927,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" @@ -3922,6 +3943,17 @@ dependencies = [ "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]] name = "minicbor-lease" version = "0.1.0" diff --git a/drv/gimlet-seq-server/Cargo.toml b/drv/gimlet-seq-server/Cargo.toml index 7e44998c2..38e065b54 100644 --- a/drv/gimlet-seq-server/Cargo.toml +++ b/drv/gimlet-seq-server/Cargo.toml @@ -22,6 +22,7 @@ task-packrat-api = { path = "../../task/packrat-api", features = ["serde"] } userlib = { path = "../../sys/userlib", features = ["panic-messages"] } static-cell = { path = "../../lib/static-cell" } ereport = { path = "../../lib/ereport", features = ["fixedstr"] } +fixedstr = { path = "../../lib/fixedstr" } 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 36b719408..dd08b9012 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -207,14 +207,39 @@ struct ServerImpl { } const TIMER_INTERVAL: u32 = 10; -const EREPORT_BUF_LEN: usize = 256; +const EREPORT_BUF_LEN: usize = as ereport::EreportData>::MAX_CBOR_LEN; -#[derive(EreportData)] +#[derive(ereport::EreportData)] pub enum EreportClass { #[ereport(rename = "hw.pwr.pmbus.alert")] PmbusAlert, } +#[derive(ereport::EreportData)] +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, ereport::EreportData)] +pub(crate) struct PmbusStatus { + word: Option, + input: Option, + iout: Option, + vout: Option, + temp: Option, + cml: Option, + mfr: Option, +} + impl ServerImpl { fn init( sys: &sys_api::Sys, diff --git a/drv/gimlet-seq-server/src/vcore.rs b/drv/gimlet-seq-server/src/vcore.rs index 056c8081d..835925955 100644 --- a/drv/gimlet-seq-server/src/vcore.rs +++ b/drv/gimlet-seq-server/src/vcore.rs @@ -31,6 +31,7 @@ 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; @@ -139,7 +140,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 +166,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 +265,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 +274,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 +333,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/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs index 183d59f3f..974dbd2fd 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/ereport-derive/src/lib.rs @@ -123,7 +123,7 @@ fn gen_enum_impl( } }); variant_lens.push(quote! { - #[allow(non_camel_case_names)] + #[allow(non_snake_case)] let #variant_name = { let mut len = 2; // map begin and end bytes #(#field_len_exprs;)* @@ -140,7 +140,7 @@ fn gen_enum_impl( )) = flattened { flattened_lens.push(quote! { - #[allow(non_camel_case_names)] + #[allow(non_snake_case)] let #variant_name = { let mut len = 0; // no map begin and end bytes, as we are flattening #(#field_len_exprs;)* diff --git a/task/packrat-api/src/lib.rs b/task/packrat-api/src/lib.rs index 596b571cc..e71c33846 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -87,6 +87,20 @@ pub enum EreportSerializeError { ), } +/// Errors returned by [`Packrat::encode_ereport`]. +#[derive(counters::Count)] +#[cfg(feature = "ereport")] +pub enum EreportEncodeError { + /// The IPC to deliver the serialized ereport failed. + Packrat { + len: usize, + #[count(children)] + err: EreportWriteError, + }, + /// Encoding the ereport failed. + Encoder(ereport::encode::Error), +} + /// Wrapper type defining common ereport fields. #[cfg(feature = "ereport")] #[derive(Clone, EreportData)] @@ -131,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 = "ereport")] + pub fn encode_ereport( + &self, + ereport: &E, + buf: &mut [u8], + ) -> Result { + let cursor = ereport::encode::write::Cursor::new(buf); + let mut encoder = ereport::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")); From 13de2cf7564527fcbb37e404be1874a0679eb668 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Fri, 26 Sep 2025 11:58:17 -0700 Subject: [PATCH 12/30] rm expanded --- lib/ereport/examples/test-expanded.rs | 121 -------------------------- 1 file changed, 121 deletions(-) delete mode 100644 lib/ereport/examples/test-expanded.rs diff --git a/lib/ereport/examples/test-expanded.rs b/lib/ereport/examples/test-expanded.rs deleted file mode 100644 index 42034d13d..000000000 --- a/lib/ereport/examples/test-expanded.rs +++ /dev/null @@ -1,121 +0,0 @@ -#![feature(prelude_import)] -#[prelude_import] -use std::prelude::rust_2024::*; -#[macro_use] -extern crate std; -use ereport::EreportData; -pub enum TestEnum { - Variant1, - #[ereport(rename = "hw.foo.cool-ereport-class")] - Variant2, -} -#[automatically_derived] -impl ::core::fmt::Debug for TestEnum { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::write_str( - f, - match self { - TestEnum::Variant1 => "Variant1", - TestEnum::Variant2 => "Variant2", - }, - ) - } -} -#[automatically_derived] -impl ::ereport::EreportData for TestEnum { - const MAX_CBOR_LEN: usize = { - let mut max = 0; - if ::ereport::str_cbor_len("Variant1") > max { - max = ::ereport::str_cbor_len("Variant1"); - } - if ::ereport::str_cbor_len("hw.foo.cool-ereport-class") > max { - max = ::ereport::str_cbor_len("hw.foo.cool-ereport-class"); - } - max - }; -} -impl ::ereport::encode::Encode for TestEnum { - fn encode( - &self, - e: &mut ::ereport::encode::Encoder, - _: &mut C, - ) -> Result<(), ::ereport::encode::Error> { - match self { - TestEnum::Variant1 => e.str("Variant1")?, - TestEnum::Variant2 => e.str("hw.foo.cool-ereport-class")?, - } - Ok(()) - } -} -struct TestStruct { - #[ereport(rename = "a")] - field1: u32, - field2: TestEnum, -} -#[automatically_derived] -impl ::ereport::EreportData for TestStruct -where - u32: ::ereport::EreportData, - TestEnum: ::ereport::EreportData, -{ - const MAX_CBOR_LEN: usize = - 2 + >::MAX_FIELDS_LEN; -} -#[automatically_derived] -impl ::ereport::encode::Encode for TestStruct -where - u32: ::ereport::EreportData + ::ereport::Encode, - TestEnum: ::ereport::EreportData + ::ereport::Encode, -{ - fn encode( - &self, - e: &mut ::ereport::encode::Encoder, - c: &mut C, - ) -> Result<(), ::ereport::encode::Error> { - e.begin_map()?; - >::encode_fields(self, e, c)?; - e.end()?; - Ok(()) - } -} -#[automatically_derived] -impl ::ereport::EncodeFields for TestStruct -where - u32: ::ereport::EreportData + ::ereport::Encode, - TestEnum: ::ereport::EreportData + ::ereport::Encode, -{ - const MAX_FIELDS_LEN: usize = { - let mut len = 0; - len += ::ereport::str_cbor_len("a"); - len += ::MAX_CBOR_LEN; - len += ::ereport::str_cbor_len("field2"); - len += ::MAX_CBOR_LEN; - len - }; - fn encode_fields( - &self, - e: &mut ::ereport::encode::Encoder, - c: &mut C, - ) -> Result<(), ::ereport::encode::Error> { - e.str("a")?; - ::ereport::Encode::::encode(&self.field1, e, c)?; - e.str("field2")?; - ::ereport::Encode::::encode(&self.field2, e, c)?; - Ok(()) - } -} -fn main() { - // { - // ::std::io::_print(format_args!( - // "TestEnum::MAX_CBOR_LEN = {0}\n", - // TestEnum::MAX_CBOR_LEN - // )); - // }; - // { - // ::std::io::_print(format_args!( - // "TestStruct::MAX_CBOR_LEN = {0}\n", - // TestStruct::MAX_CBOR_LEN - // )); - // }; -} From 576432c4ef96256d480c0d58d0c392e736aa5494 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Mon, 29 Sep 2025 11:00:55 -0700 Subject: [PATCH 13/30] fix clippy --- lib/fixedstr/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fixedstr/src/lib.rs b/lib/fixedstr/src/lib.rs index 0a267f5a0..6aeb389e2 100644 --- a/lib/fixedstr/src/lib.rs +++ b/lib/fixedstr/src/lib.rs @@ -25,6 +25,7 @@ //! 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; From 87d2d41d2cbe09085387c00ca05759511c2720ba Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Mon, 29 Sep 2025 11:02:37 -0700 Subject: [PATCH 14/30] tidy some comments --- lib/ereport-derive/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs index 974dbd2fd..0da48867c 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/ereport-derive/src/lib.rs @@ -59,7 +59,8 @@ fn gen_enum_impl( let mut variant_lens = Vec::new(); let mut flattened = Some((Vec::new(), Vec::new())); let mut all_where_bounds = Vec::new(); - // TODO(eliza): support top-level attribute for using the enum's repr instead of its name + // TODO(eliza): support top-level attribute for using the enum's repr + // instead of its name for variant in data.variants { let mut name = None; for attr in &variant.attrs { @@ -142,7 +143,9 @@ fn gen_enum_impl( flattened_lens.push(quote! { #[allow(non_snake_case)] let #variant_name = { - let mut len = 0; // no map begin and end bytes, as we are flattening + // 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 }; @@ -318,7 +321,9 @@ struct FieldGenerator<'fields> { // XXX(eliza): This really ought to be an `Option`, since there's always // either one token stream, or none. But, `quote!`'s repetition handles // `Vec`s nicer than `Option`s, since we would have to separately create an - // `Iterator` over the option for every time the expression is interpolated. + // `Iterator` over the option for every time the expression is + // interpolated. + // // Sigh. self_expr: Vec, field_idents: Vec<&'fields syn::Ident>, @@ -398,7 +403,8 @@ impl<'fields> FieldGenerator<'fields> { }); self.field_idents.push(field_ident); - // TODO(eliza): if we allow more complex ways of encoding fields as different CBOR types, this will have to handle that... + // 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; let self_expr = &self.self_expr; if flattened { From 8d83ea1e9e4f3495041ffca4126a10d0b3c3c0fd Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Mon, 29 Sep 2025 12:10:58 -0700 Subject: [PATCH 15/30] support tuple-like variants --- lib/ereport-derive/src/lib.rs | 381 +++++++++++++++++++++++++--------- lib/ereport/examples/test.rs | 34 ++- lib/ereport/src/lib.rs | 2 +- 3 files changed, 312 insertions(+), 105 deletions(-) diff --git a/lib/ereport-derive/src/lib.rs b/lib/ereport-derive/src/lib.rs index 0da48867c..df3054ad8 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/ereport-derive/src/lib.rs @@ -4,7 +4,7 @@ extern crate proc_macro; use proc_macro::TokenStream; -use quote::{ToTokens, quote}; +use quote::{ToTokens, format_ident, quote}; use syn::{ Attribute, DataEnum, DataStruct, DeriveInput, Generics, Ident, LitStr, Visibility, parse_macro_input, @@ -95,26 +95,21 @@ fn gen_enum_impl( }); } syn::Fields::Named(ref fields) => { - let mut field_gen = FieldGenerator::for_variant(); + let mut field_gen = + FieldGenerator::for_variant(FieldType::Named); for field in &fields.named { field_gen.add_field(field)?; } let FieldGenerator { - field_idents, + field_patterns, field_len_exprs, field_encode_exprs, where_bounds, - any_skipped, .. } = field_gen; all_where_bounds.extend(where_bounds); - let ignore_pattern = if any_skipped { - vec![quote!(..)] - } else { - vec![] - }; let match_pattern = quote! { - #ident::#variant_name { #(#field_idents,)* #(#ignore_pattern)*} + #ident::#variant_name { #(#field_patterns,)* } }; variant_patterns.push(quote! { #match_pattern => { @@ -160,11 +155,73 @@ fn gen_enum_impl( }); } } - _ => { - return Err(syn::Error::new_spanned( - variant, - "`#[derive(EreportData)]` only supports unit and named fields variants for now", - )); + syn::Fields::Unnamed(fields) => { + // If we've encountered a tuple variant, we can no longer + // flatten named fields. + flattened = None; + + let mut field_gen = + FieldGenerator::for_variant(FieldType::Unnamed); + for field in &fields.unnamed { + field_gen.add_field(field)?; + } + 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 => { + e.begin_array()?; + #(#field_encode_exprs)* + e.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; + } + }); + } } } } @@ -248,7 +305,17 @@ fn gen_struct_impl( generics: Generics, data: DataStruct, ) -> Result { - let mut field_gen = FieldGenerator::for_struct(); + 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(EreportData)]` is not supported on unit structs", + )); + } + }; + let mut field_gen = FieldGenerator::for_struct(field_type); // let mut data_where_bounds = Vec::new(); for field in &data.fields { field_gen.add_field(field)?; @@ -260,106 +327,173 @@ fn gen_struct_impl( where_bounds, field_encode_exprs, field_len_exprs, + field_patterns, .. } = field_gen; - Ok(quote! { - #[automatically_derived] - impl #impl_generics ::ereport::EreportData for #ident #tygenerics - #prev_where_clause - where #(#where_bounds,)* - { - const MAX_CBOR_LEN: usize = - 2 // map begin and end bytes - + >::MAX_FIELDS_LEN; - } - #[automatically_derived] - impl #impl_generics ::ereport::encode::Encode<()> - for #ident #tygenerics - #prev_where_clause - where #(#where_bounds,)* - { - fn encode( - &self, - e: &mut ::ereport::encode::Encoder, - c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { - e.begin_map()?; - >::encode_fields(self, e, c)?; - e.end()?; - Ok(()) + match (field_type, &field_encode_exprs[..], &field_len_exprs[..]) { + // Structs with named fields are flattenable. + (FieldType::Named, encode_exprs, len_exprs) => Ok(quote! { + #[automatically_derived] + impl #impl_generics ::ereport::EreportData for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_CBOR_LEN: usize = + 2 // map begin and end bytes + + >::MAX_FIELDS_LEN; } - } - #[automatically_derived] - impl #impl_generics ::ereport::EncodeFields<()> - for #ident #tygenerics - #prev_where_clause - where #(#where_bounds,)* - { - const MAX_FIELDS_LEN: usize = { - let mut len = 0; - #(#field_len_exprs;)* - len - }; + #[automatically_derived] + impl #impl_generics ::ereport::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + e.begin_map()?; + >::encode_fields(self, e, c)?; + e.end()?; + Ok(()) + } + } - fn encode_fields( - &self, - e: &mut ::ereport::encode::Encoder, - c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { - #(#field_encode_exprs;)* - Ok(()) + #[automatically_derived] + impl #impl_generics ::ereport::EncodeFields<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + const MAX_FIELDS_LEN: usize = { + let mut len = 0; + #(#len_exprs;)* + len + }; + + fn encode_fields( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + let Self { #(#field_patterns,)* } = self; + #(#encode_exprs)* + 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 ::ereport::EreportData 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 ::ereport::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::encode::Error> { + let Self( #(#field_patterns,)* ) = self; + #encode_expr + Ok(()) + } + } + }), + (FieldType::Unnamed, encode_exprs, len_exprs) => Ok(quote! { + #[automatically_derived] + impl #impl_generics ::ereport::EreportData 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 ::ereport::encode::Encode<()> + for #ident #tygenerics + #prev_where_clause + where #(#where_bounds,)* + { + fn encode( + &self, + e: &mut ::ereport::encode::Encoder, + c: &mut (), + ) -> Result<(), ::ereport::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... + e.begin_array()?; + #(#encode_exprs)* + e.end()?; + Ok(()) + } + } + }), + } } -#[derive(Default)] -struct FieldGenerator<'fields> { - // XXX(eliza): This really ought to be an `Option`, since there's always - // either one token stream, or none. But, `quote!`'s repetition handles - // `Vec`s nicer than `Option`s, since we would have to separately create an - // `Iterator` over the option for every time the expression is - // interpolated. - // - // Sigh. - self_expr: Vec, - field_idents: Vec<&'fields syn::Ident>, +struct FieldGenerator { + field_patterns: Vec, field_len_exprs: Vec, field_encode_exprs: Vec, where_bounds: Vec, any_skipped: bool, + field_type: FieldType, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum FieldType { + Named, + Unnamed, } -impl<'fields> FieldGenerator<'fields> { - fn for_struct() -> Self { +impl FieldGenerator { + fn for_struct(field_type: FieldType) -> Self { Self { - self_expr: vec![quote! { &self. }], - field_idents: Vec::new(), + field_patterns: Vec::new(), field_len_exprs: Vec::new(), field_encode_exprs: Vec::new(), where_bounds: Vec::new(), any_skipped: false, + field_type, } } - fn for_variant() -> Self { + fn for_variant(field_type: FieldType) -> Self { Self { - self_expr: vec![], - field_idents: Vec::new(), + field_patterns: Vec::new(), field_len_exprs: Vec::new(), field_encode_exprs: Vec::new(), where_bounds: Vec::new(), any_skipped: false, + field_type, } } - fn add_field( - &mut self, - field: &'fields syn::Field, - ) -> Result<(), syn::Error> { + fn add_field(&mut self, field: &syn::Field) -> Result<(), syn::Error> { let mut field_name = None; let mut skipped = false; let mut flattened = false; @@ -368,6 +502,12 @@ impl<'fields> FieldGenerator<'fields> { if attr.path().is_ident("ereport") { attr.meta.require_list()?.parse_nested_meta(|meta| { if meta.path.is_ident("rename") { + if field.ident.is_none() { + return Err(meta.error( + "`#[ereport(rename = \"...\")]` is only + supported on named fields", + )); + } field_name = Some(meta.value()?.parse::()?); Ok(()) } else if meta.path.is_ident("skip") { @@ -377,6 +517,12 @@ impl<'fields> FieldGenerator<'fields> { skipped_if_nil = true; Ok(()) } else if meta.path.is_ident("flatten") { + if self.field_type == FieldType::Unnamed { + return Err(meta.error( + "`#[ereport(flatten)]` is only supported on \ + structs and enum variants with named fields", + )); + } flattened = true; Ok(()) } else { @@ -389,24 +535,53 @@ impl<'fields> FieldGenerator<'fields> { } if skipped { self.any_skipped = true; - return Ok(()); } - let field_ident = field.ident.as_ref().ok_or_else(|| { - syn::Error::new_spanned( - field, - "#[derive(EreportData)] doesn't support tuple structs yet", - ) - })?; - let field_name = field_name.unwrap_or_else(|| { - LitStr::new(&field_ident.to_string(), field_ident.span()) - }); - self.field_idents.push(field_ident); + 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(), + ) + }); + self.field_patterns.push(quote! { #field_ident }); + let encode_name = quote! { + e.str(#field_name)?; + }; + let name_len = quote! { + len += ::ereport::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; - let self_expr = &self.self_expr; if flattened { self.where_bounds.push(quote! { #field_type: ::ereport::EncodeFields<()> @@ -415,24 +590,24 @@ impl<'fields> FieldGenerator<'fields> { len += <#field_type as ::ereport::EncodeFields<()>>::MAX_FIELDS_LEN; }); self.field_encode_exprs.push(quote! { - ::ereport::EncodeFields::<()>::encode_fields(#(#self_expr)*#field_ident, e, c)?; + ::ereport::EncodeFields::<()>::encode_fields(#field_ident, e, c)?; }); } else { self.field_len_exprs.push(quote! { - len += ::ereport::str_cbor_len(#field_name); + #name_len len += <#field_type as ::ereport::EreportData>::MAX_CBOR_LEN; }); self.field_encode_exprs.push(if skipped_if_nil { quote! { - if !::ereport::Encode::<()>::is_nil(#(#self_expr)*#field_ident) { - e.str(#field_name)?; - ::ereport::Encode::<()>::encode(#(#self_expr)*#field_ident, e, c)?; + if !::ereport::Encode::<()>::is_nil(#field_ident) { + #encode_name + ::ereport::Encode::<()>::encode(#field_ident, e, c)?; } } } else { quote! { - e.str(#field_name)?; - ::ereport::Encode::<()>::encode(#(#self_expr)*#field_ident, e, c)?; + #encode_name + ::ereport::Encode::<()>::encode(#field_ident, e, c)?; } }); self.where_bounds.push(quote! { diff --git a/lib/ereport/examples/test.rs b/lib/ereport/examples/test.rs index 909be0d31..ec4d395bc 100644 --- a/lib/ereport/examples/test.rs +++ b/lib/ereport/examples/test.rs @@ -39,12 +39,29 @@ enum TestEnum2 { }, } +#[derive(Debug, EreportData)] +enum TestEnum3 { + Named { + field1: u32, + tuple_struct: TestTupleStruct1, + }, + Unnamed(u32, TestTupleStruct2), + UnnamedSingle(f64), +} + +#[derive(Debug, EreportData)] +struct TestTupleStruct1(u32, u64); + +#[derive(Debug, EreportData)] +struct TestTupleStruct2(u64); + fn main() { const MAX_LEN: usize = ereport::max_cbor_len_for! { TestEnum, TestStruct2, TestEnum2, - TestStruct2> + TestStruct2>, + TestEnum3, }; let mut buf = [0u8; MAX_LEN]; @@ -100,6 +117,21 @@ fn main() { }, &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); } fn test_one_type(input: T, buf: &mut [u8]) { diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index e3459c167..36b08f5ee 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -19,7 +19,7 @@ pub trait EreportData: Encode<()> { #[macro_export] macro_rules! max_cbor_len_for { - ($($T:ty),+) => { + ($($T:ty),+$(,)?) => { { let mut len = 0; $( From a061398cdc55a7431b2f0c44f22808920b3489c5 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Mon, 29 Sep 2025 12:37:15 -0700 Subject: [PATCH 16/30] clippy --- drv/gimlet-seq-server/src/vcore.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/drv/gimlet-seq-server/src/vcore.rs b/drv/gimlet-seq-server/src/vcore.rs index 835925955..011ef40c4 100644 --- a/drv/gimlet-seq-server/src/vcore.rs +++ b/drv/gimlet-seq-server/src/vcore.rs @@ -33,7 +33,6 @@ 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}; From 504edf536142ff1af91f786958fb2fcb6e940183 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Mon, 29 Sep 2025 16:34:35 -0700 Subject: [PATCH 17/30] reticulating docs --- lib/ereport/src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 36b08f5ee..a02ade6cc 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -3,6 +3,31 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Hubris ereport traits. +//! +//! ## Wait, Why Not `#[derive(serde::Serialize)]`? +//! +//! TODO ELIZA EXPLAIN +//! +//! ## 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`]: https://docs.rs/minicbor-derive #![no_std] use encode::{Encoder, Write}; From 0f93c81709a45132d0e44a7d50c460b2d513443b Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 09:34:47 -0700 Subject: [PATCH 18/30] add array impl --- lib/ereport/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index a02ade6cc..f036222e0 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -125,6 +125,10 @@ impl EreportData for Option { }; } +impl EreportData for [T; LEN] { + const MAX_CBOR_LEN: usize = usize_cbor_len(LEN) + (LEN * T::MAX_CBOR_LEN); +} + impl EreportData for &T { const MAX_CBOR_LEN: usize = T::MAX_CBOR_LEN; } From 9f0a2d8d41d84387550ed88a7a1be65578511910 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 09:37:16 -0700 Subject: [PATCH 19/30] reticulating docs --- lib/ereport/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index f036222e0..3f3678ba2 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -34,6 +34,11 @@ use encode::{Encoder, Write}; pub use ereport_derive::EreportData; 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 EreportData: Encode<()> { /// The maximum length of the CBOR-encoded representation of this value. /// From 52470d6a98417e1a37bde321e0f89d51856ffe4c Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 10:22:45 -0700 Subject: [PATCH 20/30] add proptests that max sizes work --- .cargo/config.toml | 2 +- Cargo.lock | 256 +++++++++++++++++++++++++++++++---- Cargo.toml | 2 + lib/ereport/Cargo.toml | 2 + lib/ereport/src/lib.rs | 34 ++++- lib/ereport/tests/max_len.rs | 98 ++++++++++++++ 6 files changed, 366 insertions(+), 28 deletions(-) create mode 100644 lib/ereport/tests/max_len.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index a5751bbd8..ba92d46cb 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 f395f1dea..52a97f5a1 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" @@ -785,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", ] @@ -1542,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.26", "zerocopy-derive 0.8.26", @@ -2022,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.26", "zerocopy-derive 0.8.26", @@ -2826,7 +2841,7 @@ dependencies = [ "generic-array", "group", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -2930,6 +2945,8 @@ dependencies = [ "ereport-derive", "fixedstr", "minicbor 2.1.1", + "proptest", + "proptest-derive", ] [[package]] @@ -2954,9 +2971,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", @@ -2972,13 +2989,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", ] @@ -3098,7 +3121,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]] @@ -3172,7 +3207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3631,9 +3666,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" @@ -3653,6 +3688,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" @@ -4068,7 +4109,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.4", "serde", "smallvec 1.10.0", "zeroize", @@ -4256,7 +4297,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "rand_core", + "rand_core 0.6.4", "sha2", ] @@ -4328,8 +4369,8 @@ version = "0.1.0" dependencies = [ "anyhow", "phash", - "rand", - "rand_chacha", + "rand 0.8.4", + "rand_chacha 0.3.1", ] [[package]] @@ -4486,6 +4527,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.1", + "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" @@ -4499,6 +4571,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" @@ -4508,6 +4586,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" @@ -4526,11 +4610,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" @@ -4538,7 +4632,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]] @@ -4547,7 +4651,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]] @@ -4556,7 +4669,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]] @@ -4582,7 +4704,7 @@ checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.6.26", ] [[package]] @@ -4591,6 +4713,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" @@ -4649,7 +4777,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "signature", @@ -4702,19 +4830,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.1", + "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" @@ -5013,7 +5166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -6243,6 +6396,18 @@ dependencies = [ "zerocopy-derive 0.8.26", ] +[[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" @@ -6653,6 +6818,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" @@ -6788,6 +6959,15 @@ dependencies = [ "zerocopy-derive 0.8.26", ] +[[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" @@ -6805,6 +6985,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" @@ -7115,6 +7313,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" diff --git a/Cargo.toml b/Cargo.toml index 9a739c230..7d1829049 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/lib/ereport/Cargo.toml b/lib/ereport/Cargo.toml index 041f49491..cd89e40f9 100644 --- a/lib/ereport/Cargo.toml +++ b/lib/ereport/Cargo.toml @@ -14,3 +14,5 @@ workspace = true [dev-dependencies] minicbor = { workspace = true, features = ["alloc", "half"]} fixedstr = { path = "../fixedstr", features = ["minicbor"] } +proptest = { workspace = true } +proptest-derive = { workspace = true } diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 3f3678ba2..37f4bcbe6 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -29,7 +29,6 @@ //! //! [`minicbor-derive`]: https://docs.rs/minicbor-derive #![no_std] - use encode::{Encoder, Write}; pub use ereport_derive::EreportData; pub use minicbor::encode::{self, Encode}; @@ -47,6 +46,39 @@ pub trait EreportData: Encode<()> { const MAX_CBOR_LEN: usize; } +/// For a list of types implementing [`EreportData`], 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 [`EreportData`]. +/// +/// For example: +/// +/// ```rust +/// +/// #[derive(ereport::EreportData)] +/// pub struct MyGreatEreport { +/// foo: u32, +/// bar: Option, +/// } +/// +/// #[derive(ereport::EreportData)] +/// pub enum AnotherEreport { +/// A { hello: bool, world: f64 }, +/// B(usize), +/// } +/// +/// const EREPORT_BUF_LEN: usize = ereport::max_cbor_len_for![ +/// MyGreatEreport, +/// AnotherEreport, +/// ]; +/// +/// fn main() { +/// let mut ereport_buf = [0; EREPORT_BUF_LEN]; +/// // ... +/// # drop(ereport_buf); +/// } +/// ``` #[macro_export] macro_rules! max_cbor_len_for { ($($T:ty),+$(,)?) => { diff --git a/lib/ereport/tests/max_len.rs b/lib/ereport/tests/max_len.rs new file mode 100644 index 000000000..bab022e2c --- /dev/null +++ b/lib/ereport/tests/max_len.rs @@ -0,0 +1,98 @@ +use ereport::EreportData; +use proptest::test_runner::TestCaseError; +use proptest_derive::Arbitrary; + +#[derive(Debug, EreportData, Arbitrary)] +pub enum TestEnum { + Variant1, + #[ereport(rename = "hw.foo.cool-ereport-class")] + Variant2, + #[ereport(rename = "hw.bar.baz.fault.computer_on_fire")] + Variant3, +} + +#[derive(Debug, EreportData, Arbitrary)] +struct TestStruct { + #[ereport(rename = "a")] + field1: u32, + field2: TestEnum, +} + +#[derive(Debug, EreportData, Arbitrary)] +struct TestStruct2 { + #[ereport(skip_if_nil)] + field6: Option, + #[ereport(flatten)] + inner: D, +} + +#[derive(Debug, EreportData, Arbitrary)] +enum TestEnum2 { + Flattened { + #[ereport(flatten)] + flattened: D, + bar: u32, + }, + Nested { + nested: D, + quux: f32, + }, +} + +#[derive(Debug, EreportData, Arbitrary)] +enum TestEnum3 { + Named { + field1: u32, + tuple_struct: TestTupleStruct1, + }, + Unnamed(u32, TestTupleStruct2), + UnnamedSingle(f64), +} + +#[derive(Debug, EreportData, Arbitrary)] +struct TestTupleStruct1(u32, u64); + +#[derive(Debug, EreportData, Arbitrary)] +struct TestTupleStruct2(u64); + +#[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)?; + } +} From b6b5a8da16b6b7b7447796f13939d24510d97267 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 10:24:04 -0700 Subject: [PATCH 21/30] add license to tests --- lib/ereport/tests/max_len.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/ereport/tests/max_len.rs b/lib/ereport/tests/max_len.rs index bab022e2c..5fc6c4da1 100644 --- a/lib/ereport/tests/max_len.rs +++ b/lib/ereport/tests/max_len.rs @@ -1,3 +1,7 @@ +// 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 ereport::EreportData; use proptest::test_runner::TestCaseError; use proptest_derive::Arbitrary; From 1139182658c36da6e31b263abff6d77e3ea173dd Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 10:26:48 -0700 Subject: [PATCH 22/30] more tests --- lib/ereport/tests/max_len.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/ereport/tests/max_len.rs b/lib/ereport/tests/max_len.rs index 5fc6c4da1..cee65bc44 100644 --- a/lib/ereport/tests/max_len.rs +++ b/lib/ereport/tests/max_len.rs @@ -59,6 +59,13 @@ struct TestTupleStruct1(u32, u64); #[derive(Debug, EreportData, Arbitrary)] struct TestTupleStruct2(u64); +#[derive(Debug, EreportData, Arbitrary)] +struct TestStructWithArrays { + bytes: [u8; 10], + enums: [TestEnum3; 4], + blargh: usize, +} + #[track_caller] fn assert_max_len( input: &T, @@ -99,4 +106,14 @@ proptest::proptest! { 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)?; + } } From 7c36a731447b03acb28bf843b3edc1f14885e0c2 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 10:29:06 -0700 Subject: [PATCH 23/30] docsing --- lib/ereport/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 37f4bcbe6..54174c362 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -27,6 +27,10 @@ //! 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}; From 81606eeeccaaf8488062a881b42454f5e4ef8154 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 10:59:12 -0700 Subject: [PATCH 24/30] more docs for encode fields --- lib/ereport/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/ereport/src/lib.rs b/lib/ereport/src/lib.rs index 54174c362..ddb7705b7 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/ereport/src/lib.rs @@ -98,6 +98,17 @@ macro_rules! max_cbor_len_for { }; } +/// 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 `EreportData`, and the field in the parent type is annotated with +/// `#[ereport(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. pub trait EncodeFields { const MAX_FIELDS_LEN: usize; From 5108f566122257825586a33f56947bd2beb81007 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 11:08:45 -0700 Subject: [PATCH 25/30] rename crates to be less ereport-specific --- Cargo.lock | 44 +++++----- drv/gimlet-seq-server/Cargo.toml | 2 +- drv/gimlet-seq-server/src/main.rs | 8 +- .../Cargo.toml | 2 +- .../src/lib.rs | 82 +++++++++---------- lib/{ereport => microcbor}/Cargo.toml | 4 +- lib/{ereport => microcbor}/examples/test.rs | 0 lib/{ereport => microcbor}/src/lib.rs | 10 +-- lib/{ereport => microcbor}/tests/max_len.rs | 0 task/packrat-api/Cargo.toml | 6 +- task/packrat-api/src/lib.rs | 16 ++-- 11 files changed, 87 insertions(+), 87 deletions(-) rename lib/{ereport-derive => microcbor-derive}/Cargo.toml (89%) rename lib/{ereport-derive => microcbor-derive}/src/lib.rs (87%) rename lib/{ereport => microcbor}/Cargo.toml (85%) rename lib/{ereport => microcbor}/examples/test.rs (100%) rename lib/{ereport => microcbor}/src/lib.rs (96%) rename lib/{ereport => microcbor}/tests/max_len.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 52a97f5a1..d1f88bbac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1282,11 +1282,11 @@ dependencies = [ "drv-spi-api", "drv-stm32h7-spi", "drv-stm32xx-sys-api", - "ereport", "fixedstr", "gnarle", "idol", "idol-runtime", + "microcbor", "num-derive 0.4.2", "num-traits", "pmbus", @@ -2938,26 +2938,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "ereport" -version = "0.1.0" -dependencies = [ - "ereport-derive", - "fixedstr", - "minicbor 2.1.1", - "proptest", - "proptest-derive", -] - -[[package]] -name = "ereport-derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "errno" version = "0.2.8" @@ -3940,6 +3920,26 @@ 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 = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "minibar" version = "0.1.0" @@ -6019,11 +6019,11 @@ dependencies = [ "anyhow", "counters", "derive-idol-err", - "ereport", "gateway-ereport-messages", "host-sp-messages", "idol", "idol-runtime", + "microcbor", "minicbor 2.1.1", "minicbor-serde 0.6.0", "num-traits", diff --git a/drv/gimlet-seq-server/Cargo.toml b/drv/gimlet-seq-server/Cargo.toml index 38e065b54..7e77d5757 100644 --- a/drv/gimlet-seq-server/Cargo.toml +++ b/drv/gimlet-seq-server/Cargo.toml @@ -21,7 +21,7 @@ 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" } -ereport = { path = "../../lib/ereport", features = ["fixedstr"] } +microcbor = { path = "../../lib/microcbor", features = ["fixedstr"] } fixedstr = { path = "../../lib/fixedstr" } cfg-if = { workspace = true } diff --git a/drv/gimlet-seq-server/src/main.rs b/drv/gimlet-seq-server/src/main.rs index dd08b9012..db6c4d1d8 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -210,15 +210,15 @@ const TIMER_INTERVAL: u32 = 10; const EREPORT_BUF_LEN: usize = as ereport::EreportData>::MAX_CBOR_LEN; +> as microcbor::EreportData>::MAX_CBOR_LEN; -#[derive(ereport::EreportData)] +#[derive(microcbor::EreportData)] pub enum EreportClass { #[ereport(rename = "hw.pwr.pmbus.alert")] PmbusAlert, } -#[derive(ereport::EreportData)] +#[derive(microcbor::EreportData)] pub(crate) enum EreportKind { PmbusAlert { refdes: fixedstr::FixedStr<{ crate::i2c_config::MAX_COMPONENT_ID_LEN }>, @@ -229,7 +229,7 @@ pub(crate) enum EreportKind { }, } -#[derive(Copy, Clone, Default, ereport::EreportData)] +#[derive(Copy, Clone, Default, microcbor::EreportData)] pub(crate) struct PmbusStatus { word: Option, input: Option, diff --git a/lib/ereport-derive/Cargo.toml b/lib/microcbor-derive/Cargo.toml similarity index 89% rename from lib/ereport-derive/Cargo.toml rename to lib/microcbor-derive/Cargo.toml index 4b780f8ea..f17287719 100644 --- a/lib/ereport-derive/Cargo.toml +++ b/lib/microcbor-derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ereport-derive" +name = "microcbor-derive" version = "0.1.0" edition = "2024" diff --git a/lib/ereport-derive/src/lib.rs b/lib/microcbor-derive/src/lib.rs similarity index 87% rename from lib/ereport-derive/src/lib.rs rename to lib/microcbor-derive/src/lib.rs index df3054ad8..925f7cb47 100644 --- a/lib/ereport-derive/src/lib.rs +++ b/lib/microcbor-derive/src/lib.rs @@ -89,8 +89,8 @@ fn gen_enum_impl( #ident::#variant_name => { e.str(#name)?; } }); variant_lens.push(quote! { - if ::ereport::str_cbor_len(#name) > max { - max = ::ereport::str_cbor_len(#name); + if ::microcbor::str_cbor_len(#name) > max { + max = ::microcbor::str_cbor_len(#name); } }); } @@ -235,7 +235,7 @@ fn gen_enum_impl( if let Some((flattened_lens, flattened_encode_patterns)) = flattened { quote! { #[automatically_derived] - impl #impl_generics ::ereport::EncodeFields<()> + impl #impl_generics ::microcbor::EncodeFields<()> for #ident #tygenerics #prev_where_clause where #(#all_where_bounds,)* @@ -246,11 +246,11 @@ fn gen_enum_impl( max }; - fn encode_fields( + fn encode_fields( &self, - e: &mut ::ereport::encode::Encoder, + e: &mut ::microcbor::encode::Encoder, c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { + ) -> Result<(), ::microcbor::encode::Error> { match self { #(#flattened_encode_patterns,)* } @@ -266,7 +266,7 @@ fn gen_enum_impl( #maybe_fields_impl #[automatically_derived] - impl #impl_generics ::ereport::EreportData + impl #impl_generics ::microcbor::EreportData for #ident #tygenerics #prev_where_clause where #(#all_where_bounds,)* @@ -279,16 +279,16 @@ fn gen_enum_impl( } #[automatically_derived] - impl #impl_generics ::ereport::encode::Encode<()> + impl #impl_generics ::microcbor::encode::Encode<()> for #ident #tygenerics #prev_where_clause where #(#all_where_bounds,)* { - fn encode( + fn encode( &self, - e: &mut ::ereport::encode::Encoder, + e: &mut ::microcbor::encode::Encoder, c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { + ) -> Result<(), ::microcbor::encode::Error> { match self { #(#variant_patterns,)* } @@ -335,35 +335,35 @@ fn gen_struct_impl( // Structs with named fields are flattenable. (FieldType::Named, encode_exprs, len_exprs) => Ok(quote! { #[automatically_derived] - impl #impl_generics ::ereport::EreportData for #ident #tygenerics + impl #impl_generics ::microcbor::EreportData for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { const MAX_CBOR_LEN: usize = 2 // map begin and end bytes - + >::MAX_FIELDS_LEN; + + >::MAX_FIELDS_LEN; } #[automatically_derived] - impl #impl_generics ::ereport::encode::Encode<()> + impl #impl_generics ::microcbor::encode::Encode<()> for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { - fn encode( + fn encode( &self, - e: &mut ::ereport::encode::Encoder, + e: &mut ::microcbor::encode::Encoder, c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { + ) -> Result<(), ::microcbor::encode::Error> { e.begin_map()?; - >::encode_fields(self, e, c)?; + >::encode_fields(self, e, c)?; e.end()?; Ok(()) } } #[automatically_derived] - impl #impl_generics ::ereport::EncodeFields<()> + impl #impl_generics ::microcbor::EncodeFields<()> for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* @@ -374,11 +374,11 @@ fn gen_struct_impl( len }; - fn encode_fields( + fn encode_fields( &self, - e: &mut ::ereport::encode::Encoder, + e: &mut ::microcbor::encode::Encoder, c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { + ) -> Result<(), ::microcbor::encode::Error> { let Self { #(#field_patterns,)* } = self; #(#encode_exprs)* Ok(()) @@ -389,7 +389,7 @@ fn gen_struct_impl( // single value. (FieldType::Unnamed, [encode_expr], [len_expr]) => Ok(quote! { #[automatically_derived] - impl #impl_generics ::ereport::EreportData for #ident #tygenerics + impl #impl_generics ::microcbor::EreportData for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { @@ -401,16 +401,16 @@ fn gen_struct_impl( } #[automatically_derived] - impl #impl_generics ::ereport::encode::Encode<()> + impl #impl_generics ::microcbor::encode::Encode<()> for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { - fn encode( + fn encode( &self, - e: &mut ::ereport::encode::Encoder, + e: &mut ::microcbor::encode::Encoder, c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { + ) -> Result<(), ::microcbor::encode::Error> { let Self( #(#field_patterns,)* ) = self; #encode_expr Ok(()) @@ -419,7 +419,7 @@ fn gen_struct_impl( }), (FieldType::Unnamed, encode_exprs, len_exprs) => Ok(quote! { #[automatically_derived] - impl #impl_generics ::ereport::EreportData for #ident #tygenerics + impl #impl_generics ::microcbor::EreportData for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { @@ -431,16 +431,16 @@ fn gen_struct_impl( } #[automatically_derived] - impl #impl_generics ::ereport::encode::Encode<()> + impl #impl_generics ::microcbor::encode::Encode<()> for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { - fn encode( + fn encode( &self, - e: &mut ::ereport::encode::Encoder, + e: &mut ::microcbor::encode::Encoder, c: &mut (), - ) -> Result<(), ::ereport::encode::Error> { + ) -> 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 @@ -564,7 +564,7 @@ impl FieldGenerator { e.str(#field_name)?; }; let name_len = quote! { - len += ::ereport::str_cbor_len(#field_name); + len += ::microcbor::str_cbor_len(#field_name); }; (field_ident.clone(), encode_name, name_len) } @@ -584,34 +584,34 @@ impl FieldGenerator { let field_type = &field.ty; if flattened { self.where_bounds.push(quote! { - #field_type: ::ereport::EncodeFields<()> + #field_type: ::microcbor::EncodeFields<()> }); self.field_len_exprs.push(quote! { - len += <#field_type as ::ereport::EncodeFields<()>>::MAX_FIELDS_LEN; + len += <#field_type as ::microcbor::EncodeFields<()>>::MAX_FIELDS_LEN; }); self.field_encode_exprs.push(quote! { - ::ereport::EncodeFields::<()>::encode_fields(#field_ident, e, c)?; + ::microcbor::EncodeFields::<()>::encode_fields(#field_ident, e, c)?; }); } else { self.field_len_exprs.push(quote! { #name_len - len += <#field_type as ::ereport::EreportData>::MAX_CBOR_LEN; + len += <#field_type as ::microcbor::EreportData>::MAX_CBOR_LEN; }); self.field_encode_exprs.push(if skipped_if_nil { quote! { - if !::ereport::Encode::<()>::is_nil(#field_ident) { + if !::microcbor::Encode::<()>::is_nil(#field_ident) { #encode_name - ::ereport::Encode::<()>::encode(#field_ident, e, c)?; + ::microcbor::Encode::<()>::encode(#field_ident, e, c)?; } } } else { quote! { #encode_name - ::ereport::Encode::<()>::encode(#field_ident, e, c)?; + ::microcbor::Encode::<()>::encode(#field_ident, e, c)?; } }); self.where_bounds.push(quote! { - #field_type: ::ereport::EreportData + #field_type: ::microcbor::EreportData }); } diff --git a/lib/ereport/Cargo.toml b/lib/microcbor/Cargo.toml similarity index 85% rename from lib/ereport/Cargo.toml rename to lib/microcbor/Cargo.toml index cd89e40f9..5451f1149 100644 --- a/lib/ereport/Cargo.toml +++ b/lib/microcbor/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "ereport" +name = "microcbor" version = "0.1.0" edition = "2024" [dependencies] minicbor.workspace = true -ereport-derive = { path = "../ereport-derive" } +microcbor-derive = { path = "../microcbor-derive" } fixedstr = { path = "../fixedstr", optional = true, features = ["minicbor"] } [lints] diff --git a/lib/ereport/examples/test.rs b/lib/microcbor/examples/test.rs similarity index 100% rename from lib/ereport/examples/test.rs rename to lib/microcbor/examples/test.rs diff --git a/lib/ereport/src/lib.rs b/lib/microcbor/src/lib.rs similarity index 96% rename from lib/ereport/src/lib.rs rename to lib/microcbor/src/lib.rs index ddb7705b7..691bdff1d 100644 --- a/lib/ereport/src/lib.rs +++ b/lib/microcbor/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/. -//! Hubris ereport traits. +//! Fixed-length CBOR encoding.. //! //! ## Wait, Why Not `#[derive(serde::Serialize)]`? //! @@ -34,7 +34,7 @@ //! [`minicbor-derive`]: https://docs.rs/minicbor-derive #![no_std] use encode::{Encoder, Write}; -pub use ereport_derive::EreportData; +pub use microcbor_derive::EreportData; pub use minicbor::encode::{self, Encode}; /// A CBOR-encodable value with a statically-known maximum length. @@ -60,19 +60,19 @@ pub trait EreportData: Encode<()> { /// /// ```rust /// -/// #[derive(ereport::EreportData)] +/// #[derive(microcbor::EreportData)] /// pub struct MyGreatEreport { /// foo: u32, /// bar: Option, /// } /// -/// #[derive(ereport::EreportData)] +/// #[derive(microcbor::EreportData)] /// pub enum AnotherEreport { /// A { hello: bool, world: f64 }, /// B(usize), /// } /// -/// const EREPORT_BUF_LEN: usize = ereport::max_cbor_len_for![ +/// const EREPORT_BUF_LEN: usize = microcbor::max_cbor_len_for![ /// MyGreatEreport, /// AnotherEreport, /// ]; diff --git a/lib/ereport/tests/max_len.rs b/lib/microcbor/tests/max_len.rs similarity index 100% rename from lib/ereport/tests/max_len.rs rename to lib/microcbor/tests/max_len.rs diff --git a/task/packrat-api/Cargo.toml b/task/packrat-api/Cargo.toml index a97507fd7..c32ed2623 100644 --- a/task/packrat-api/Cargo.toml +++ b/task/packrat-api/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" [features] # Enable Serde support for the ereport API. serde = ["dep:serde", "dep:minicbor", "dep:minicbor-serde"] -ereport = ["dep:ereport", "dep:static_assertions", "dep:minicbor"] -default = ["ereport"] +microcbor = ["dep:microcbor", "dep:static_assertions", "dep:minicbor"] +default = ["microcbor"] [dependencies] counters = { path = "../../lib/counters" } @@ -15,7 +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" -ereport = { path = "../../lib/ereport", optional = true } +microcbor = { path = "../../lib/microcbor", optional = true } gateway-ereport-messages.workspace = true idol-runtime.workspace = true diff --git a/task/packrat-api/src/lib.rs b/task/packrat-api/src/lib.rs index e9d1db012..477ad630b 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -12,10 +12,10 @@ use zerocopy::{ FromBytes, Immutable, IntoBytes, KnownLayout, LittleEndian, U16, }; -#[cfg(feature = "ereport")] -use ereport::EreportData; pub use gateway_ereport_messages as ereport_messages; pub use host_sp_messages::HostStartupOptions; +#[cfg(feature = "microcbor")] +use microcbor::EreportData; pub use oxide_barcode::OxideIdentity; /// Represents a range of allocated MAC addresses, per RFD 320 @@ -89,7 +89,7 @@ pub enum EreportSerializeError { /// Errors returned by [`Packrat::encode_ereport`]. #[derive(counters::Count)] -#[cfg(feature = "ereport")] +#[cfg(feature = "microcbor")] pub enum EreportEncodeError { /// The IPC to deliver the serialized ereport failed. Packrat { @@ -98,11 +98,11 @@ pub enum EreportEncodeError { err: EreportWriteError, }, /// Encoding the ereport failed. - Encoder(ereport::encode::Error), + Encoder(microcbor::encode::Error), } /// Wrapper type defining common ereport fields. -#[cfg(feature = "ereport")] +#[cfg(feature = "microcbor")] #[derive(Clone, EreportData)] pub struct Ereport { #[ereport(rename = "k")] @@ -149,14 +149,14 @@ impl Packrat { // 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 = "ereport")] + #[cfg(feature = "microcbor")] pub fn encode_ereport( &self, ereport: &E, buf: &mut [u8], ) -> Result { - let cursor = ereport::encode::write::Cursor::new(buf); - let mut encoder = ereport::encode::Encoder::new(cursor); + 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)?; From 7b64e31e01ff54df6f5b9cffe06eb135aa71bfef Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 11:14:11 -0700 Subject: [PATCH 26/30] rename traits to be less ereport-specific --- drv/gimlet-seq-server/src/main.rs | 2 +- lib/microcbor/examples/test.rs | 2 +- lib/microcbor/src/lib.rs | 22 +++++++++++----------- task/packrat-api/src/lib.rs | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/drv/gimlet-seq-server/src/main.rs b/drv/gimlet-seq-server/src/main.rs index db6c4d1d8..199bd9b9d 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -210,7 +210,7 @@ const TIMER_INTERVAL: u32 = 10; const EREPORT_BUF_LEN: usize = as microcbor::EreportData>::MAX_CBOR_LEN; +> as microcbor::StaticCborLen>::MAX_CBOR_LEN; #[derive(microcbor::EreportData)] pub enum EreportClass { diff --git a/lib/microcbor/examples/test.rs b/lib/microcbor/examples/test.rs index ec4d395bc..a521c10da 100644 --- a/lib/microcbor/examples/test.rs +++ b/lib/microcbor/examples/test.rs @@ -56,7 +56,7 @@ struct TestTupleStruct1(u32, u64); struct TestTupleStruct2(u64); fn main() { - const MAX_LEN: usize = ereport::max_cbor_len_for! { + const MAX_LEN: usize = microcbor::max_cbor_len_for! { TestEnum, TestStruct2, TestEnum2, diff --git a/lib/microcbor/src/lib.rs b/lib/microcbor/src/lib.rs index 691bdff1d..577ab6349 100644 --- a/lib/microcbor/src/lib.rs +++ b/lib/microcbor/src/lib.rs @@ -42,7 +42,7 @@ pub use minicbor::encode::{self, Encode}; /// 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 EreportData: Encode<()> { +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 @@ -89,8 +89,8 @@ macro_rules! max_cbor_len_for { { let mut len = 0; $( - if <$T as $crate::EreportData>::MAX_CBOR_LEN > len { - len = <$T as $crate::EreportData>::MAX_CBOR_LEN; + if <$T as $crate::StaticCborLen>::MAX_CBOR_LEN > len { + len = <$T as $crate::StaticCborLen>::MAX_CBOR_LEN; } )+ len @@ -120,21 +120,21 @@ pub trait EncodeFields { } #[cfg(feature = "fixedstr")] -impl EreportData for fixedstr::FixedStr { +impl StaticCborLen for fixedstr::FixedStr { const MAX_CBOR_LEN: usize = LEN + usize::MAX_CBOR_LEN; } -macro_rules! impl_ereport_data { +macro_rules! impl_static_cbor_len { ($($T:ty = $len:expr),*$(,)?) => { $( - impl EreportData for $T { + impl StaticCborLen for $T { const MAX_CBOR_LEN: usize = $len; } )* }; } -impl_ereport_data! { +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, @@ -161,7 +161,7 @@ impl_ereport_data! { bool = 1, } -impl EreportData for usize { +impl StaticCborLen for usize { #[cfg(target_pointer_width = "32")] const MAX_CBOR_LEN: usize = u32::MAX_CBOR_LEN; @@ -169,7 +169,7 @@ impl EreportData for usize { const MAX_CBOR_LEN: usize = u64::MAX_CBOR_LEN; } -impl EreportData for Option { +impl StaticCborLen for Option { const MAX_CBOR_LEN: usize = if T::MAX_CBOR_LEN > 1 { T::MAX_CBOR_LEN + 1 } else { @@ -177,11 +177,11 @@ impl EreportData for Option { }; } -impl EreportData for [T; LEN] { +impl StaticCborLen for [T; LEN] { const MAX_CBOR_LEN: usize = usize_cbor_len(LEN) + (LEN * T::MAX_CBOR_LEN); } -impl EreportData for &T { +impl StaticCborLen for &T { const MAX_CBOR_LEN: usize = T::MAX_CBOR_LEN; } diff --git a/task/packrat-api/src/lib.rs b/task/packrat-api/src/lib.rs index 477ad630b..88a6cfb2c 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -15,7 +15,7 @@ use zerocopy::{ pub use gateway_ereport_messages as ereport_messages; pub use host_sp_messages::HostStartupOptions; #[cfg(feature = "microcbor")] -use microcbor::EreportData; +use microcbor::StaticCborLen; pub use oxide_barcode::OxideIdentity; /// Represents a range of allocated MAC addresses, per RFD 320 @@ -150,7 +150,7 @@ impl Packrat { // 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( + pub fn encode_ereport( &self, ereport: &E, buf: &mut [u8], From 057be162c30530d3b58b8c50bd76f958af679df9 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 11:21:06 -0700 Subject: [PATCH 27/30] rename derive macros too --- drv/gimlet-seq-server/src/main.rs | 8 ++++---- lib/microcbor-derive/src/lib.rs | 30 +++++++++++++++-------------- lib/microcbor/src/lib.rs | 6 +++--- lib/microcbor/tests/max_len.rs | 32 +++++++++++++++---------------- task/packrat-api/src/lib.rs | 8 ++++---- 5 files changed, 43 insertions(+), 41 deletions(-) diff --git a/drv/gimlet-seq-server/src/main.rs b/drv/gimlet-seq-server/src/main.rs index 199bd9b9d..a113c76a0 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -212,13 +212,13 @@ const EREPORT_BUF_LEN: usize = as microcbor::StaticCborLen>::MAX_CBOR_LEN; -#[derive(microcbor::EreportData)] +#[derive(microcbor::Encode)] pub enum EreportClass { - #[ereport(rename = "hw.pwr.pmbus.alert")] + #[cbor(rename = "hw.pwr.pmbus.alert")] PmbusAlert, } -#[derive(microcbor::EreportData)] +#[derive(microcbor::Encode)] pub(crate) enum EreportKind { PmbusAlert { refdes: fixedstr::FixedStr<{ crate::i2c_config::MAX_COMPONENT_ID_LEN }>, @@ -229,7 +229,7 @@ pub(crate) enum EreportKind { }, } -#[derive(Copy, Clone, Default, microcbor::EreportData)] +#[derive(Copy, Clone, Default, microcbor::Encode)] pub(crate) struct PmbusStatus { word: Option, input: Option, diff --git a/lib/microcbor-derive/src/lib.rs b/lib/microcbor-derive/src/lib.rs index 925f7cb47..58909962a 100644 --- a/lib/microcbor-derive/src/lib.rs +++ b/lib/microcbor-derive/src/lib.rs @@ -10,10 +10,10 @@ use syn::{ Visibility, parse_macro_input, }; -/// Derives an implementation of the `EreportData` trait for the annotated -/// `struct` or `enum` type. -#[proc_macro_derive(EreportData, attributes(ereport))] -pub fn derive_ereport_data(input: TokenStream) -> TokenStream { +/// Derives an implementation of the `Encode` and `StaticCborLen` traits for the +/// annotated `struct` or `enum` type. +#[proc_macro_derive(Encode, attributes(cbor))] +pub fn derive_encode(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); match gen_ereport_data_impl(input) { Ok(tokens) => tokens, @@ -43,11 +43,13 @@ fn gen_ereport_data_impl( .map(|tokens| tokens.to_token_stream().into()), _ => Err(syn::Error::new_spanned( input, - "`EreportData` can only be derived for `struct` and `enum` types", + "`StaticCborLen` can only be derived for `struct` and `enum` types", )), } } +const HELPER_ATTR: &str = "cbor"; + fn gen_enum_impl( _attrs: Vec, _vis: Visibility, @@ -64,7 +66,7 @@ fn gen_enum_impl( for variant in data.variants { let mut name = None; for attr in &variant.attrs { - if attr.path().is_ident("ereport") { + if attr.path().is_ident(HELPER_ATTR) { attr.meta.require_list()?.parse_nested_meta(|meta| { if meta.path.is_ident("rename") { name = Some(meta.value()?.parse::()?); @@ -266,7 +268,7 @@ fn gen_enum_impl( #maybe_fields_impl #[automatically_derived] - impl #impl_generics ::microcbor::EreportData + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics #prev_where_clause where #(#all_where_bounds,)* @@ -311,7 +313,7 @@ fn gen_struct_impl( syn::Fields::Unit => { return Err(syn::Error::new_spanned( &data.fields, - "`#[derive(EreportData)]` is not supported on unit structs", + "`#[derive(microcbor::Encode)]` is not supported on unit structs", )); } }; @@ -335,7 +337,7 @@ fn gen_struct_impl( // Structs with named fields are flattenable. (FieldType::Named, encode_exprs, len_exprs) => Ok(quote! { #[automatically_derived] - impl #impl_generics ::microcbor::EreportData for #ident #tygenerics + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { @@ -389,7 +391,7 @@ fn gen_struct_impl( // single value. (FieldType::Unnamed, [encode_expr], [len_expr]) => Ok(quote! { #[automatically_derived] - impl #impl_generics ::microcbor::EreportData for #ident #tygenerics + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { @@ -419,7 +421,7 @@ fn gen_struct_impl( }), (FieldType::Unnamed, encode_exprs, len_exprs) => Ok(quote! { #[automatically_derived] - impl #impl_generics ::microcbor::EreportData for #ident #tygenerics + impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics #prev_where_clause where #(#where_bounds,)* { @@ -499,7 +501,7 @@ impl FieldGenerator { let mut flattened = false; let mut skipped_if_nil = false; for attr in &field.attrs { - if attr.path().is_ident("ereport") { + if attr.path().is_ident(HELPER_ATTR) { attr.meta.require_list()?.parse_nested_meta(|meta| { if meta.path.is_ident("rename") { if field.ident.is_none() { @@ -595,7 +597,7 @@ impl FieldGenerator { } else { self.field_len_exprs.push(quote! { #name_len - len += <#field_type as ::microcbor::EreportData>::MAX_CBOR_LEN; + len += <#field_type as ::microcbor::StaticCborLen>::MAX_CBOR_LEN; }); self.field_encode_exprs.push(if skipped_if_nil { quote! { @@ -611,7 +613,7 @@ impl FieldGenerator { } }); self.where_bounds.push(quote! { - #field_type: ::microcbor::EreportData + #field_type: ::microcbor::StaticCborLen }); } diff --git a/lib/microcbor/src/lib.rs b/lib/microcbor/src/lib.rs index 577ab6349..f745a8807 100644 --- a/lib/microcbor/src/lib.rs +++ b/lib/microcbor/src/lib.rs @@ -34,7 +34,7 @@ //! [`minicbor-derive`]: https://docs.rs/minicbor-derive #![no_std] use encode::{Encoder, Write}; -pub use microcbor_derive::EreportData; +pub use microcbor_derive::Encode; pub use minicbor::encode::{self, Encode}; /// A CBOR-encodable value with a statically-known maximum length. @@ -60,13 +60,13 @@ pub trait StaticCborLen: Encode<()> { /// /// ```rust /// -/// #[derive(microcbor::EreportData)] +/// #[derive(microcbor::Encode)] /// pub struct MyGreatEreport { /// foo: u32, /// bar: Option, /// } /// -/// #[derive(microcbor::EreportData)] +/// #[derive(microcbor::Encode)] /// pub enum AnotherEreport { /// A { hello: bool, world: f64 }, /// B(usize), diff --git a/lib/microcbor/tests/max_len.rs b/lib/microcbor/tests/max_len.rs index cee65bc44..9878c62d5 100644 --- a/lib/microcbor/tests/max_len.rs +++ b/lib/microcbor/tests/max_len.rs @@ -2,38 +2,38 @@ // 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 ereport::EreportData; +use microcbor::{Encode, StaticCborLen}; use proptest::test_runner::TestCaseError; use proptest_derive::Arbitrary; -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] pub enum TestEnum { Variant1, - #[ereport(rename = "hw.foo.cool-ereport-class")] + #[cbor(rename = "hw.foo.cool-cbor-class")] Variant2, - #[ereport(rename = "hw.bar.baz.fault.computer_on_fire")] + #[cbor(rename = "hw.bar.baz.fault.computer_on_fire")] Variant3, } -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] struct TestStruct { - #[ereport(rename = "a")] + #[cbor(rename = "a")] field1: u32, field2: TestEnum, } -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] struct TestStruct2 { - #[ereport(skip_if_nil)] + #[cbor(skip_if_nil)] field6: Option, - #[ereport(flatten)] + #[cbor(flatten)] inner: D, } -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] enum TestEnum2 { Flattened { - #[ereport(flatten)] + #[cbor(flatten)] flattened: D, bar: u32, }, @@ -43,7 +43,7 @@ enum TestEnum2 { }, } -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] enum TestEnum3 { Named { field1: u32, @@ -53,13 +53,13 @@ enum TestEnum3 { UnnamedSingle(f64), } -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] struct TestTupleStruct1(u32, u64); -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] struct TestTupleStruct2(u64); -#[derive(Debug, EreportData, Arbitrary)] +#[derive(Debug, Encode, Arbitrary)] struct TestStructWithArrays { bytes: [u8; 10], enums: [TestEnum3; 4], @@ -67,7 +67,7 @@ struct TestStructWithArrays { } #[track_caller] -fn assert_max_len( +fn assert_max_len( input: &T, ) -> Result<(), TestCaseError> { let max_size = T::MAX_CBOR_LEN; diff --git a/task/packrat-api/src/lib.rs b/task/packrat-api/src/lib.rs index 88a6cfb2c..e120a6c6e 100644 --- a/task/packrat-api/src/lib.rs +++ b/task/packrat-api/src/lib.rs @@ -103,13 +103,13 @@ pub enum EreportEncodeError { /// Wrapper type defining common ereport fields. #[cfg(feature = "microcbor")] -#[derive(Clone, EreportData)] +#[derive(Clone, microcbor::Encode)] pub struct Ereport { - #[ereport(rename = "k")] + #[cbor(rename = "k")] pub class: C, - #[ereport(rename = "v")] + #[cbor(rename = "v")] pub version: u32, - #[ereport(flatten)] + #[cbor(flatten)] pub report: D, } From d28fff005fade3310f14b6974472c6abeb4a0e6d Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 11:44:54 -0700 Subject: [PATCH 28/30] separate derives for `Encode` and `EncodeFields` having the `EncodeFields` derive *fail* if the type can't be flattened felt less like spooky action at a distance than always trying to derive both `Encode` and `EncodeFields` and silently giving up if unnamed fields are encountered... --- drv/gimlet-seq-server/src/main.rs | 2 +- lib/microcbor-derive/src/lib.rs | 308 ++++++++++++++++++++---------- lib/microcbor/examples/test.rs | 28 +-- lib/microcbor/src/lib.rs | 2 +- lib/microcbor/tests/max_len.rs | 8 +- 5 files changed, 222 insertions(+), 126 deletions(-) diff --git a/drv/gimlet-seq-server/src/main.rs b/drv/gimlet-seq-server/src/main.rs index a113c76a0..4fe214b7b 100644 --- a/drv/gimlet-seq-server/src/main.rs +++ b/drv/gimlet-seq-server/src/main.rs @@ -218,7 +218,7 @@ pub enum EreportClass { PmbusAlert, } -#[derive(microcbor::Encode)] +#[derive(microcbor::EncodeFields)] pub(crate) enum EreportKind { PmbusAlert { refdes: fixedstr::FixedStr<{ crate::i2c_config::MAX_COMPONENT_ID_LEN }>, diff --git a/lib/microcbor-derive/src/lib.rs b/lib/microcbor-derive/src/lib.rs index 58909962a..0328a5c23 100644 --- a/lib/microcbor-derive/src/lib.rs +++ b/lib/microcbor-derive/src/lib.rs @@ -15,17 +15,38 @@ use syn::{ #[proc_macro_derive(Encode, attributes(cbor))] pub fn derive_encode(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - match gen_ereport_data_impl(input) { + match gen_encode_impl(input) { Ok(tokens) => tokens, Err(err) => err.to_compile_error().into(), } } -fn gen_ereport_data_impl( - input: DeriveInput, -) -> Result { +/// 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. +#[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_impl( + syn::Data::Enum(data) => gen_enum_encode_impl( input.attrs, input.vis, input.ident, @@ -33,7 +54,7 @@ fn gen_ereport_data_impl( data.clone(), ) .map(|tokens| tokens.to_token_stream().into()), - syn::Data::Struct(data) => gen_struct_impl( + syn::Data::Struct(data) => gen_encode_struct_impl( input.attrs, input.vis, input.ident, @@ -50,7 +71,7 @@ fn gen_ereport_data_impl( const HELPER_ATTR: &str = "cbor"; -fn gen_enum_impl( +fn gen_enum_encode_impl( _attrs: Vec, _vis: Visibility, ident: Ident, @@ -59,7 +80,6 @@ fn gen_enum_impl( ) -> Result { let mut variant_patterns = Vec::new(); let mut variant_lens = Vec::new(); - let mut flattened = Some((Vec::new(), Vec::new())); let mut all_where_bounds = Vec::new(); // TODO(eliza): support top-level attribute for using the enum's repr // instead of its name @@ -84,9 +104,6 @@ fn gen_enum_impl( let variant_name = &variant.ident; match variant.fields { syn::Fields::Unit => { - // If there's a unit variant, we cannot generate an - // `EncodeField` impl for flattening this type. - flattened = None; variant_patterns.push(quote! { #ident::#variant_name => { e.str(#name)?; } }); @@ -131,37 +148,8 @@ fn gen_enum_impl( max = #variant_name; } }); - // If we are still able to generate a flattened impl, add to that. - if let Some(( - ref mut flattened_lens, - ref mut flattened_patterns, - )) = flattened - { - flattened_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; - } - }); - flattened_patterns.push(quote! { - #match_pattern => { - #(#field_encode_exprs)* - } - }); - } } syn::Fields::Unnamed(fields) => { - // If we've encountered a tuple variant, we can no longer - // flatten named fields. - flattened = None; - let mut field_gen = FieldGenerator::for_variant(FieldType::Unnamed); for field in &fields.unnamed { @@ -229,44 +217,7 @@ fn gen_enum_impl( } let (impl_generics, tygenerics, prev_where_clause) = generics.split_for_impl(); - - // If all variants of this enum contain multiple named fields (and can - // therefore be flattened into an enclosing struct), generate an - // `EncodeFields` impl. - let maybe_fields_impl = - if let Some((flattened_lens, flattened_encode_patterns)) = flattened { - 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; - #(#flattened_lens;)* - max - }; - - fn encode_fields( - &self, - e: &mut ::microcbor::encode::Encoder, - c: &mut (), - ) -> Result<(), ::microcbor::encode::Error> { - match self { - #(#flattened_encode_patterns,)* - } - Ok(()) - } - } - } - } else { - quote! {} - }; - Ok(quote! { - #maybe_fields_impl - #[automatically_derived] impl #impl_generics ::microcbor::StaticCborLen for #ident #tygenerics @@ -300,7 +251,34 @@ fn gen_enum_impl( }) } -fn gen_struct_impl( +fn gen_encode_fields_impl( + input: DeriveInput, +) -> Result { + match &input.data { + syn::Data::Enum(data) => gen_encode_fields_enum_impl( + input.attrs, + input.vis, + input.ident, + input.generics, + data.clone(), + ) + .map(|tokens| tokens.to_token_stream().into()), + syn::Data::Struct(data) => gen_encode_fields_struct_impl( + input.attrs, + input.vis, + 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_struct_impl( _attrs: Vec, _vis: Visibility, ident: Ident, @@ -318,7 +296,6 @@ fn gen_struct_impl( } }; let mut field_gen = FieldGenerator::for_struct(field_type); - // let mut data_where_bounds = Vec::new(); for field in &data.fields { field_gen.add_field(field)?; } @@ -334,16 +311,17 @@ fn gen_struct_impl( } = field_gen; match (field_type, &field_encode_exprs[..], &field_len_exprs[..]) { - // Structs with named fields are flattenable. (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 = - 2 // map begin and end bytes - + >::MAX_FIELDS_LEN; + const MAX_CBOR_LEN: usize = { + let mut len = 2; // map begin and end bytes + #(#len_exprs;)* + len + }; } #[automatically_derived] @@ -357,32 +335,11 @@ fn gen_struct_impl( e: &mut ::microcbor::encode::Encoder, c: &mut (), ) -> Result<(), ::microcbor::encode::Error> { - e.begin_map()?; - >::encode_fields(self, e, c)?; - e.end()?; - Ok(()) - } - } - #[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; - #(#len_exprs;)* - len - }; - - fn encode_fields( - &self, - e: &mut ::microcbor::encode::Encoder, - c: &mut (), - ) -> Result<(), ::microcbor::encode::Error> { let Self { #(#field_patterns,)* } = self; + e.begin_map()?; #(#encode_exprs)* + e.end()?; Ok(()) } } @@ -457,6 +414,145 @@ fn gen_struct_impl( } } +fn gen_encode_fields_struct_impl( + _attrs: Vec, + _vis: Visibility, + 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::for_struct(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, + e: &mut ::microcbor::encode::Encoder, + c: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + let Self { #(#field_patterns,)* } = self; + #(#field_encode_exprs)* + Ok(()) + } + } + }) +} + +fn gen_encode_fields_enum_impl( + _attrs: Vec, + _vis: Visibility, + ident: Ident, + generics: Generics, + data: DataEnum, +) -> Result { + 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 syn::Fields::Named(ref fields) = variant.fields else { + // If there's a unit variant, we cannot generate an + // `EncodeField` impl for flattening this type. + return Err(syn::Error::new_spanned( + &variant, + "deriving `microcbor::EncodeFields` for an `enum` type requires that all variants have named fields", + )); + }; + + let mut field_gen = FieldGenerator::for_variant(FieldType::Named); + for field in &fields.named { + field_gen.add_field(field)?; + } + 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, + e: &mut ::microcbor::encode::Encoder, + c: &mut (), + ) -> Result<(), ::microcbor::encode::Error> { + match self { + #(#variant_patterns,)* + } + Ok(()) + } + } + }) +} + struct FieldGenerator { field_patterns: Vec, field_len_exprs: Vec, diff --git a/lib/microcbor/examples/test.rs b/lib/microcbor/examples/test.rs index a521c10da..fa1ebb8f0 100644 --- a/lib/microcbor/examples/test.rs +++ b/lib/microcbor/examples/test.rs @@ -2,34 +2,34 @@ // 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 ereport::EreportData; +use microcbor::{Encode, EncodeFields, StaticCborLen}; -#[derive(Debug, EreportData)] +#[derive(Debug, Encode)] pub enum TestEnum { Variant1, - #[ereport(rename = "hw.foo.cool-ereport-class")] + #[cbor(rename = "hw.foo.cool-ereport-class")] Variant2, } -#[derive(Debug, EreportData)] +#[derive(Debug, Encode, EncodeFields)] struct TestStruct { - #[ereport(rename = "a")] + #[cbor(rename = "a")] field1: u32, field2: TestEnum, } -#[derive(Debug, EreportData)] +#[derive(Debug, Encode)] struct TestStruct2 { - #[ereport(skip_if_nil)] + #[cbor(skip_if_nil)] field6: Option, - #[ereport(flatten)] + #[cbor(flatten)] inner: D, } -#[derive(Debug, EreportData)] +#[derive(Debug, Encode, EncodeFields)] enum TestEnum2 { Flattened { - #[ereport(flatten)] + #[cbor(flatten)] flattened: D, bar: u32, }, @@ -39,7 +39,7 @@ enum TestEnum2 { }, } -#[derive(Debug, EreportData)] +#[derive(Debug, Encode)] enum TestEnum3 { Named { field1: u32, @@ -49,10 +49,10 @@ enum TestEnum3 { UnnamedSingle(f64), } -#[derive(Debug, EreportData)] +#[derive(Debug, Encode)] struct TestTupleStruct1(u32, u64); -#[derive(Debug, EreportData)] +#[derive(Debug, Encode)] struct TestTupleStruct2(u64); fn main() { @@ -134,7 +134,7 @@ fn main() { test_one_type(TestEnum3::UnnamedSingle(42069.0), &mut buf); } -fn test_one_type(input: T, buf: &mut [u8]) { +fn test_one_type(input: T, buf: &mut [u8]) { println!( "{}::MAX_CBOR_LEN = {}", std::any::type_name::(), diff --git a/lib/microcbor/src/lib.rs b/lib/microcbor/src/lib.rs index f745a8807..d50584cb6 100644 --- a/lib/microcbor/src/lib.rs +++ b/lib/microcbor/src/lib.rs @@ -34,7 +34,7 @@ //! [`minicbor-derive`]: https://docs.rs/minicbor-derive #![no_std] use encode::{Encoder, Write}; -pub use microcbor_derive::Encode; +pub use microcbor_derive::{Encode, EncodeFields}; pub use minicbor::encode::{self, Encode}; /// A CBOR-encodable value with a statically-known maximum length. diff --git a/lib/microcbor/tests/max_len.rs b/lib/microcbor/tests/max_len.rs index 9878c62d5..5773d8f8c 100644 --- a/lib/microcbor/tests/max_len.rs +++ b/lib/microcbor/tests/max_len.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/. -use microcbor::{Encode, StaticCborLen}; +use microcbor::{Encode, EncodeFields, StaticCborLen}; use proptest::test_runner::TestCaseError; use proptest_derive::Arbitrary; @@ -15,14 +15,14 @@ pub enum TestEnum { Variant3, } -#[derive(Debug, Encode, Arbitrary)] +#[derive(Debug, Encode, EncodeFields, Arbitrary)] struct TestStruct { #[cbor(rename = "a")] field1: u32, field2: TestEnum, } -#[derive(Debug, Encode, Arbitrary)] +#[derive(Debug, Encode, EncodeFields, Arbitrary)] struct TestStruct2 { #[cbor(skip_if_nil)] field6: Option, @@ -30,7 +30,7 @@ struct TestStruct2 { inner: D, } -#[derive(Debug, Encode, Arbitrary)] +#[derive(Debug, Encode, EncodeFields, Arbitrary)] enum TestEnum2 { Flattened { #[cbor(flatten)] From 4c9e086182302317409ed6eb311fb74b67445795 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 11:53:14 -0700 Subject: [PATCH 29/30] line wrap errors --- lib/microcbor-derive/src/lib.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/microcbor-derive/src/lib.rs b/lib/microcbor-derive/src/lib.rs index 0328a5c23..5b3987027 100644 --- a/lib/microcbor-derive/src/lib.rs +++ b/lib/microcbor-derive/src/lib.rs @@ -64,7 +64,8 @@ fn gen_encode_impl(input: DeriveInput) -> Result { .map(|tokens| tokens.to_token_stream().into()), _ => Err(syn::Error::new_spanned( input, - "`StaticCborLen` can only be derived for `struct` and `enum` types", + "`StaticCborLen` can only be derived for `struct` and `enum` \ + types", )), } } @@ -273,7 +274,8 @@ fn gen_encode_fields_impl( .map(|tokens| tokens.to_token_stream().into()), _ => Err(syn::Error::new_spanned( input, - "`microcbor::EncodeFields` can only be derived for `struct` and `enum` types", + "`microcbor::EncodeFields` can only be derived for `struct` and \ + `enum` types", )), } } @@ -291,7 +293,8 @@ fn gen_encode_struct_impl( syn::Fields::Unit => { return Err(syn::Error::new_spanned( &data.fields, - "`#[derive(microcbor::Encode)]` is not supported on unit structs", + "`#[derive(microcbor::Encode)]` is not supported on unit \ + structs", )); } }; @@ -424,7 +427,8 @@ fn gen_encode_fields_struct_impl( 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", + "`microcbor::EncodeFields` may only be derived for structs with \ + named fields", )); }; let mut field_gen = FieldGenerator::for_struct(FieldType::Named); @@ -485,7 +489,8 @@ fn gen_encode_fields_enum_impl( // `EncodeField` impl for flattening this type. return Err(syn::Error::new_spanned( &variant, - "deriving `microcbor::EncodeFields` for an `enum` type requires that all variants have named fields", + "deriving `microcbor::EncodeFields` for an `enum` type \ + requires that all variants have named fields", )); }; @@ -602,7 +607,7 @@ impl FieldGenerator { if meta.path.is_ident("rename") { if field.ident.is_none() { return Err(meta.error( - "`#[ereport(rename = \"...\")]` is only + "`#[cbor(rename = \"...\")]` is only \ supported on named fields", )); } @@ -617,7 +622,7 @@ impl FieldGenerator { } else if meta.path.is_ident("flatten") { if self.field_type == FieldType::Unnamed { return Err(meta.error( - "`#[ereport(flatten)]` is only supported on \ + "`#[cbor(flatten)]` is only supported on \ structs and enum variants with named fields", )); } @@ -625,7 +630,8 @@ impl FieldGenerator { Ok(()) } else { Err(meta.error( - "expected `rename`, `skip`, `skip_if_nil`, or `flatten` attribute", + "expected `rename`, `skip`, `skip_if_nil`, or \ + `flatten` attribute", )) } })?; From ca11c7564898dd154b52ea75260fa1ef4c80b41d Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 30 Sep 2025 12:29:13 -0700 Subject: [PATCH 30/30] document all the things --- lib/microcbor-derive/src/lib.rs | 101 +++++++++++++++++++++++++++++++- lib/microcbor/src/lib.rs | 37 ++++++++++-- 2 files changed, 132 insertions(+), 6 deletions(-) diff --git a/lib/microcbor-derive/src/lib.rs b/lib/microcbor-derive/src/lib.rs index 5b3987027..9bea55e9d 100644 --- a/lib/microcbor-derive/src/lib.rs +++ b/lib/microcbor-derive/src/lib.rs @@ -10,8 +10,99 @@ use syn::{ Visibility, parse_macro_input, }; -/// Derives an implementation of the `Encode` and `StaticCborLen` traits for the +/// 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. +/// +/// # 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. +/// +/// ## Field Attributes +/// +/// The following `#[cbor(..)]` attributes are supported on fields of structs +/// and enum variants: +/// +/// - `#[cbor(skip)]`: Completely skip ignoring 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 no tbe 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. #[proc_macro_derive(Encode, attributes(cbor))] pub fn derive_encode(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -21,7 +112,7 @@ pub fn derive_encode(input: TokenStream) -> TokenStream { } } -/// Derives an implementation of the `EncodeFields` trait for the annotated +/// Derives an implementation of the [`EncodeFields`] trait for the annotated /// `struct` or `enum` type. /// /// Deriving `EncodeFields` allows the implementing type to be annotated with @@ -35,6 +126,12 @@ pub fn derive_encode(input: TokenStream) -> TokenStream { /// /// 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); diff --git a/lib/microcbor/src/lib.rs b/lib/microcbor/src/lib.rs index d50584cb6..ba7858a2c 100644 --- a/lib/microcbor/src/lib.rs +++ b/lib/microcbor/src/lib.rs @@ -2,11 +2,39 @@ // 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/. -//! Fixed-length CBOR encoding.. +//! 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)]`? //! -//! TODO ELIZA EXPLAIN +//! 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)]`? //! @@ -34,6 +62,7 @@ //! [`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}; @@ -50,11 +79,11 @@ pub trait StaticCborLen: Encode<()> { const MAX_CBOR_LEN: usize; } -/// For a list of types implementing [`EreportData`], returns the maximum length +/// 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 [`EreportData`]. +/// encode any of a set of types implementing [`StaticCborLen`]. /// /// For example: ///