wasm_metadata/
dependencies.rs1use std::fmt::{self, Display};
2use std::io::Read;
3use std::str::FromStr;
4
5use anyhow::{Result, ensure};
6use auditable_serde::VersionInfo;
7use flate2::Compression;
8use flate2::read::{ZlibDecoder, ZlibEncoder};
9use serde::Serialize;
10use wasm_encoder::{ComponentSection, CustomSection, Encode, Section};
11use wasmparser::CustomSectionReader;
12
13const MAX_DECOMPRESSED_SIZE: u64 = 2 * 1024 * 1024;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct Dependencies {
20 version_info: VersionInfo,
21 custom_section: CustomSection<'static>,
22}
23
24impl Dependencies {
25 pub(crate) fn parse_custom_section(reader: &CustomSectionReader<'_>) -> Result<Self> {
27 ensure!(
28 reader.name() == ".dep-v0",
29 "The `dependencies` custom section should have a name of '.dep-v0'"
30 );
31 let mut decompressed_data = Vec::new();
32 ZlibDecoder::new(reader.data())
33 .take(MAX_DECOMPRESSED_SIZE + 1)
34 .read_to_end(&mut decompressed_data)?;
35 ensure!(
36 decompressed_data.len() as u64 <= MAX_DECOMPRESSED_SIZE,
37 "`.dep-v0` custom section decompresses to more than the {MAX_DECOMPRESSED_SIZE} byte limit"
38 );
39 let decompressed_data = String::from_utf8(decompressed_data)?;
40 let dependency_tree = auditable_serde::VersionInfo::from_str(&decompressed_data)?;
41
42 Ok(Self {
43 version_info: dependency_tree,
44 custom_section: CustomSection {
45 name: ".dep-v0".into(),
46 data: reader.data().to_owned().into(),
47 },
48 })
49 }
50
51 pub fn new(dependency_tree: auditable_serde::VersionInfo) -> Result<Self> {
53 let data = serde_json::to_string(&dependency_tree)?;
54 ensure!(
55 data.len() as u64 <= MAX_DECOMPRESSED_SIZE,
56 "`.dep-v0` custom section would decompress to more than the {MAX_DECOMPRESSED_SIZE} byte limit"
57 );
58
59 let mut ret_vec = Vec::new();
60 let mut encoder = ZlibEncoder::new(data.as_bytes(), Compression::fast());
61 encoder.read_to_end(&mut ret_vec)?;
62
63 Ok(Self {
64 version_info: dependency_tree,
65 custom_section: CustomSection {
66 name: ".dep-v0".into(),
67 data: ret_vec.into(),
68 },
69 })
70 }
71
72 pub fn version_info(&self) -> &VersionInfo {
74 &self.version_info
75 }
76}
77
78impl Serialize for Dependencies {
79 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
80 where
81 S: serde::Serializer,
82 {
83 serializer.serialize_str(&self.to_string())
84 }
85}
86
87impl Display for Dependencies {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 let data = serde_json::to_string(&self.version_info).unwrap();
93 write!(f, "{data}")
94 }
95}
96
97impl ComponentSection for Dependencies {
98 fn id(&self) -> u8 {
99 ComponentSection::id(&self.custom_section)
100 }
101}
102
103impl Section for Dependencies {
104 fn id(&self) -> u8 {
105 Section::id(&self.custom_section)
106 }
107}
108
109impl Encode for Dependencies {
110 fn encode(&self, sink: &mut Vec<u8>) {
111 self.custom_section.encode(sink);
112 }
113}
114
115#[cfg(test)]
116mod test {
117 use super::*;
118 use auditable_serde::{Source, VersionInfo};
119 use std::str::FromStr;
120 use wasm_encoder::Component;
121 use wasmparser::Payload;
122
123 #[test]
124 fn roundtrip() {
125 let json_str = r#"{"packages":[{"name":"adler","version":"0.2.3","source":"registry"}]}"#;
126 let info = VersionInfo::from_str(json_str).unwrap();
127 assert_eq!(&info.packages[0].name, "adler");
128 let mut component = Component::new();
129 component.section(&Dependencies::new(info).unwrap());
130 let component = component.finish();
131
132 let mut parsed = false;
133 for section in wasmparser::Parser::new(0).parse_all(&component) {
134 if let Payload::CustomSection(reader) = section.unwrap() {
135 let dependencies = Dependencies::parse_custom_section(&reader).unwrap();
136 assert_eq!(dependencies.to_string(), json_str);
137 parsed = true;
138 }
139 }
140 assert!(parsed);
141 }
142
143 #[test]
144 fn rejects_decompression_too_big() {
145 let plain = vec![b' '; (MAX_DECOMPRESSED_SIZE * 2) as usize];
146 let mut compressed = Vec::new();
147 ZlibEncoder::new(&plain[..], Compression::best())
148 .read_to_end(&mut compressed)
149 .unwrap();
150 assert!(
151 compressed.len() < plain.len(),
152 "the section body should be much smaller than its decompressed form"
153 );
154
155 let mut component = Component::new();
156 component.section(&wasm_encoder::CustomSection {
157 name: ".dep-v0".into(),
158 data: compressed.into(),
159 });
160 let component = component.finish();
161
162 let mut saw_section = false;
163 for section in wasmparser::Parser::new(0).parse_all(&component) {
164 if let Payload::CustomSection(reader) = section.unwrap() {
165 saw_section = true;
166 let err = Dependencies::parse_custom_section(&reader).unwrap_err();
167 assert!(
168 err.to_string().contains("byte limit"),
169 "unexpected error: {err}"
170 );
171 }
172 }
173 assert!(saw_section);
174 }
175
176 #[test]
177 fn serialize() {
178 let json_str = r#"{"packages":[{"name":"adler","version":"0.2.3","source":"registry"}]}"#;
179 let info = VersionInfo::from_str(json_str).unwrap();
180 let dependencies = Dependencies::new(info).unwrap();
181 assert_eq!(dependencies.version_info().packages[0].name, "adler");
182 assert_eq!(
183 dependencies.version_info().packages[0].version.to_string(),
184 "0.2.3"
185 );
186 assert_eq!(
187 dependencies.version_info().packages[0].source,
188 Source::Registry,
189 );
190 }
191}