diff --git a/Cargo.toml b/Cargo.toml index 9a47941621..1d787c7ff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,13 +38,15 @@ num_cpus = "1.13" rand = { version = "0.8.4", features = ["small_rng"] } rayon = "1.3" serde = { version = "1.0.137", features = ["derive"] } -wasmtime = { version = "3.0.0", default-features = false, features = ['cranelift'] } +wasmtime = { version = "3.0.0", default-features = false, features = [ + 'cranelift', +] } url = "2.0.0" pretty_assertions = "1.3.0" semver = "1.0.0" -wasm-encoder = { version = "0.29.0", path = "crates/wasm-encoder"} -wasm-compose = { version = "0.2.17", path = "crates/wasm-compose"} +wasm-encoder = { version = "0.29.0", path = "crates/wasm-encoder" } +wasm-compose = { version = "0.2.17", path = "crates/wasm-compose" } wasm-metadata = { version = "0.8.0", path = "crates/wasm-metadata" } wasm-mutate = { version = "0.2.27", path = "crates/wasm-mutate" } wasm-shrink = { version = "0.1.28", path = "crates/wasm-shrink" } @@ -99,7 +101,9 @@ rustc-demangle = { version = "0.1.21", optional = true } cpp_demangle = { version = "0.4.0", optional = true } # Dependencies of `component` -wit-component = { workspace = true, optional = true, features = ['dummy-module'] } +wit-component = { workspace = true, optional = true, features = [ + 'dummy-module', +] } wit-parser = { workspace = true, optional = true } wast = { workspace = true, optional = true } @@ -160,7 +164,13 @@ objdump = ['dep:wasmparser'] strip = ['wasm-encoder', 'dep:wasmparser', 'regex'] compose = ['wasm-compose'] demangle = ['rustc-demangle', 'cpp_demangle', 'dep:wasmparser', 'wasm-encoder'] -component = ['wit-component', 'wit-parser', 'wast', 'wasm-encoder', 'dep:wasmparser'] +component = [ + 'wit-component', + 'wit-parser', + 'wast', + 'wasm-encoder', + 'dep:wasmparser', +] metadata = ['dep:wasmparser', 'wasm-metadata', 'serde_json'] wit-smith = ['dep:wit-smith', 'arbitrary'] addr2line = ['dep:addr2line', 'dep:gimli', 'dep:wasmparser'] diff --git a/crates/wasm-metadata/Cargo.toml b/crates/wasm-metadata/Cargo.toml index 76bf9859a6..1432087ea9 100644 --- a/crates/wasm-metadata/Cargo.toml +++ b/crates/wasm-metadata/Cargo.toml @@ -13,6 +13,8 @@ wasmparser = { workspace = true } wasm-encoder = { workspace = true } indexmap = { workspace = true, features = ["serde"] } serde = { workspace = true } +serde_json = { version = "1" } +spdx = "0.10.1" [dev-dependencies] wat = { workspace = true } diff --git a/crates/wasm-metadata/src/lib.rs b/crates/wasm-metadata/src/lib.rs index 749554ca14..1fdfa03963 100644 --- a/crates/wasm-metadata/src/lib.rs +++ b/crates/wasm-metadata/src/lib.rs @@ -1,7 +1,10 @@ use anyhow::Result; use indexmap::{map::Entry, IndexMap}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use spdx::Expression; +use std::borrow::Cow; use std::fmt; +use std::fmt::Display; use std::mem; use std::ops::Range; use wasm_encoder::{ComponentSection as _, ComponentSectionId, Encode, Section}; @@ -141,7 +144,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, input) + rewrite_wasm(&None, self, None, input) } fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result { @@ -202,6 +205,10 @@ pub struct AddMetadata { /// Add an SDK and its version to the producers section #[cfg_attr(feature="clap", clap(long, value_parser = parse_key_value, value_name="NAME=VERSION"))] pub sdk: Vec<(String, String)>, + + /// 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, } #[cfg(feature = "clap")] @@ -211,18 +218,33 @@ fn parse_key_value(s: &str) -> Result<(String, String)> { .ok_or_else(|| anyhow::anyhow!("expected KEY=VALUE")) } +#[cfg(feature = "clap")] +fn parse_registry_metadata_value(s: &str) -> Result { + let contents = std::fs::read(s)?; + + let registry_metadata = RegistryMetadata::from_bytes(&contents, 0)?; + + Ok(registry_metadata) +} + impl AddMetadata { /// Process a WebAssembly binary. Supports both core WebAssembly modules, and WebAssembly /// components. The module and component will have, at very least, an empty name and producers /// section created. pub fn to_wasm(&self, input: &[u8]) -> Result> { - rewrite_wasm(&self.name, &Producers::from_meta(self), input) + rewrite_wasm( + &self.name, + &Producers::from_meta(self), + self.registry_metadata.as_ref(), + input, + ) } } fn rewrite_wasm( add_name: &Option, add_producers: &Producers, + add_registry_metadata: Option<&RegistryMetadata>, input: &[u8], ) -> Result> { let mut producers_found = false; @@ -291,6 +313,19 @@ fn rewrite_wasm( names.section()?.as_custom().append_to(&mut output); } + CustomSection(c) if c.name() == "registry-metadata" && stack.len() == 0 => { + // Pass section through if a new registry metadata isn't provided, otherwise ignore and overwrite with new + if add_registry_metadata.is_none() { + let registry: RegistryMetadata = RegistryMetadata::from_bytes(&c.data(), 0)?; + + let registry_metadata = wasm_encoder::CustomSection { + name: Cow::Borrowed("registry-metadata"), + data: Cow::Owned(serde_json::to_vec(®istry)?), + }; + registry_metadata.append_to(&mut output); + } + } + // All other sections get passed through unmodified: _ => { if let Some((id, range)) = payload.as_section() { @@ -319,6 +354,13 @@ fn rewrite_wasm( // Encode into output: producers.section().append_to(&mut output); } + if add_registry_metadata.is_some() { + let registry_metadata = wasm_encoder::CustomSection { + name: Cow::Borrowed("registry-metadata"), + data: Cow::Owned(serde_json::to_vec(&add_registry_metadata)?), + }; + registry_metadata.append_to(&mut output); + } Ok(output) } @@ -332,6 +374,8 @@ pub enum Metadata { name: Option, /// The component's producers section, if any. producers: Option, + /// The component's registry metadata section, if any. + registry_metadata: Option, /// All child modules and components inside the component. children: Vec>, /// Byte range of the module in the parent binary @@ -343,6 +387,8 @@ pub enum Metadata { name: Option, /// The module's producers section, if any. producers: Option, + /// The module's registry metadata section, if any. + registry_metadata: Option, /// Byte range of the module in the parent binary range: Range, }, @@ -406,6 +452,13 @@ impl Metadata { .expect("non-empty metadata stack") .set_producers(producers); } + CustomSection(c) if c.name() == "registry-metadata" => { + let registry: RegistryMetadata = RegistryMetadata::from_bytes(&c.data(), 0)?; + metadata + .last_mut() + .expect("non-empty metadata stack") + .set_registry_metadata(registry); + } _ => {} } @@ -419,6 +472,7 @@ impl Metadata { Metadata::Component { name: None, producers: None, + registry_metadata: None, children: Vec::new(), range, } @@ -428,6 +482,7 @@ impl Metadata { Metadata::Module { name: None, producers: None, + registry_metadata: None, range, } } @@ -443,6 +498,16 @@ impl Metadata { Metadata::Component { producers, .. } => *producers = Some(p), } } + fn set_registry_metadata(&mut self, r: RegistryMetadata) { + match self { + Metadata::Module { + registry_metadata, .. + } => *registry_metadata = Some(r), + Metadata::Component { + registry_metadata, .. + } => *registry_metadata = Some(r), + } + } fn push_child(&mut self, child: Self) { match self { Metadata::Module { .. } => panic!("module shouldnt have children"), @@ -454,7 +519,10 @@ impl Metadata { let spaces = std::iter::repeat(" ").take(indent).collect::(); match self { Metadata::Module { - name, producers, .. + name, + producers, + registry_metadata, + .. } => { if let Some(name) = name { writeln!(f, "{spaces}module {name}:")?; @@ -464,11 +532,15 @@ impl Metadata { if let Some(producers) = producers { producers.display(f, indent + 4)?; } + if let Some(registry_metadata) = registry_metadata { + registry_metadata.display(f, indent + 4)?; + } Ok(()) } Metadata::Component { name, producers, + registry_metadata, children, .. } => { @@ -480,6 +552,9 @@ impl Metadata { if let Some(producers) = producers { producers.display(f, indent + 4)?; } + if let Some(registry_metadata) = registry_metadata { + registry_metadata.display(f, indent + 4)?; + } for c in children { c.display(f, indent + 4)?; } @@ -672,8 +747,344 @@ fn indirect_name_map( Ok(out) } +#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq)] +pub struct RegistryMetadata { + /// List of authors who has created this package. + #[serde(skip_serializing_if = "Option::is_none")] + authors: Option>, + + /// Package description in markdown format. + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + + /// SPDX License Expression + /// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ + /// SPDX License List: https://spdx.org/licenses/ + #[serde(skip_serializing_if = "Option::is_none")] + license: Option, + + /// A list of custom licenses that should be referenced to from the license expression. + /// https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/ + #[serde(skip_serializing_if = "Option::is_none")] + custom_licenses: Option>, + + /// A list of links that can contain predefined link types or custom links for use with tooling or registries. + #[serde(skip_serializing_if = "Option::is_none")] + links: Option>, + + /// A list of categories that a package should be listed under when uploaded to a registry. + #[serde(skip_serializing_if = "Option::is_none")] + categories: Option>, +} + +const LICENSE_REF: &str = "LicenseRef-"; + +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(), Some(&self), input) + } + + pub fn from_wasm(bytes: &[u8]) -> Result> { + let mut depth = 0; + for payload in Parser::new(0).parse_all(bytes) { + let payload = payload?; + use wasmparser::Payload::*; + match payload { + ModuleSection { .. } | ComponentSection { .. } => depth += 1, + End { .. } => depth -= 1, + CustomSection(c) if c.name() == "registry-metadata" && depth == 0 => { + let registry = RegistryMetadata::from_bytes(&c.data(), 0)?; + return Ok(Some(registry)); + } + _ => {} + } + } + Ok(None) + } + + /// Gets the registry-matadata from a slice of bytes + pub fn from_bytes(bytes: &[u8], offset: usize) -> Result { + let registry: RegistryMetadata = serde_json::from_slice(&bytes[offset..])?; + return Ok(registry); + } + + pub fn validate(&self) -> Result<()> { + fn validate_expression(expression: &str) -> Result> { + let expression = Expression::parse(expression)?; + + let mut licenses = Vec::new(); + + for license in expression.iter() { + match license { + spdx::expression::ExprNode::Op(_) => continue, + spdx::expression::ExprNode::Req(req) => { + if let spdx::LicenseItem::Spdx { .. } = req.req.license { + // Continue if it's a license that exists on the Spdx license list + continue; + } + + let license_id = req.req.to_string(); + + if license_id.starts_with(LICENSE_REF) { + // Strip "LicenseRef-", convert to lowercase and then append + licenses.push(license_id[LICENSE_REF.len()..].to_lowercase()); + } + } + } + } + + Ok(licenses) + } + + match (&self.license, &self.custom_licenses) { + (None, Some(custom_licenses)) => { + let ids = custom_licenses + .iter() + .map(|license| license.id.clone()) + .collect::>() + .join(", "); + + return Err(anyhow::anyhow!( + "{ids} are defined but nevered referenced in license expression" + )); + } + (Some(license), Some(custom_licenses)) => { + let licenses = validate_expression(license.as_str())?; + + if !licenses.is_empty() { + for license in &licenses { + let mut match_found = false; + for custom_license in custom_licenses { + // Ignore license id casing + if custom_license.id.to_lowercase() == *license { + match_found = true; + } + } + + if !match_found { + return Err(anyhow::anyhow!( + "No matching reference for license '{license}' was defined" + )); + } + } + } + } + (Some(license), None) => { + let licenses = validate_expression(license.as_str())?; + + if !licenses.is_empty() { + return Err(anyhow::anyhow!( + "Reference to custom license exists but no custom license was given" + )); + } + } + (None, None) => {} + } + + Ok(()) + } + + /// Get authors + pub fn get_authors(&self) -> Option<&Vec> { + self.authors.as_ref() + } + + /// Set authors + pub fn set_authors(&mut self, authors: Option>) { + self.authors = authors; + } + + /// Get description + pub fn get_description(&self) -> Option<&String> { + self.description.as_ref() + } + + /// Set description + pub fn set_description(&mut self, description: Option) { + self.description = description; + } + + /// Get license + pub fn get_license(&self) -> Option<&String> { + self.license.as_ref() + } + + /// Set license + pub fn set_license(&mut self, license: Option) { + self.license = license; + } + + /// Get custom_licenses + pub fn get_custom_licenses(&self) -> Option<&Vec> { + self.custom_licenses.as_ref() + } + + /// Set custom_licenses + pub fn set_custom_licenses(&mut self, custom_licenses: Option>) { + self.custom_licenses = custom_licenses; + } + + /// Get links + pub fn get_links(&self) -> Option<&Vec> { + self.links.as_ref() + } + + /// Set links + pub fn set_links(&mut self, links: Option>) { + self.links = links; + } + + /// Get categories + pub fn get_categories(&self) -> Option<&Vec> { + self.categories.as_ref() + } + + /// Set categories + pub fn set_categories(&mut self, categories: Option>) { + self.categories = categories; + } + + fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result { + let spaces = std::iter::repeat(" ").take(indent).collect::(); + + if let Some(authors) = &self.authors { + writeln!(f, "{spaces}authors:")?; + for author in authors { + writeln!(f, "{spaces} {author}")?; + } + } + + if let Some(license) = &self.license { + writeln!(f, "{spaces}license:")?; + writeln!(f, "{spaces} {license}")?; + } + + if let Some(links) = &self.links { + writeln!(f, "{spaces}links:")?; + for link in links { + writeln!(f, "{spaces} {link}")?; + } + } + + if let Some(categories) = &self.categories { + writeln!(f, "{spaces}categories:")?; + for category in categories { + writeln!(f, "{spaces} {category}")?; + } + } + + if let Some(description) = &self.description { + writeln!(f, "{spaces}description:")?; + writeln!(f, "{spaces} {description}")?; + } + + if let Some(custom_licenses) = &self.custom_licenses { + writeln!(f, "{spaces}custom_licenses:")?; + for license in custom_licenses { + license.display(f, indent + 4)?; + } + } + + Ok(()) + } +} + +impl Display for RegistryMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct Link { + pub ty: LinkType, + pub value: String, +} + +impl Display for Link { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.ty, self.value) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum LinkType { + Documentation, + Homepage, + Repository, + Funding, + #[serde(untagged)] + Custom(String), +} + +impl Display for LinkType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + LinkType::Documentation => "Documentation", + LinkType::Homepage => "Homepage", + LinkType::Repository => "Repository", + LinkType::Funding => "Funding", + LinkType::Custom(s) => s.as_str(), + }; + + write!(f, "{s}") + } +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)] +pub struct CustomLicense { + /// License Identifier + /// Provides a locally unique identifier to refer to licenses that are not found on the SPDX License List. + /// https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#101-license-identifier-field + pub id: String, + + /// License Name + /// Provide a common name of the license that is not on the SPDX list. + /// https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#103-license-name-field + pub name: String, + + /// Extracted Text + /// Provides a copy of the actual text of the license reference extracted from the package or file that is associated with the License Identifier to aid in future analysis. + /// https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#102-extracted-text-field + pub text: String, + + /// License Cross Reference + /// Provides a pointer to the official source of a license that is not included in the SPDX License List, that is referenced by the License Identifier. + /// https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#104-license-cross-reference-field + #[serde(skip_serializing_if = "Option::is_none")] + pub reference: Option, +} + +impl CustomLicense { + fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result { + let spaces = std::iter::repeat(" ").take(indent).collect::(); + + writeln!(f, "{spaces}{}:", self.id)?; + writeln!(f, "{spaces} name: {}", self.name)?; + + if let Some(reference) = &self.reference { + writeln!(f, "{spaces} reference: {reference}")?; + } + + writeln!(f, "{spaces} text:")?; + writeln!(f, "{spaces} {}", self.text)?; + + Ok(()) + } +} + +impl Display for CustomLicense { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + #[cfg(test)] mod test { + use std::vec; + use super::*; #[test] fn add_to_empty_module() { @@ -684,6 +1095,28 @@ mod test { language: vec!["bar".to_owned()], processed_by: vec![("baz".to_owned(), "1.0".to_owned())], sdk: vec![], + registry_metadata: Some(RegistryMetadata { + authors: Some(vec!["foo".to_owned()]), + description: Some("foo bar baz".to_owned()), + license: Some("MIT OR LicenseRef-FOO".to_owned()), + custom_licenses: Some(vec![CustomLicense { + id: "FOO".to_owned(), + name: "Foo".to_owned(), + text: "Foo License".to_owned(), + reference: Some("https://exaple.com/license/foo".to_owned()), + }]), + links: Some(vec![ + Link { + ty: LinkType::Custom("CustomFoo".to_owned()), + value: "https://example.com/custom".to_owned(), + }, + Link { + ty: LinkType::Homepage, + value: "https://example.com".to_owned(), + }, + ]), + categories: Some(vec!["Tools".to_owned()]), + }), }; let module = add.to_wasm(&module).unwrap(); @@ -692,6 +1125,7 @@ mod test { Metadata::Module { name, producers, + registry_metadata, range, } => { assert_eq!(name, Some("foo".to_owned())); @@ -701,8 +1135,50 @@ mod test { producers.get("processed-by").unwrap().get("baz").unwrap(), "1.0" ); + + let registry_metadata = registry_metadata.unwrap(); + + assert!(registry_metadata.validate().is_ok()); + + assert_eq!(registry_metadata.authors.unwrap(), vec!["foo".to_owned()]); + assert_eq!( + registry_metadata.description.unwrap(), + "foo bar baz".to_owned() + ); + + assert_eq!( + registry_metadata.license.unwrap(), + "MIT OR LicenseRef-FOO".to_owned() + ); + assert_eq!( + registry_metadata.custom_licenses.unwrap(), + vec![CustomLicense { + id: "FOO".to_owned(), + name: "Foo".to_owned(), + text: "Foo License".to_owned(), + reference: Some("https://exaple.com/license/foo".to_owned()), + }] + ); + assert_eq!( + registry_metadata.links.unwrap(), + vec![ + Link { + ty: LinkType::Custom("CustomFoo".to_owned()), + value: "https://example.com/custom".to_owned(), + }, + Link { + ty: LinkType::Homepage, + value: "https://example.com".to_owned(), + }, + ] + ); + assert_eq!( + registry_metadata.categories.unwrap(), + vec!["Tools".to_owned()] + ); + assert_eq!(range.start, 0); - assert_eq!(range.end, 71); + assert_eq!(range.end, 422); } _ => panic!("metadata should be module"), } @@ -717,6 +1193,28 @@ mod test { language: vec!["bar".to_owned()], processed_by: vec![("baz".to_owned(), "1.0".to_owned())], sdk: vec![], + registry_metadata: Some(RegistryMetadata { + authors: Some(vec!["foo".to_owned()]), + description: Some("foo bar baz".to_owned()), + license: Some("MIT OR LicenseRef-FOO".to_owned()), + custom_licenses: Some(vec![CustomLicense { + id: "FOO".to_owned(), + name: "Foo".to_owned(), + text: "Foo License".to_owned(), + reference: Some("https://exaple.com/license/foo".to_owned()), + }]), + links: Some(vec![ + Link { + ty: LinkType::Custom("CustomFoo".to_owned()), + value: "https://example.com/custom".to_owned(), + }, + Link { + ty: LinkType::Homepage, + value: "https://example.com".to_owned(), + }, + ]), + categories: Some(vec!["Tools".to_owned()]), + }), }; let component = add.to_wasm(&component).unwrap(); @@ -725,6 +1223,7 @@ mod test { Metadata::Component { name, producers, + registry_metadata, children, range, } => { @@ -736,8 +1235,50 @@ mod test { producers.get("processed-by").unwrap().get("baz").unwrap(), "1.0" ); + + let registry_metadata = registry_metadata.unwrap(); + + assert!(registry_metadata.validate().is_ok()); + + assert_eq!(registry_metadata.authors.unwrap(), vec!["foo".to_owned()]); + assert_eq!( + registry_metadata.description.unwrap(), + "foo bar baz".to_owned() + ); + + assert_eq!( + registry_metadata.license.unwrap(), + "MIT OR LicenseRef-FOO".to_owned() + ); + assert_eq!( + registry_metadata.custom_licenses.unwrap(), + vec![CustomLicense { + id: "FOO".to_owned(), + name: "Foo".to_owned(), + text: "Foo License".to_owned(), + reference: Some("https://exaple.com/license/foo".to_owned()), + }] + ); + assert_eq!( + registry_metadata.links.unwrap(), + vec![ + Link { + ty: LinkType::Custom("CustomFoo".to_owned()), + value: "https://example.com/custom".to_owned(), + }, + Link { + ty: LinkType::Homepage, + value: "https://example.com".to_owned(), + }, + ] + ); + assert_eq!( + registry_metadata.categories.unwrap(), + vec!["Tools".to_owned()] + ); + assert_eq!(range.start, 0); - assert_eq!(range.end, 81); + assert_eq!(range.end, 432); } _ => panic!("metadata should be component"), } @@ -753,6 +1294,10 @@ mod test { language: vec!["bar".to_owned()], processed_by: vec![("baz".to_owned(), "1.0".to_owned())], sdk: vec![], + registry_metadata: Some(RegistryMetadata { + authors: Some(vec!["Foo".to_owned()]), + ..Default::default() + }), }; let module = add.to_wasm(&module).unwrap(); @@ -794,6 +1339,7 @@ mod test { Metadata::Module { name, producers, + registry_metadata, range, } => { assert_eq!(name, &Some("foo".to_owned())); @@ -803,8 +1349,15 @@ mod test { producers.get("processed-by").unwrap().get("baz").unwrap(), "1.0" ); + + let registry_metadata = registry_metadata.as_ref().unwrap(); + assert_eq!( + registry_metadata.authors.as_ref().unwrap(), + &["Foo".to_owned()] + ); + assert_eq!(range.start, 10); - assert_eq!(range.end, 81); + assert_eq!(range.end, 120); } _ => panic!("child is a module"), } @@ -895,4 +1448,32 @@ mod test { _ => panic!("metadata should be module"), } } + + #[test] + fn overwrite_registry_metadata() { + let wat = "(module)"; + let module = wat::parse_str(wat).unwrap(); + let registry_metadata = RegistryMetadata { + authors: Some(vec!["Foo".to_owned()]), + ..Default::default() + }; + let module = registry_metadata.add_to_wasm(&module).unwrap(); + + let registry_metadata = RegistryMetadata { + authors: Some(vec!["Bar".to_owned()]), + ..Default::default() + }; + let module = registry_metadata.add_to_wasm(&module).unwrap(); + + let metadata = Metadata::from_binary(&module).unwrap(); + match metadata { + Metadata::Module { + registry_metadata, .. + } => { + let registry_metadata = registry_metadata.expect("some registry_metadata"); + assert_eq!(registry_metadata.authors.unwrap(), vec!["Bar".to_owned()]); + } + _ => panic!("metadata should be module"), + } + } }