Skip to main content

wasm_metadata/
dependencies.rs

1use 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
13/// This section is currently zlib-compressed so have a hard limit to avoid
14/// decompressing undue amounts of data from a wasm module.
15const MAX_DECOMPRESSED_SIZE: u64 = 2 * 1024 * 1024;
16
17/// Human-readable description of the binary
18#[derive(Debug, Clone, PartialEq)]
19pub struct Dependencies {
20    version_info: VersionInfo,
21    custom_section: CustomSection<'static>,
22}
23
24impl Dependencies {
25    /// Parse an `description` custom section from a wasm binary.
26    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    /// Create a new instance of `Dependencies`.
52    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    /// Provides access to the version information stored in the object
73    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        // NOTE: this will never panic since we always guarantee the data is
90        // encoded as utf8, even if we internally store it as [u8].
91        // let data = String::from_utf8(self.0.data.to_vec()).unwrap();
92        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}