Repository: bytecodealliance/wasm-tools
Pull request #1936 (2024-12-05 02:34:57)

[wasm-metadata] add OCI description support
Follow up to #1925, makes progress towards #1922, adds support for descriptions. Thanks!


Patch:
diff --git a/crates/wasm-metadata/src/add_metadata.rs b/crates/wasm-metadata/src/add_metadata.rs
index cb185694..fe04adbb 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<Author>,
 
+    /// A human-readable description of the binary
+    #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
+    pub description: Option<Description>,
+
     /// 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<RegistryMetadata>,
@@ -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 497617fd..2c081ac3 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 2277455a..fa76d5a0 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<RegistryMetadata>,
         /// The component's author section, if any.
         author: Option<Author>,
+        /// Human-readable description of the binary
+        description: Option<Description>,
         /// All child modules and components inside the component.
         children: Vec<Box<Metadata>>,
         /// Byte range of the module in the parent binary
@@ -35,6 +37,8 @@ pub enum Metadata {
         registry_metadata: Option<RegistryMetadata>,
         /// The component's author section, if any.
         author: Option<Author>,
+        /// Human-readable description of the binary
+        description: Option<Description>,
         /// Byte range of the module in the parent binary
         range: Range<usize>,
     },
@@ -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 00000000..43dfcfd0
--- /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: Into<Cow<'static, str>>>(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<Self> {
+        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<Self, Self::Err> {
+        Ok(Self::new(s.to_owned()))
+    }
+}
+
+impl Serialize for Description {
+    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+    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<u8>) {
+        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 18aa0479..ff7b8bd4 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 4ddac5e3..992850d1 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<Vec<u8>> {
-        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 e52a349b..53908ca7 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<Vec<u8>> {
-        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 b1e8aa14..45cbc535 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<String>,
     add_producers: &Producers,
     add_author: &Option<Author>,
+    add_description: &Option<Description>,
     add_registry_metadata: Option<&RegistryMetadata>,
     input: &[u8],
 ) -> Result<Vec<u8>> {
@@ -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"),


Test patch:
diff --git a/crates/wasm-metadata/tests/component.rs b/crates/wasm-metadata/tests/component.rs
index ae588d9b..9871b1bb 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 c59484a0..0dba3fb4 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"),
     }