Skip to content

Commit 96c1aa1

Browse files
authored
[wasm-metadata] add support for OCI licenses (#1937)
1 parent bf5854f commit 96c1aa1

File tree

10 files changed

+201
-9
lines changed

10 files changed

+201
-9
lines changed

crates/wasm-metadata/src/add_metadata.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{rewrite_wasm, Author, Description, Producers, RegistryMetadata};
1+
use crate::{rewrite_wasm, Author, Description, Licenses, Producers, RegistryMetadata};
22

33
use anyhow::Result;
44

@@ -34,6 +34,10 @@ pub struct AddMetadata {
3434
#[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
3535
pub description: Option<Description>,
3636

37+
/// License(s) under which contained software is distributed as an SPDX License Expression.
38+
#[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
39+
pub licenses: Option<Licenses>,
40+
3741
/// Add an registry metadata to the registry-metadata section
3842
#[cfg_attr(feature="clap", clap(long, value_parser = parse_registry_metadata_value, value_name="PATH"))]
3943
pub registry_metadata: Option<RegistryMetadata>,
@@ -65,6 +69,7 @@ impl AddMetadata {
6569
&Producers::from_meta(self),
6670
&self.author,
6771
&self.description,
72+
&self.licenses,
6873
self.registry_metadata.as_ref(),
6974
input,
7075
)

crates/wasm-metadata/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
pub use add_metadata::AddMetadata;
66
pub use metadata::Metadata;
77
pub use names::{ComponentNames, ModuleNames};
8-
pub use oci_annotations::{Author, Description};
8+
pub use oci_annotations::{Author, Description, Licenses};
99
pub use producers::{Producers, ProducersField};
1010
pub use registry::{CustomLicense, Link, LinkType, RegistryMetadata};
1111

crates/wasm-metadata/src/metadata.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::fmt;
44
use std::ops::Range;
55
use wasmparser::{KnownCustom, Parser, Payload::*};
66

7-
use crate::{Author, ComponentNames, Description, ModuleNames, Producers, RegistryMetadata};
7+
use crate::{
8+
Author, ComponentNames, Description, Licenses, ModuleNames, Producers, RegistryMetadata,
9+
};
810

911
/// A tree of the metadata found in a WebAssembly binary.
1012
#[derive(Debug, Serialize)]
@@ -22,6 +24,8 @@ pub enum Metadata {
2224
author: Option<Author>,
2325
/// Human-readable description of the binary
2426
description: Option<Description>,
27+
/// License(s) under which contained software is distributed as an SPDX License Expression.
28+
licenses: Option<Licenses>,
2529
/// All child modules and components inside the component.
2630
children: Vec<Box<Metadata>>,
2731
/// Byte range of the module in the parent binary
@@ -39,6 +43,8 @@ pub enum Metadata {
3943
author: Option<Author>,
4044
/// Human-readable description of the binary
4145
description: Option<Description>,
46+
/// License(s) under which contained software is distributed as an SPDX License Expression.
47+
licenses: Option<Licenses>,
4248
/// Byte range of the module in the parent binary
4349
range: Range<usize>,
4450
},
@@ -128,6 +134,13 @@ impl Metadata {
128134
Metadata::Component { description, .. } => *description = Some(a),
129135
}
130136
}
137+
KnownCustom::Unknown if c.name() == "licenses" => {
138+
let a = Licenses::parse_custom_section(&c)?;
139+
match metadata.last_mut().expect("non-empty metadata stack") {
140+
Metadata::Module { licenses, .. } => *licenses = Some(a),
141+
Metadata::Component { licenses, .. } => *licenses = Some(a),
142+
}
143+
}
131144
_ => {}
132145
},
133146
_ => {}
@@ -144,6 +157,7 @@ impl Metadata {
144157
producers: None,
145158
author: None,
146159
description: None,
160+
licenses: None,
147161
registry_metadata: None,
148162
children: Vec::new(),
149163
range,
@@ -156,6 +170,7 @@ impl Metadata {
156170
producers: None,
157171
author: None,
158172
description: None,
173+
licenses: None,
159174
registry_metadata: None,
160175
range,
161176
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use std::borrow::Cow;
2+
use std::fmt::{self, Display};
3+
use std::str::FromStr;
4+
5+
use anyhow::{ensure, Error, Result};
6+
use serde::Serialize;
7+
use wasm_encoder::{ComponentSection, CustomSection, Encode, Section};
8+
use wasmparser::CustomSectionReader;
9+
10+
/// License(s) under which contained software is distributed as an SPDX License Expression.
11+
#[derive(Debug, Clone, PartialEq)]
12+
pub struct Licenses(CustomSection<'static>);
13+
14+
impl Licenses {
15+
/// Create a new instance of `Licenses`.
16+
pub fn new(s: &str) -> Result<Self> {
17+
Ok(spdx::Expression::parse(s)?.into())
18+
}
19+
20+
/// Parse a `licenses` custom section from a wasm binary.
21+
pub(crate) fn parse_custom_section(reader: &CustomSectionReader<'_>) -> Result<Self> {
22+
ensure!(
23+
reader.name() == "licenses",
24+
"The `licenses` custom section should have a name of 'license'"
25+
);
26+
let data = String::from_utf8(reader.data().to_owned())?;
27+
Self::new(&data)
28+
}
29+
}
30+
31+
impl FromStr for Licenses {
32+
type Err = Error;
33+
34+
fn from_str(s: &str) -> Result<Self, Self::Err> {
35+
Self::new(s)
36+
}
37+
}
38+
39+
impl From<spdx::Expression> for Licenses {
40+
fn from(expression: spdx::Expression) -> Self {
41+
Self(CustomSection {
42+
name: "licenses".into(),
43+
data: Cow::Owned(expression.to_string().into_bytes()),
44+
})
45+
}
46+
}
47+
48+
impl Serialize for Licenses {
49+
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
50+
where
51+
S: serde::Serializer,
52+
{
53+
serializer.serialize_str(&self.to_string())
54+
}
55+
}
56+
57+
impl Display for Licenses {
58+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59+
// NOTE: this will never panic since we always guarantee the data is
60+
// encoded as utf8, even if we internally store it as [u8].
61+
let data = String::from_utf8(self.0.data.to_vec()).unwrap();
62+
write!(f, "{data}")
63+
}
64+
}
65+
66+
impl ComponentSection for Licenses {
67+
fn id(&self) -> u8 {
68+
ComponentSection::id(&self.0)
69+
}
70+
}
71+
72+
impl Section for Licenses {
73+
fn id(&self) -> u8 {
74+
Section::id(&self.0)
75+
}
76+
}
77+
78+
impl Encode for Licenses {
79+
fn encode(&self, sink: &mut Vec<u8>) {
80+
self.0.encode(sink);
81+
}
82+
}
83+
84+
#[cfg(test)]
85+
mod test {
86+
use super::*;
87+
use wasm_encoder::Component;
88+
use wasmparser::Payload;
89+
90+
#[test]
91+
fn roundtrip() {
92+
let mut component = Component::new();
93+
component.section(
94+
&Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap(),
95+
);
96+
let component = component.finish();
97+
98+
let mut parsed = false;
99+
for section in wasmparser::Parser::new(0).parse_all(&component) {
100+
if let Payload::CustomSection(reader) = section.unwrap() {
101+
let description = Licenses::parse_custom_section(&reader).unwrap();
102+
assert_eq!(
103+
description.to_string(),
104+
"Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT"
105+
);
106+
parsed = true;
107+
}
108+
}
109+
assert!(parsed);
110+
}
111+
112+
#[test]
113+
fn serialize() {
114+
let description =
115+
Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap();
116+
let json = serde_json::to_string(&description).unwrap();
117+
assert_eq!(
118+
r#""Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT""#,
119+
json
120+
);
121+
}
122+
}

crates/wasm-metadata/src/oci_annotations/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
1818
pub use author::Author;
1919
pub use description::Description;
20+
pub use licenses::Licenses;
2021

2122
mod author;
2223
mod description;
24+
mod licenses;

crates/wasm-metadata/src/producers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ impl Producers {
148148
/// Merge into an existing wasm module. Rewrites the module with this producers section
149149
/// merged into its existing one, or adds this producers section if none is present.
150150
pub fn add_to_wasm(&self, input: &[u8]) -> Result<Vec<u8>> {
151-
rewrite_wasm(&None, self, &None, &None, None, input)
151+
rewrite_wasm(&None, self, &None, &None, &None, None, input)
152152
}
153153

154154
pub(crate) fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result {

crates/wasm-metadata/src/registry.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,15 @@ impl RegistryMetadata {
4444
/// Merge into an existing wasm module. Rewrites the module with this registry-metadata section
4545
/// overwriting its existing one, or adds this registry-metadata section if none is present.
4646
pub fn add_to_wasm(&self, input: &[u8]) -> Result<Vec<u8>> {
47-
rewrite_wasm(&None, &Producers::empty(), &None, &None, Some(&self), input)
47+
rewrite_wasm(
48+
&None,
49+
&Producers::empty(),
50+
&None,
51+
&None,
52+
&None,
53+
Some(&self),
54+
input,
55+
)
4856
}
4957

5058
/// Parse a Wasm binary and extract the `Registry` section, if there is any.

crates/wasm-metadata/src/rewrite.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::{Author, ComponentNames, Description, ModuleNames, Producers, RegistryMetadata};
1+
use crate::{
2+
Author, ComponentNames, Description, Licenses, ModuleNames, Producers, RegistryMetadata,
3+
};
24
use anyhow::Result;
35
use std::borrow::Cow;
46
use std::mem;
@@ -11,6 +13,7 @@ pub(crate) fn rewrite_wasm(
1113
add_producers: &Producers,
1214
add_author: &Option<Author>,
1315
add_description: &Option<Description>,
16+
add_licenses: &Option<Licenses>,
1417
add_registry_metadata: Option<&RegistryMetadata>,
1518
input: &[u8],
1619
) -> Result<Vec<u8>> {
@@ -106,6 +109,13 @@ pub(crate) fn rewrite_wasm(
106109
continue;
107110
}
108111
}
112+
KnownCustom::Unknown if c.name() == "licenses" => {
113+
if add_licenses.is_none() {
114+
let licenses = Licenses::parse_custom_section(c)?;
115+
licenses.append_to(&mut output);
116+
continue;
117+
}
118+
}
109119
_ => {}
110120
}
111121
}
@@ -141,6 +151,9 @@ pub(crate) fn rewrite_wasm(
141151
if let Some(description) = add_description {
142152
description.append_to(&mut output);
143153
}
154+
if let Some(licenses) = add_licenses {
155+
licenses.append_to(&mut output);
156+
}
144157
if add_registry_metadata.is_some() {
145158
let registry_metadata = wasm_encoder::CustomSection {
146159
name: Cow::Borrowed("registry-metadata"),

crates/wasm-metadata/tests/component.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ fn add_to_empty_component() {
1313
sdk: vec![],
1414
author: Some(Author::new("Chashu Cat")),
1515
description: Some(Description::new("Chashu likes tuna")),
16+
licenses: Some(
17+
Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap(),
18+
),
1619
registry_metadata: Some(RegistryMetadata {
1720
authors: Some(vec!["foo".to_owned()]),
1821
description: Some("foo bar baz".to_owned()),
@@ -46,6 +49,7 @@ fn add_to_empty_component() {
4649
registry_metadata,
4750
author,
4851
description,
52+
licenses,
4953
children,
5054
range,
5155
} => {
@@ -63,6 +67,10 @@ fn add_to_empty_component() {
6367

6468
assert_eq!(author.unwrap(), Author::new("Chashu Cat"));
6569
assert_eq!(description.unwrap(), Description::new("Chashu likes tuna"));
70+
assert_eq!(
71+
licenses.unwrap(),
72+
Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap()
73+
);
6674

6775
let registry_metadata = registry_metadata.unwrap();
6876

@@ -106,7 +114,7 @@ fn add_to_empty_component() {
106114
);
107115

108116
assert_eq!(range.start, 0);
109-
assert_eq!(range.end, 485);
117+
assert_eq!(range.end, 547);
110118
}
111119
_ => panic!("metadata should be component"),
112120
}
@@ -123,6 +131,9 @@ fn add_to_nested_component() {
123131
sdk: vec![],
124132
author: Some(Author::new("Chashu Cat")),
125133
description: Some(Description::new("Chashu likes tuna")),
134+
licenses: Some(
135+
Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT").unwrap(),
136+
),
126137
registry_metadata: Some(RegistryMetadata {
127138
authors: Some(vec!["Foo".to_owned()]),
128139
..Default::default()
@@ -169,6 +180,7 @@ fn add_to_nested_component() {
169180
name,
170181
producers,
171182
author,
183+
licenses,
172184
registry_metadata,
173185
range,
174186
description,
@@ -186,6 +198,13 @@ fn add_to_nested_component() {
186198

187199
assert_eq!(author, &Some(Author::new("Chashu Cat")));
188200
assert_eq!(description, &Some(Description::new("Chashu likes tuna")));
201+
assert_eq!(
202+
licenses,
203+
&Some(
204+
Licenses::new("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT")
205+
.unwrap()
206+
)
207+
);
189208

190209
let registry_metadata = registry_metadata.as_ref().unwrap();
191210
assert_eq!(
@@ -194,7 +213,7 @@ fn add_to_nested_component() {
194213
);
195214

196215
assert_eq!(range.start, 11);
197-
assert_eq!(range.end, 174);
216+
assert_eq!(range.end, 236);
198217
}
199218
_ => panic!("child is a module"),
200219
}

0 commit comments

Comments
 (0)