Repository: bytecodealliance/wasm-tools
Pull request #1940 (2024-12-06 18:52:04)

[wasm-metadata] add support for OCI source annotations
Makes progress towards #1922. Thanks!


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index bd0ad4cc..c78924b3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -528,6 +528,17 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
 
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "dl"
 version = "0.1.0"
@@ -667,6 +678,15 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
 
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
 [[package]]
 name = "fuzz-stats"
 version = "0.0.0"
@@ -784,12 +804,151 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "id-arena"
 version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
 
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
 [[package]]
 name = "im-rc"
 version = "15.1.0"
@@ -961,6 +1120,12 @@ version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
+[[package]]
+name = "litemap"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+
 [[package]]
 name = "log"
 version = "0.4.22"
@@ -1093,6 +1258,12 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
 
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
 [[package]]
 name = "petgraph"
 version = "0.6.5"
@@ -1446,6 +1617,17 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "target-lexicon"
 version = "0.12.16"
@@ -1513,6 +1695,16 @@ dependencies = [
  "num_cpus",
 ]
 
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
 [[package]]
 name = "tinytemplate"
 version = "1.2.1"
@@ -1569,6 +1761,29 @@ version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
 
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
 [[package]]
 name = "utf8parse"
 version = "0.2.2"
@@ -1677,6 +1892,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "spdx",
+ "url",
  "wasm-encoder 0.221.2",
  "wasmparser 0.221.2",
 ]
@@ -2411,12 +2627,48 @@ dependencies = [
  "wit-parser 0.221.2",
 ]
 
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
 [[package]]
 name = "yansi"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
 
+[[package]]
+name = "yoke"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.7.35"
@@ -2437,3 +2689,46 @@ dependencies = [
  "quote",
  "syn",
 ]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/crates/wasm-metadata/Cargo.toml b/crates/wasm-metadata/Cargo.toml
index a00f74f6..70911348 100644
--- a/crates/wasm-metadata/Cargo.toml
+++ b/crates/wasm-metadata/Cargo.toml
@@ -20,3 +20,4 @@ serde = { workspace = true }
 serde_derive = { workspace = true }
 serde_json = { version = "1" }
 spdx = "0.10.1"
+url = { workspace = true }
diff --git a/crates/wasm-metadata/src/add_metadata.rs b/crates/wasm-metadata/src/add_metadata.rs
index 699a57e3..cc961d90 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, RegistryMetadata};
+use crate::{rewrite_wasm, Author, Description, Licenses, Producers, RegistryMetadata, Source};
 
 use anyhow::Result;
 
@@ -38,6 +38,10 @@ pub struct AddMetadata {
     #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
     pub licenses: Option<Licenses>,
 
+    /// URL to get source code for building the image
+    #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
+    pub source: Option<Source>,
+
     /// 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>,
@@ -70,6 +74,7 @@ impl AddMetadata {
             &self.author,
             &self.description,
             &self.licenses,
+            &self.source,
             self.registry_metadata.as_ref(),
             input,
         )
diff --git a/crates/wasm-metadata/src/lib.rs b/crates/wasm-metadata/src/lib.rs
index 25a58f9c..2988707e 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};
+pub use oci_annotations::{Author, Description, Licenses, Source};
 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 d182f139..f9ab6e60 100644
--- a/crates/wasm-metadata/src/metadata.rs
+++ b/crates/wasm-metadata/src/metadata.rs
@@ -5,7 +5,7 @@ use std::ops::Range;
 use wasmparser::{KnownCustom, Parser, Payload::*};
 
 use crate::{
-    Author, ComponentNames, Description, Licenses, ModuleNames, Producers, RegistryMetadata,
+    Author, ComponentNames, Description, Licenses, ModuleNames, Producers, RegistryMetadata, Source,
 };
 
 /// A tree of the metadata found in a WebAssembly binary.
@@ -26,6 +26,8 @@ pub enum Metadata {
         description: Option<Description>,
         /// License(s) under which contained software is distributed as an SPDX License Expression.
         licenses: Option<Licenses>,
+        /// URL to get source code for building the image
+        source: Option<Source>,
         /// All child modules and components inside the component.
         children: Vec<Box<Metadata>>,
         /// Byte range of the module in the parent binary
@@ -45,6 +47,8 @@ pub enum Metadata {
         description: Option<Description>,
         /// License(s) under which contained software is distributed as an SPDX License Expression.
         licenses: Option<Licenses>,
+        /// URL to get source code for building the image
+        source: Option<Source>,
         /// Byte range of the module in the parent binary
         range: Range<usize>,
     },
@@ -141,6 +145,13 @@ impl Metadata {
                             Metadata::Component { licenses, .. } => *licenses = Some(a),
                         }
                     }
+                    KnownCustom::Unknown if c.name() == "source" => {
+                        let a = Source::parse_custom_section(&c)?;
+                        match metadata.last_mut().expect("non-empty metadata stack") {
+                            Metadata::Module { source, .. } => *source = Some(a),
+                            Metadata::Component { source, .. } => *source = Some(a),
+                        }
+                    }
                     _ => {}
                 },
                 _ => {}
@@ -158,6 +169,7 @@ impl Metadata {
             author: None,
             description: None,
             licenses: None,
+            source: None,
             registry_metadata: None,
             children: Vec::new(),
             range,
@@ -171,6 +183,7 @@ impl Metadata {
             author: None,
             description: None,
             licenses: None,
+            source: None,
             registry_metadata: None,
             range,
         }
diff --git a/crates/wasm-metadata/src/oci_annotations/mod.rs b/crates/wasm-metadata/src/oci_annotations/mod.rs
index ab19290f..bbd01aef 100644
--- a/crates/wasm-metadata/src/oci_annotations/mod.rs
+++ b/crates/wasm-metadata/src/oci_annotations/mod.rs
@@ -18,7 +18,9 @@
 pub use author::Author;
 pub use description::Description;
 pub use licenses::Licenses;
+pub use source::Source;
 
 mod author;
 mod description;
 mod licenses;
+mod source;
diff --git a/crates/wasm-metadata/src/oci_annotations/source.rs b/crates/wasm-metadata/src/oci_annotations/source.rs
new file mode 100644
index 00000000..8c25b444
--- /dev/null
+++ b/crates/wasm-metadata/src/oci_annotations/source.rs
@@ -0,0 +1,117 @@
+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 get source code for building the image
+#[derive(Debug, Clone, PartialEq)]
+pub struct Source(CustomSection<'static>);
+
+impl Source {
+    /// Create a new instance of `Source`.
+    pub fn new(s: &str) -> Result<Self> {
+        Ok(Url::parse(s)?.into())
+    }
+
+    /// Parse a `source` custom section from a wasm binary.
+    pub(crate) fn parse_custom_section(reader: &CustomSectionReader<'_>) -> Result<Self> {
+        ensure!(
+            reader.name() == "source",
+            "The `source` custom section should have a name of 'source'"
+        );
+        let data = String::from_utf8(reader.data().to_owned())?;
+        Self::new(&data)
+    }
+}
+
+impl FromStr for Source {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::new(s)
+    }
+}
+
+impl From<Url> for Source {
+    fn from(expression: Url) -> Self {
+        Self(CustomSection {
+            name: "source".into(),
+            data: Cow::Owned(expression.to_string().into_bytes()),
+        })
+    }
+}
+
+impl Serialize for Source {
+    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 Source {
+    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 Source {
+    fn id(&self) -> u8 {
+        ComponentSection::id(&self.0)
+    }
+}
+
+impl Section for Source {
+    fn id(&self) -> u8 {
+        Section::id(&self.0)
+    }
+}
+
+impl Encode for Source {
+    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(&Source::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 = Source::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 = Source::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/producers.rs b/crates/wasm-metadata/src/producers.rs
index 56b8b70a..e3b4cde6 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, &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/registry.rs b/crates/wasm-metadata/src/registry.rs
index 9f9d647f..286c61bc 100644
--- a/crates/wasm-metadata/src/registry.rs
+++ b/crates/wasm-metadata/src/registry.rs
@@ -50,6 +50,7 @@ impl RegistryMetadata {
             &None,
             &None,
             &None,
+            &None,
             Some(&self),
             input,
         )
diff --git a/crates/wasm-metadata/src/rewrite.rs b/crates/wasm-metadata/src/rewrite.rs
index ba2c4dfd..44c10d50 100644
--- a/crates/wasm-metadata/src/rewrite.rs
+++ b/crates/wasm-metadata/src/rewrite.rs
@@ -1,5 +1,5 @@
 use crate::{
-    Author, ComponentNames, Description, Licenses, ModuleNames, Producers, RegistryMetadata,
+    Author, ComponentNames, Description, Licenses, ModuleNames, Producers, RegistryMetadata, Source,
 };
 use anyhow::Result;
 use std::borrow::Cow;
@@ -14,6 +14,7 @@ pub(crate) fn rewrite_wasm(
     add_author: &Option<Author>,
     add_description: &Option<Description>,
     add_licenses: &Option<Licenses>,
+    add_source: &Option<Source>,
     add_registry_metadata: Option<&RegistryMetadata>,
     input: &[u8],
 ) -> Result<Vec<u8>> {
@@ -116,6 +117,13 @@ pub(crate) fn rewrite_wasm(
                             continue;
                         }
                     }
+                    KnownCustom::Unknown if c.name() == "source" => {
+                        if add_source.is_none() {
+                            let source = Source::parse_custom_section(c)?;
+                            source.append_to(&mut output);
+                            continue;
+                        }
+                    }
                     _ => {}
                 }
             }
@@ -154,6 +162,9 @@ pub(crate) fn rewrite_wasm(
     if let Some(licenses) = add_licenses {
         licenses.append_to(&mut output);
     }
+    if let Some(source) = add_source {
+        source.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 c00e23a7..fdb930fa 100644
--- a/crates/wasm-metadata/tests/component.rs
+++ b/crates/wasm-metadata/tests/component.rs
@@ -16,6 +16,7 @@ fn add_to_empty_component() {
         licenses: Some(
             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()),
         registry_metadata: Some(RegistryMetadata {
             authors: Some(vec!["foo".to_owned()]),
             description: Some("foo bar baz".to_owned()),
@@ -50,6 +51,7 @@ fn add_to_empty_component() {
             author,
             description,
             licenses,
+            source,
             children,
             range,
         } => {
@@ -71,6 +73,10 @@ fn add_to_empty_component() {
                 licenses.unwrap(),
                 Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap()
             );
+            assert_eq!(
+                source.unwrap(),
+                Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap(),
+            );
 
             let registry_metadata = registry_metadata.unwrap();
 
@@ -114,7 +120,7 @@ fn add_to_empty_component() {
             );
 
             assert_eq!(range.start, 0);
-            assert_eq!(range.end, 547);
+            assert_eq!(range.end, 602);
         }
         _ => panic!("metadata should be component"),
     }
@@ -134,6 +140,7 @@ fn add_to_nested_component() {
         licenses: Some(
             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()),
         registry_metadata: Some(RegistryMetadata {
             authors: Some(vec!["Foo".to_owned()]),
             ..Default::default()
@@ -182,6 +189,7 @@ fn add_to_nested_component() {
                     author,
                     licenses,
                     registry_metadata,
+                    source,
                     range,
                     description,
                 } => {
@@ -205,6 +213,12 @@ fn add_to_nested_component() {
                                 .unwrap()
                         )
                     );
+                    assert_eq!(
+                        source,
+                        &Some(
+                            Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap()
+                        ),
+                    );
 
                     let registry_metadata = registry_metadata.as_ref().unwrap();
                     assert_eq!(
@@ -213,7 +227,7 @@ fn add_to_nested_component() {
                     );
 
                     assert_eq!(range.start, 11);
-                    assert_eq!(range.end, 236);
+                    assert_eq!(range.end, 291);
                 }
                 _ => panic!("child is a module"),
             }
diff --git a/crates/wasm-metadata/tests/module.rs b/crates/wasm-metadata/tests/module.rs
index 035c2d91..4a1bb9f7 100644
--- a/crates/wasm-metadata/tests/module.rs
+++ b/crates/wasm-metadata/tests/module.rs
@@ -16,6 +16,7 @@ fn add_to_empty_module() {
         licenses: Some(
             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()),
         registry_metadata: Some(RegistryMetadata {
             authors: Some(vec!["foo".to_owned()]),
             description: Some("foo bar baz".to_owned()),
@@ -49,6 +50,7 @@ fn add_to_empty_module() {
             author,
             description,
             licenses,
+            source,
             registry_metadata,
             range,
         } => {
@@ -84,6 +86,12 @@ fn add_to_empty_module() {
                 registry_metadata.license.unwrap(),
                 "MIT OR LicenseRef-FOO".to_owned()
             );
+
+            assert_eq!(
+                source.unwrap(),
+                Source::new("https://github.com/bytecodealliance/wasm-tools").unwrap(),
+            );
+
             assert_eq!(
                 registry_metadata.custom_licenses.unwrap(),
                 vec![CustomLicense {
@@ -112,7 +120,7 @@ fn add_to_empty_module() {
             );
 
             assert_eq!(range.start, 0);
-            assert_eq!(range.end, 537);
+            assert_eq!(range.end, 592);
         }
         _ => panic!("metadata should be module"),
     }