From 1bcecbccdd60d5f4302935cafa8d0a2f8168c572 Mon Sep 17 00:00:00 2001 From: Yosh <2467194+yoshuawuyts@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:35:40 +0100 Subject: [PATCH] [wasm-metadata] add support for Homepage custom section (#1945) --- crates/wasm-metadata/src/add_metadata.rs | 7 +- crates/wasm-metadata/src/lib.rs | 2 +- crates/wasm-metadata/src/metadata.rs | 4 +- .../src/oci_annotations/homepage.rs | 118 ++++++++++++++++++ .../wasm-metadata/src/oci_annotations/mod.rs | 2 + crates/wasm-metadata/src/payload.rs | 11 +- crates/wasm-metadata/src/producers.rs | 2 +- crates/wasm-metadata/src/rewrite.rs | 15 ++- crates/wasm-metadata/tests/component.rs | 19 ++- crates/wasm-metadata/tests/module.rs | 8 +- 10 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 crates/wasm-metadata/src/oci_annotations/homepage.rs diff --git a/crates/wasm-metadata/src/add_metadata.rs b/crates/wasm-metadata/src/add_metadata.rs index c30e273d0a..089ee08db9 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, Description, Licenses, Producers, Source}; +use crate::{rewrite_wasm, Author, Description, Homepage, Licenses, Producers, Source}; use anyhow::Result; @@ -41,6 +41,10 @@ pub struct AddMetadata { /// URL to get source code for building the image #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))] pub source: Option, + + /// URL to find more information on the binary + #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))] + pub homepage: Option, } #[cfg(feature = "clap")] @@ -62,6 +66,7 @@ impl AddMetadata { &self.description, &self.licenses, &self.source, + &self.homepage, input, ) } diff --git a/crates/wasm-metadata/src/lib.rs b/crates/wasm-metadata/src/lib.rs index 287fd6e3b7..125c66a223 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, Description, Licenses, Source}; +pub use oci_annotations::{Author, Description, Homepage, Licenses, Source}; pub use payload::Payload; pub use producers::{Producers, ProducersField}; diff --git a/crates/wasm-metadata/src/metadata.rs b/crates/wasm-metadata/src/metadata.rs index fcac7ff6b7..ee9c31148b 100644 --- a/crates/wasm-metadata/src/metadata.rs +++ b/crates/wasm-metadata/src/metadata.rs @@ -1,7 +1,7 @@ use serde_derive::Serialize; use std::ops::Range; -use crate::{Author, Description, Licenses, Producers, Source}; +use crate::{Author, Description, Homepage, Licenses, Producers, Source}; /// Metadata associated with a Wasm Component or Module #[derive(Debug, Serialize, Default)] @@ -19,6 +19,8 @@ pub struct Metadata { pub licenses: Option, /// URL to get source code for building the image pub source: Option, + /// URL to find more information on the binary + pub homepage: Option, /// Byte range of the module in the parent binary pub range: Range, } diff --git a/crates/wasm-metadata/src/oci_annotations/homepage.rs b/crates/wasm-metadata/src/oci_annotations/homepage.rs new file mode 100644 index 0000000000..ab26ea2794 --- /dev/null +++ b/crates/wasm-metadata/src/oci_annotations/homepage.rs @@ -0,0 +1,118 @@ +use std::borrow::Cow; +use std::fmt::{self, Display}; +use std::str::FromStr; + +use anyhow::{ensure, Error, Result}; +use serde::Serialize; +use url::Url; +use wasm_encoder::{ComponentSection, CustomSection, Encode, Section}; +use wasmparser::CustomSectionReader; + +/// URL to find more information on the binary +#[derive(Debug, Clone, PartialEq)] +pub struct Homepage(CustomSection<'static>); + +impl Homepage { + /// Create a new instance of `Homepage`. + pub fn new(s: &str) -> Result { + Ok(Url::parse(s)?.into()) + } + + /// Parse a `homepage` custom section from a wasm binary. + pub(crate) fn parse_custom_section(reader: &CustomSectionReader<'_>) -> Result { + ensure!( + reader.name() == "homepage", + "The `homepage` custom section should have a name of 'homepage'" + ); + let data = String::from_utf8(reader.data().to_owned())?; + Self::new(&data) + } +} + +impl FromStr for Homepage { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +impl From for Homepage { + fn from(expression: Url) -> Self { + Self(CustomSection { + name: "homepage".into(), + data: Cow::Owned(expression.to_string().into_bytes()), + }) + } +} + +impl Serialize for Homepage { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl Display for Homepage { + 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 Homepage { + fn id(&self) -> u8 { + ComponentSection::id(&self.0) + } +} + +impl Section for Homepage { + fn id(&self) -> u8 { + Section::id(&self.0) + } +} + +impl Encode for Homepage { + 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(&Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap()); + 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 = Homepage::parse_custom_section(&reader).unwrap(); + assert_eq!( + description.to_string(), + "https://github.com/bytecodealliance/wasm-tools" + ); + parsed = true; + } + } + assert!(parsed); + } + + #[test] + fn serialize() { + let description = Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap(); + let json = serde_json::to_string(&description).unwrap(); + assert_eq!(r#""https://github.com/bytecodealliance/wasm-tools""#, json); + } +} diff --git a/crates/wasm-metadata/src/oci_annotations/mod.rs b/crates/wasm-metadata/src/oci_annotations/mod.rs index bbd01aef10..760c3ed5ca 100644 --- a/crates/wasm-metadata/src/oci_annotations/mod.rs +++ b/crates/wasm-metadata/src/oci_annotations/mod.rs @@ -17,10 +17,12 @@ pub use author::Author; pub use description::Description; +pub use homepage::Homepage; pub use licenses::Licenses; pub use source::Source; mod author; mod description; +mod homepage; mod licenses; mod source; diff --git a/crates/wasm-metadata/src/payload.rs b/crates/wasm-metadata/src/payload.rs index 5560373d8c..6acb22aaae 100644 --- a/crates/wasm-metadata/src/payload.rs +++ b/crates/wasm-metadata/src/payload.rs @@ -6,7 +6,8 @@ use serde_derive::Serialize; use wasmparser::{KnownCustom, Parser, Payload::*}; use crate::{ - Author, ComponentNames, Description, Licenses, Metadata, ModuleNames, Producers, Source, + Author, ComponentNames, Description, Homepage, Licenses, Metadata, ModuleNames, Producers, + Source, }; /// Data representing either a Wasm Component or module @@ -125,6 +126,14 @@ impl Payload { .metadata_mut(); *source = Some(a); } + KnownCustom::Unknown if c.name() == "homepage" => { + let a = Homepage::parse_custom_section(&c)?; + let Metadata { homepage, .. } = output + .last_mut() + .expect("non-empty metadata stack") + .metadata_mut(); + *homepage = Some(a); + } _ => {} }, _ => {} diff --git a/crates/wasm-metadata/src/producers.rs b/crates/wasm-metadata/src/producers.rs index fca295ad99..104e7498df 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, &None, &None, input) + rewrite_wasm(&None, self, &None, &None, &None, &None, &None, input) } pub(crate) fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result { diff --git a/crates/wasm-metadata/src/rewrite.rs b/crates/wasm-metadata/src/rewrite.rs index 43ea44f9b8..7611ebb032 100644 --- a/crates/wasm-metadata/src/rewrite.rs +++ b/crates/wasm-metadata/src/rewrite.rs @@ -1,4 +1,6 @@ -use crate::{Author, ComponentNames, Description, Licenses, ModuleNames, Producers, Source}; +use crate::{ + Author, ComponentNames, Description, Homepage, Licenses, ModuleNames, Producers, Source, +}; use anyhow::Result; use std::mem; use wasm_encoder::ComponentSection as _; @@ -12,6 +14,7 @@ pub(crate) fn rewrite_wasm( add_description: &Option, add_licenses: &Option, add_source: &Option, + add_homepage: &Option, input: &[u8], ) -> Result> { let mut producers_found = false; @@ -106,6 +109,13 @@ pub(crate) fn rewrite_wasm( continue; } } + KnownCustom::Unknown if c.name() == "homepage" => { + if add_source.is_none() { + let homepage = Homepage::parse_custom_section(c)?; + homepage.append_to(&mut output); + continue; + } + } _ => {} } } @@ -147,5 +157,8 @@ pub(crate) fn rewrite_wasm( if let Some(source) = add_source { source.append_to(&mut output); } + if let Some(homepage) = add_homepage { + homepage.append_to(&mut output); + } Ok(output) } diff --git a/crates/wasm-metadata/tests/component.rs b/crates/wasm-metadata/tests/component.rs index 6eba7d465c..db850d5632 100644 --- a/crates/wasm-metadata/tests/component.rs +++ b/crates/wasm-metadata/tests/component.rs @@ -17,6 +17,7 @@ fn add_to_empty_component() { Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap(), ), source: Some(Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap()), + homepage: Some(Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap()), }; let component = add.to_wasm(&component).unwrap(); @@ -32,6 +33,7 @@ fn add_to_empty_component() { licenses, source, range, + homepage, }, } => { assert!(children.is_empty()); @@ -56,9 +58,13 @@ fn add_to_empty_component() { source.unwrap(), Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap(), ); + assert_eq!( + homepage.unwrap(), + Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap(), + ); assert_eq!(range.start, 0); - assert_eq!(range.end, 251); + assert_eq!(range.end, 308); } _ => panic!("metadata should be component"), } @@ -79,6 +85,7 @@ fn add_to_nested_component() { Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap(), ), source: Some(Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap()), + homepage: Some(Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap()), }; let module = add.to_wasm(&module).unwrap(); @@ -124,6 +131,7 @@ fn add_to_nested_component() { source, range, description, + homepage, }) => { assert_eq!(name, &Some("foo".to_owned())); let producers = producers.as_ref().expect("some producers"); @@ -151,9 +159,16 @@ fn add_to_nested_component() { Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap() ), ); + assert_eq!( + homepage, + &Some( + Homepage::new("https://github.com/bytecodealliance/wasm-tools") + .unwrap() + ), + ); assert_eq!(range.start, 11); - assert_eq!(range.end, 252); + assert_eq!(range.end, 309); } _ => panic!("child is a module"), } diff --git a/crates/wasm-metadata/tests/module.rs b/crates/wasm-metadata/tests/module.rs index 91c064d3d2..662d801f0b 100644 --- a/crates/wasm-metadata/tests/module.rs +++ b/crates/wasm-metadata/tests/module.rs @@ -17,6 +17,7 @@ fn add_to_empty_module() { Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap(), ), source: Some(Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap()), + homepage: Some(Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap()), }; let module = add.to_wasm(&module).unwrap(); @@ -29,6 +30,7 @@ fn add_to_empty_module() { source, range, description, + homepage, }) => { assert_eq!(name, Some("foo".to_owned())); let producers = producers.expect("some producers"); @@ -51,9 +53,13 @@ fn add_to_empty_module() { source.unwrap(), Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap(), ); + assert_eq!( + homepage.unwrap(), + Homepage::new("https://github.com/bytecodealliance/wasm-tools").unwrap(), + ); assert_eq!(range.start, 0); - assert_eq!(range.end, 241); + assert_eq!(range.end, 298); } _ => panic!("metadata should be module"), }