diff --git a/Cargo.lock b/Cargo.lock index ce4c144..f9a66d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,18 +460,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] diff --git a/Cargo.toml b/Cargo.toml index debfb80..d8618ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -amd-apcb = { git = "https://github.com/oxidecomputer/amd-apcb.git", tag = "v0.1.5", features = ["std", "serde", "schemars"] } -amd-efs = { git = "ssh://git@github.com/oxidecomputer/amd-efs.git", tag = "v0.3.0", features = ["std", "serde", "schemars"] } +amd-apcb = { git = "https://github.com/oxidecomputer/amd-apcb.git", branch = "issue-113", features = ["std", "serde", "schemars"] } +amd-efs = { git = "ssh://git@github.com/oxidecomputer/amd-efs.git", branch = "issue-99", features = ["std", "serde", "schemars"] } amd-flash = { git = "ssh://git@github.com/oxidecomputer/amd-flash.git", tag = "v0.2.1", features = ["std"] } goblin = { version = "0.4", features = ["elf64", "endian_fd"] } #serde = { version = "1.0", default-features = false, features = ["derive"] } diff --git a/amd-host-image-builder-config/Cargo.toml b/amd-host-image-builder-config/Cargo.toml index 8f325fe..2ce21e7 100644 --- a/amd-host-image-builder-config/Cargo.toml +++ b/amd-host-image-builder-config/Cargo.toml @@ -6,8 +6,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -amd-apcb = { git = "https://github.com/oxidecomputer/amd-apcb.git", tag = "v0.1.5", features = ["std", "serde", "schemars"] } -amd-efs = { git = "ssh://git@github.com/oxidecomputer/amd-efs.git", tag = "v0.3.0", features = ["std", "serde", "schemars"] } +amd-apcb = { git = "https://github.com/oxidecomputer/amd-apcb.git", branch = "issue-113", features = ["std", "serde", "schemars"] } +amd-efs = { git = "ssh://git@github.com/oxidecomputer/amd-efs.git", branch = "issue-99", features = ["std", "serde", "schemars"] } amd-flash = { git = "ssh://git@github.com/oxidecomputer/amd-flash.git", tag = "v0.2.1", features = ["std"] } schemars = "0.8.8" serde = { version = "1.0", default-features = false, features = ["derive"] } diff --git a/src/main.rs b/src/main.rs index 4c96305..dd0b89b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use amd_apcb::{Apcb, ApcbIoOptions}; use amd_efs::{ AddressMode, BhdDirectory, BhdDirectoryEntry, BhdDirectoryEntryType, - DirectoryEntry, Efs, ProcessorGeneration, PspDirectory, PspDirectoryEntry, + DirectoryEntry, EfhBulldozerSpiMode, EfhNaplesSpiMode, EfhRomeSpiMode, Efs, + ProcessorGeneration, PspDirectory, PspDirectoryEntry, PspDirectoryEntryType, ValueOrLocation, }; use amd_host_image_builder_config::{ @@ -19,6 +20,7 @@ use std::cmp::min; use std::collections::HashSet; use std::fs; use std::fs::File; +use std::io::stdout; use std::io::BufReader; use std::io::Read; use std::io::Seek; @@ -30,6 +32,9 @@ use structopt::StructOpt; mod static_config; use amd_flash::allocators::{ArenaFlashAllocator, FlashAllocate}; +mod serializers; +use serializers::DummySerializer; + use amd_flash::{ ErasableLocation, ErasableRange, FlashAlign, FlashRead, FlashWrite, Location, @@ -634,6 +639,29 @@ fn serde_from_bhd_entry( } } +/// Try to return RESULT?. +/// If that doesn't work, print RESULT_TEXT and then return FALLBACK instead. +/// This is useful because some images don't have SPI mode set. As far as +/// we can tell, the SPI mode is mandatory. In order not to fail the entire +/// dump just because of the SPI mode, we just make one up here. +fn spi_mode_fallback_on_error( + result: std::result::Result, + fallback: T, + result_text: &str, +) -> T { + match result { + Ok(x) => x, + Err(e) => { + eprintln!( + "{} was invalid: {}. Falling back to default.", + result_text, e + ); + // TODO: Maybe set program error status somehow + fallback + } + } +} + fn dump_bhd_directory<'a, T: FlashRead + FlashWrite>( storage: &T, bhd_directory: &BhdDirectory, @@ -653,6 +681,11 @@ fn dump_bhd_directory<'a, T: FlashRead + FlashWrite>( .map_while(|entry| { let entry = entry.clone(); if let Ok(typ) = entry.typ_or_err() { + if typ == BhdDirectoryEntryType::Apob { + // Since this is a runtime value we cannot read it + // from the image. + return None; + } let payload_beginning = bhd_directory.payload_beginning(&entry).unwrap(); let size = entry.size().unwrap() as usize; @@ -669,11 +702,14 @@ fn dump_bhd_directory<'a, T: FlashRead + FlashWrite>( ) .unwrap(); + let apcb_options = ApcbIoOptions::builder() + .with_check_checksum(false) + .build(); let apcb = Apcb::load( std::borrow::Cow::Borrowed( &mut apcb_buffer[..], ), - &ApcbIoOptions::default(), + &apcb_options, ) .unwrap(); apcb.validate(None).unwrap(); // TODO: abl0 version ? @@ -727,6 +763,7 @@ fn dump_bhd_directory<'a, T: FlashRead + FlashWrite>( fn dump( image_filename: &Path, blob_dump_dirname: Option, + output_config_file: &mut impl std::io::Write, ) -> std::io::Result<()> { let filename = image_filename; let storage = FlashImage::load(filename)?; @@ -735,25 +772,48 @@ fn dump( if filesize <= 0x100_0000 { Some(filesize as u32) } else { None }; let efs = Efs::load(&storage, None, amd_physical_mode_mmio_size).unwrap(); if !efs.compatible_with_processor_generation(ProcessorGeneration::Milan) { - panic!("only Milan is supported for dumping right now"); + if !efs.compatible_with_processor_generation(ProcessorGeneration::Rome) + { + panic!("only Milan or Rome is supported for dumping right now"); + } } let mut apcb_buffer = [0xFFu8; Apcb::MAX_SIZE]; let mut apcb_buffer_option = Some(&mut apcb_buffer[..]); + let processor_generation = if efs + .compatible_with_processor_generation(ProcessorGeneration::Milan) + { + ProcessorGeneration::Milan + } else { + ProcessorGeneration::Rome + }; let config = SerdeConfig { - processor_generation: ProcessorGeneration::Milan, // FIXME could be ambiguous - spi_mode_bulldozer: efs.spi_mode_bulldozer().unwrap(), - spi_mode_zen_naples: efs.spi_mode_zen_naples().unwrap(), - spi_mode_zen_rome: efs.spi_mode_zen_rome().unwrap(), + processor_generation, + spi_mode_bulldozer: spi_mode_fallback_on_error( + efs.spi_mode_bulldozer(), + EfhBulldozerSpiMode::default(), + "Bulldozer SPI Mode", + ), + spi_mode_zen_naples: spi_mode_fallback_on_error( + efs.spi_mode_zen_naples(), + EfhNaplesSpiMode::default(), + "Naples SPI Mode", + ), + spi_mode_zen_rome: spi_mode_fallback_on_error( + efs.spi_mode_zen_rome(), + EfhRomeSpiMode::default(), + "Rome SPI Mode", + ), // TODO: psp_directory or psp_combo_directory psp: dump_psp_directory( &storage, - &efs.psp_directory().unwrap(), + &efs.psp_directory().expect("PSP directory"), &blob_dump_dirname, ), // TODO: bhd_directory or bhd_combo_directory bhd: dump_bhd_directory( &storage, - &efs.bhd_directory(None).unwrap(), + &efs.bhd_directory(Some(processor_generation)) + .expect("BHD directory"), &mut apcb_buffer_option, &blob_dump_dirname, ), @@ -766,7 +826,11 @@ fn dump( let mut file = File::create(&path).expect("creation failed"); writeln!(file, "{}", json5::to_string(&config).unwrap())?; } else { - println!("{}", serde_json::to_string_pretty(&config)?); + writeln!( + output_config_file, + "{}", + serde_json::to_string_pretty(&config)? + )?; } Ok(()) } @@ -1155,7 +1219,7 @@ fn run() -> std::io::Result<()> { }; match opts { Opts::Dump { input_filename, blob_dump_dirname } => { - dump(&input_filename, blob_dump_dirname) + dump(&input_filename, blob_dump_dirname, &mut stdout().lock()) } Opts::Generate { output_filename, @@ -1163,13 +1227,34 @@ fn run() -> std::io::Result<()> { reset_image_filename, blobdirs, verbose, - } => generate( - &output_filename, - &efs_configuration_filename, - &reset_image_filename, - blobdirs, - verbose, - ), + } => { + let x = generate( + &output_filename, + &efs_configuration_filename, + &reset_image_filename, + blobdirs, + verbose, + ); + + // In order to make sure that we can dump it back out, try it. + struct DummyOutput {} + impl std::io::Write for DummyOutput { + fn write( + &mut self, + buf: &[u8], + ) -> std::result::Result + { + Ok(buf.len()) + } + fn flush(&mut self) -> std::result::Result<(), std::io::Error> { + Ok(()) + } + } + let mut dummy_output = DummyOutput {}; + dump(&output_filename, None, &mut dummy_output) + .expect("read it back out from the image"); + x + } } } diff --git a/src/serializers.rs b/src/serializers.rs new file mode 100644 index 0000000..a43bb75 --- /dev/null +++ b/src/serializers.rs @@ -0,0 +1,668 @@ +use serde::{ser, Serialize}; +use std::fmt::{Display, Formatter}; +use std::rc::Rc; +use std::result::Result; + +// TODO: maybe use a 'proxy' approach like serde path-to-error. "trigger" will actually show the path. + +pub(crate) struct PathNode { + text: String, + next: Option>, +} +impl PathNode { + pub(crate) fn append( + next: Option>, + text: String, + ) -> Option> { + Some(Rc::new(Self { text, next })) + } +} +impl std::fmt::Display for PathNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.next { + Some(ref x) => { + x.fmt(f)?; + write!(f, "/")?; + } + None => {} + } + write!(f, "{}", self.text) + } +} + +/// This serializer actually doesn't have an output. +/// Its purpose is to notice when fields are skipped. +/// Since our Permissive Serializers skip fields on error, that means +/// that we encountered a raw value we don't know while serializing. +/// This serializer here makes sure to log the path to that raw value +/// on standard error. +pub(crate) struct DummySerializer { + pub(crate) path: Option>, +} + +#[derive(Debug)] +pub struct Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl ser::StdError for Error {} + +impl ser::Error for Error { + fn custom(msg: T) -> Self + where + T: Display, + { + todo!() + } +} + +type Ok = (); + +pub struct SerializeVec { + pub(crate) path: Option>, +} + +impl serde::ser::SerializeSeq for SerializeVec { + type Ok = Ok; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + value.serialize(&mut DummySerializer { + path: self.path.clone(), // TODO: Count elements or something + }) + } + + fn end(self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl serde::ser::SerializeTupleStruct for SerializeVec { + type Ok = Ok; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result<(), Self::Error> { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTuple for SerializeVec { + type Ok = Ok; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result<(), Self::Error> { + serde::ser::SerializeSeq::end(self) + } +} + +pub struct SerializeMap { + pub(crate) path: Option>, +} + +impl serde::ser::SerializeMap for SerializeMap { + type Ok = OK; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + key.serialize(&mut DummySerializer { + path: self.path.clone(), // TODO: maybe more complicated keys ? + }) + } + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + value.serialize(&mut DummySerializer { + path: self.path.clone(), // TODO: maybe more complicated values ? + }) + } + fn end(self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl serde::ser::SerializeStruct for SerializeMap { + type Ok = Ok; + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + value.serialize(&mut DummySerializer { + path: PathNode::append(self.path.clone(), key.to_string()), + }) + } + + fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> { + eprintln!( + "{}: skipped field {}", + match self.path { + Some(ref x) => { + x.to_string() + } + None => { + "".to_string() + } + }, + key + ); + Ok(()) + } + + fn end(self) -> Result<(), Self::Error> { + Ok(()) + } +} + +// TODO SerializeStructVariant + +pub struct SerializeTupleVariant { + pub(crate) path: Option>, +} + +impl serde::ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Ok; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + value.serialize(&mut DummySerializer { + path: self.path.clone(), // TODO: tuple variant + }) + } + + fn end(self) -> Result<(), Self::Error> { + Ok(()) + } +} + +pub struct SerializeStructVariant { + name: String, + pub(crate) path: Option>, +} + +impl serde::ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Ok; + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + value.serialize(&mut DummySerializer { + path: PathNode::append(self.path.clone(), name.to_string()), + }) + } + + fn end(self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl<'a> ser::Serializer for &'a mut DummySerializer { + type Ok = Ok; + type Error = Error; + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + fn serialize_bool(self, _v: bool) -> Result { + Ok(()) + } + + fn serialize_i8(self, _v: i8) -> Result { + Ok(()) + } + + fn serialize_i16(self, _v: i16) -> Result { + Ok(()) + } + + fn serialize_i32(self, _v: i32) -> Result { + Ok(()) + } + + fn serialize_i64(self, _v: i64) -> Result { + Ok(()) + } + + fn serialize_u8(self, _v: u8) -> Result { + Ok(()) + } + + fn serialize_u16(self, _v: u16) -> Result { + Ok(()) + } + + fn serialize_u32(self, _v: u32) -> Result { + Ok(()) + } + + fn serialize_u64(self, _v: u64) -> Result { + Ok(()) + } + + fn serialize_f32(self, _v: f32) -> Result { + Ok(()) + } + + fn serialize_f64(self, _v: f64) -> Result { + Ok(()) + } + + fn serialize_char(self, _v: char) -> Result { + Ok(()) + } + + fn serialize_str(self, _v: &str) -> Result { + Ok(()) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Ok(()) + } + + fn serialize_none(self) -> Result { + self.serialize_unit() + } + + fn serialize_some( + self, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(()) + } + + fn serialize_unit_struct( + self, + name: &'static str, + ) -> Result { + Ok(()) + } + + fn serialize_unit_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_seq( + self, + len: Option, + ) -> Result { + Ok(SerializeVec { + path: self.path.clone(), // FIXME + }) + } + + fn serialize_tuple( + self, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(SerializeTupleVariant { + // name: String::from(variant), + path: self.path.clone(), // FIXME name variant + }) + } + + fn serialize_map( + self, + len: Option, + ) -> Result { + Ok(SerializeMap { + path: self.path.clone(), // FIXME check + }) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + // TODO: More features + Ok(SerializeMap { + path: PathNode::append(self.path.clone(), name.to_string()), + }) + } + + fn serialize_struct_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(SerializeStructVariant { + name: String::from(variant), + path: PathNode::append( + PathNode::append(self.path.clone(), name.to_string()), + variant.to_string(), + ), + }) + } +} + +impl<'a> ser::SerializeSeq for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + value.serialize(&mut DummySerializer { + path: self.path.clone(), // FIXME check + })?; + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'a> ser::SerializeTuple for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl<'a> ser::SerializeTupleStruct for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl<'a> ser::SerializeTupleVariant for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + value.serialize(&mut DummySerializer { + path: self.path.clone(), // FIXME check + })?; + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'a> ser::SerializeMap for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + key.serialize(&mut DummySerializer { path: self.path.clone() })?; + Ok(()) + } + + fn serialize_value( + &mut self, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + value.serialize(&mut DummySerializer { + path: self.path.clone(), // FIXME check + })?; + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'a> ser::SerializeStruct for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + value.serialize(&mut DummySerializer { + path: PathNode::append(self.path.clone(), key.to_string()), + })?; + Ok(()) + } + + fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> { + eprintln!( + "{}: skipped field {}", + match self.path { + Some(ref x) => { + x.to_string() + } + None => { + "".to_string() + } + }, + key + ); + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'a> ser::SerializeStructVariant for &'a mut DummySerializer { + // Must match the `Ok` type of the serializer. + type Ok = Ok; + // Must match the `Error` type of the serializer. + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + value.serialize(&mut DummySerializer { + path: PathNode::append(self.path.clone(), key.to_string()), + })?; + Ok(()) + } + + fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> { + eprintln!( + "{}: skipped field {}", + match self.path { + Some(ref x) => { + x.to_string() + } + None => { + "".to_string() + } + }, + key + ); + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +// By convention, the public API of a Serde serializer is one or more `to_abc` +// functions such as `to_string`, `to_bytes`, or `to_writer` depending on what +// Rust types the serializer is able to produce as output. +// +// This basic serializer supports only `to_string`. +fn to_string(value: &T) +where + T: Serialize, +{ + let mut serializer = DummySerializer { path: None }; + value.serialize(&mut serializer).unwrap(); +} + +#[test] +fn test_struct() { + use serde::Serialize; + #[derive(Serialize)] + struct Test { + #[serde(skip_serializing_if = "Option::is_none")] + a: Option, + #[serde(skip_serializing_if = "Option::is_none")] + b: Option, + } + + let test = Test { a: None, b: Some(1) }; + to_string(&test); +} + +// See serde_test maybe