From 3fcc7e2f2028069a55e46c4317dfae4364419e96 Mon Sep 17 00:00:00 2001 From: Yosh Date: Thu, 5 Dec 2024 03:33:06 +0100 Subject: [PATCH] [wasm-metadata] add OCI description support --- crates/wasm-metadata/src/add_metadata.rs | 7 +- crates/wasm-metadata/src/lib.rs | 2 +- crates/wasm-metadata/src/metadata.rs | 15 ++- .../src/oci_annotations/description.rs | 110 ++++++++++++++++++ .../wasm-metadata/src/oci_annotations/mod.rs | 2 + crates/wasm-metadata/src/producers.rs | 2 +- crates/wasm-metadata/src/registry.rs | 2 +- crates/wasm-metadata/src/rewrite.rs | 13 ++- crates/wasm-metadata/tests/component.rs | 10 +- crates/wasm-metadata/tests/module.rs | 5 +- 10 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 crates/wasm-metadata/src/oci_annotations/description.rs diff --git a/crates/wasm-metadata/src/add_metadata.rs b/crates/wasm-metadata/src/add_metadata.rs index cb18569476..fe04adbbae 100644 --- a/crates/wasm-metadata/src/add_metadata.rs +++ b/crates/wasm-metadata/src/add_metadata.rs @@ -1,4 +1,4 @@ -use crate::{rewrite_wasm, Author, Producers, RegistryMetadata}; +use crate::{rewrite_wasm, Author, Description, Producers, RegistryMetadata}; use anyhow::Result; @@ -30,6 +30,10 @@ pub struct AddMetadata { #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))] pub author: Option, + /// A human-readable description of the binary + #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))] + pub description: Option, + /// Add an registry metadata to the registry-metadata section #[cfg_attr(feature="clap", clap(long, value_parser = parse_registry_metadata_value, value_name="PATH"))] pub registry_metadata: Option, @@ -60,6 +64,7 @@ impl AddMetadata { &self.name, &Producers::from_meta(self), &self.author, + &self.description, self.registry_metadata.as_ref(), input, ) diff --git a/crates/wasm-metadata/src/lib.rs b/crates/wasm-metadata/src/lib.rs index 497617fdb3..2c081ac3d2 100644 --- a/crates/wasm-metadata/src/lib.rs +++ b/crates/wasm-metadata/src/lib.rs @@ -5,7 +5,7 @@ pub use add_metadata::AddMetadata; pub use metadata::Metadata; pub use names::{ComponentNames, ModuleNames}; -pub use oci_annotations::Author; +pub use oci_annotations::{Author, Description}; pub use producers::{Producers, ProducersField}; pub use registry::{CustomLicense, Link, LinkType, RegistryMetadata}; diff --git a/crates/wasm-metadata/src/metadata.rs b/crates/wasm-metadata/src/metadata.rs index 2277455a36..fa76d5a09f 100644 --- a/crates/wasm-metadata/src/metadata.rs +++ b/crates/wasm-metadata/src/metadata.rs @@ -4,7 +4,7 @@ use std::fmt; use std::ops::Range; use wasmparser::{KnownCustom, Parser, Payload::*}; -use crate::{Author, ComponentNames, ModuleNames, Producers, RegistryMetadata}; +use crate::{Author, ComponentNames, Description, ModuleNames, Producers, RegistryMetadata}; /// A tree of the metadata found in a WebAssembly binary. #[derive(Debug, Serialize)] @@ -20,6 +20,8 @@ pub enum Metadata { registry_metadata: Option, /// The component's author section, if any. author: Option, + /// Human-readable description of the binary + description: Option, /// All child modules and components inside the component. children: Vec>, /// Byte range of the module in the parent binary @@ -35,6 +37,8 @@ pub enum Metadata { registry_metadata: Option, /// The component's author section, if any. author: Option, + /// Human-readable description of the binary + description: Option, /// Byte range of the module in the parent binary range: Range, }, @@ -117,6 +121,13 @@ impl Metadata { Metadata::Component { author, .. } => *author = Some(a), } } + KnownCustom::Unknown if c.name() == "description" => { + let a = Description::parse_custom_section(&c)?; + match metadata.last_mut().expect("non-empty metadata stack") { + Metadata::Module { description, .. } => *description = Some(a), + Metadata::Component { description, .. } => *description = Some(a), + } + } _ => {} }, _ => {} @@ -132,6 +143,7 @@ impl Metadata { name: None, producers: None, author: None, + description: None, registry_metadata: None, children: Vec::new(), range, @@ -143,6 +155,7 @@ impl Metadata { name: None, producers: None, author: None, + description: None, registry_metadata: None, range, } diff --git a/crates/wasm-metadata/src/oci_annotations/description.rs b/crates/wasm-metadata/src/oci_annotations/description.rs new file mode 100644 index 0000000000..43dfcfd010 --- /dev/null +++ b/crates/wasm-metadata/src/oci_annotations/description.rs @@ -0,0 +1,110 @@ +use std::borrow::Cow; +use std::fmt::{self, Display}; +use std::str::FromStr; + +use anyhow::{ensure, Error, Result}; +use serde::Serialize; +use wasm_encoder::{ComponentSection, CustomSection, Encode, Section}; +use wasmparser::CustomSectionReader; + +/// Human-readable description of the binary +#[derive(Debug, Clone, PartialEq)] +pub struct Description(CustomSection<'static>); + +impl Description { + /// Create a new instance of `Desrciption`. + pub fn new>>(s: S) -> Self { + Self(CustomSection { + name: "description".into(), + data: match s.into() { + Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()), + Cow::Owned(s) => Cow::Owned(s.into()), + }, + }) + } + + /// Parse an `description` custom section from a wasm binary. + pub(crate) fn parse_custom_section(reader: &CustomSectionReader<'_>) -> Result { + ensure!( + reader.name() == "description", + "The `description` custom section should have a name of 'description'" + ); + let data = String::from_utf8(reader.data().to_owned())?; + Ok(Self::new(data)) + } +} + +impl FromStr for Description { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self::new(s.to_owned())) + } +} + +impl Serialize for Description { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl Display for Description { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // NOTE: this will never panic since we always guarantee the data is + // encoded as utf8, even if we internally store it as [u8]. + let data = String::from_utf8(self.0.data.to_vec()).unwrap(); + write!(f, "{data}") + } +} + +impl ComponentSection for Description { + fn id(&self) -> u8 { + ComponentSection::id(&self.0) + } +} + +impl Section for Description { + fn id(&self) -> u8 { + Section::id(&self.0) + } +} + +impl Encode for Description { + fn encode(&self, sink: &mut Vec) { + self.0.encode(sink); + } +} + +#[cfg(test)] +mod test { + use super::*; + use wasm_encoder::Component; + use wasmparser::Payload; + + #[test] + fn roundtrip() { + let mut component = Component::new(); + component.section(&Description::new("Nori likes chicken")); + let component = component.finish(); + + let mut parsed = false; + for section in wasmparser::Parser::new(0).parse_all(&component) { + if let Payload::CustomSection(reader) = section.unwrap() { + let description = Description::parse_custom_section(&reader).unwrap(); + assert_eq!(description.to_string(), "Nori likes chicken"); + parsed = true; + } + } + assert!(parsed); + } + + #[test] + fn serialize() { + let description = Description::new("Chashu likes tuna"); + let json = serde_json::to_string(&description).unwrap(); + assert_eq!(r#""Chashu likes tuna""#, json); + } +} diff --git a/crates/wasm-metadata/src/oci_annotations/mod.rs b/crates/wasm-metadata/src/oci_annotations/mod.rs index 18aa04799b..ff7b8bd4b0 100644 --- a/crates/wasm-metadata/src/oci_annotations/mod.rs +++ b/crates/wasm-metadata/src/oci_annotations/mod.rs @@ -16,5 +16,7 @@ //! [OCI Annotations Spec]: https://specs.opencontainers.org/image-spec/annotations/ pub use author::Author; +pub use description::Description; mod author; +mod description; diff --git a/crates/wasm-metadata/src/producers.rs b/crates/wasm-metadata/src/producers.rs index 4ddac5e312..992850d10b 100644 --- a/crates/wasm-metadata/src/producers.rs +++ b/crates/wasm-metadata/src/producers.rs @@ -148,7 +148,7 @@ impl Producers { /// Merge into an existing wasm module. Rewrites the module with this producers section /// merged into its existing one, or adds this producers section if none is present. pub fn add_to_wasm(&self, input: &[u8]) -> Result> { - rewrite_wasm(&None, self, &None, None, input) + rewrite_wasm(&None, self, &None, &None, None, input) } pub(crate) fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result { diff --git a/crates/wasm-metadata/src/registry.rs b/crates/wasm-metadata/src/registry.rs index e52a349bef..53908ca7af 100644 --- a/crates/wasm-metadata/src/registry.rs +++ b/crates/wasm-metadata/src/registry.rs @@ -44,7 +44,7 @@ impl RegistryMetadata { /// Merge into an existing wasm module. Rewrites the module with this registry-metadata section /// overwriting its existing one, or adds this registry-metadata section if none is present. pub fn add_to_wasm(&self, input: &[u8]) -> Result> { - rewrite_wasm(&None, &Producers::empty(), &None, Some(&self), input) + rewrite_wasm(&None, &Producers::empty(), &None, &None, Some(&self), input) } /// Parse a Wasm binary and extract the `Registry` section, if there is any. diff --git a/crates/wasm-metadata/src/rewrite.rs b/crates/wasm-metadata/src/rewrite.rs index b1e8aa1424..45cbc535f2 100644 --- a/crates/wasm-metadata/src/rewrite.rs +++ b/crates/wasm-metadata/src/rewrite.rs @@ -1,4 +1,4 @@ -use crate::{Author, ComponentNames, ModuleNames, Producers, RegistryMetadata}; +use crate::{Author, ComponentNames, Description, ModuleNames, Producers, RegistryMetadata}; use anyhow::Result; use std::borrow::Cow; use std::mem; @@ -10,6 +10,7 @@ pub(crate) fn rewrite_wasm( add_name: &Option, add_producers: &Producers, add_author: &Option, + add_description: &Option, add_registry_metadata: Option<&RegistryMetadata>, input: &[u8], ) -> Result> { @@ -98,6 +99,13 @@ pub(crate) fn rewrite_wasm( continue; } } + KnownCustom::Unknown if c.name() == "description" => { + if add_description.is_none() { + let description = Description::parse_custom_section(c)?; + description.append_to(&mut output); + continue; + } + } _ => {} } } @@ -130,6 +138,9 @@ pub(crate) fn rewrite_wasm( if let Some(author) = add_author { author.append_to(&mut output); } + if let Some(description) = add_description { + description.append_to(&mut output); + } if add_registry_metadata.is_some() { let registry_metadata = wasm_encoder::CustomSection { name: Cow::Borrowed("registry-metadata"), diff --git a/crates/wasm-metadata/tests/component.rs b/crates/wasm-metadata/tests/component.rs index ae588d9b69..9871b1bb65 100644 --- a/crates/wasm-metadata/tests/component.rs +++ b/crates/wasm-metadata/tests/component.rs @@ -12,6 +12,7 @@ fn add_to_empty_component() { processed_by: vec![("baz".to_owned(), "1.0".to_owned())], sdk: vec![], author: Some(Author::new("Chashu Cat")), + description: Some(Description::new("Chashu likes tuna")), registry_metadata: Some(RegistryMetadata { authors: Some(vec!["foo".to_owned()]), description: Some("foo bar baz".to_owned()), @@ -44,6 +45,7 @@ fn add_to_empty_component() { producers, registry_metadata, author, + description, children, range, } => { @@ -60,6 +62,7 @@ fn add_to_empty_component() { ); assert_eq!(author.unwrap(), Author::new("Chashu Cat")); + assert_eq!(description.unwrap(), Description::new("Chashu likes tuna")); let registry_metadata = registry_metadata.unwrap(); @@ -103,7 +106,7 @@ fn add_to_empty_component() { ); assert_eq!(range.start, 0); - assert_eq!(range.end, 454); + assert_eq!(range.end, 485); } _ => panic!("metadata should be component"), } @@ -119,6 +122,7 @@ fn add_to_nested_component() { processed_by: vec![("baz".to_owned(), "1.0".to_owned())], sdk: vec![], author: Some(Author::new("Chashu Cat")), + description: Some(Description::new("Chashu likes tuna")), registry_metadata: Some(RegistryMetadata { authors: Some(vec!["Foo".to_owned()]), ..Default::default() @@ -167,6 +171,7 @@ fn add_to_nested_component() { author, registry_metadata, range, + description, } => { assert_eq!(name, &Some("foo".to_owned())); let producers = producers.as_ref().expect("some producers"); @@ -180,6 +185,7 @@ fn add_to_nested_component() { ); assert_eq!(author, &Some(Author::new("Chashu Cat"))); + assert_eq!(description, &Some(Description::new("Chashu likes tuna"))); let registry_metadata = registry_metadata.as_ref().unwrap(); assert_eq!( @@ -188,7 +194,7 @@ fn add_to_nested_component() { ); assert_eq!(range.start, 11); - assert_eq!(range.end, 143); + assert_eq!(range.end, 174); } _ => panic!("child is a module"), } diff --git a/crates/wasm-metadata/tests/module.rs b/crates/wasm-metadata/tests/module.rs index c59484a020..0dba3fb42e 100644 --- a/crates/wasm-metadata/tests/module.rs +++ b/crates/wasm-metadata/tests/module.rs @@ -12,6 +12,7 @@ fn add_to_empty_module() { processed_by: vec![("baz".to_owned(), "1.0".to_owned())], sdk: vec![], author: Some(Author::new("Chashu Cat")), + description: Some(Description::new("Chashu likes tuna")), registry_metadata: Some(RegistryMetadata { authors: Some(vec!["foo".to_owned()]), description: Some("foo bar baz".to_owned()), @@ -43,6 +44,7 @@ fn add_to_empty_module() { name, producers, author, + description, registry_metadata, range, } => { @@ -58,6 +60,7 @@ fn add_to_empty_module() { ); assert_eq!(author.unwrap(), Author::new("Chashu Cat")); + assert_eq!(description.unwrap(), Description::new("Chashu likes tuna")); let registry_metadata = registry_metadata.unwrap(); @@ -101,7 +104,7 @@ fn add_to_empty_module() { ); assert_eq!(range.start, 0); - assert_eq!(range.end, 444); + assert_eq!(range.end, 475); } _ => panic!("metadata should be module"), }