Coverage Report

Created: 2026-08-02 07:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wasm-tools/crates/wasm-metadata/src/producers.rs
Line
Count
Source
1
use anyhow::Result;
2
use indexmap::{IndexMap, map::Entry};
3
use wasm_encoder::Encode;
4
use wasmparser::{BinaryReader, KnownCustom, Parser, ProducersSectionReader};
5
6
use crate::{AddMetadata, rewrite_wasm};
7
/// A representation of a WebAssembly producers section.
8
///
9
/// Spec: <https://github.com/WebAssembly/tool-conventions/blob/main/ProducersSection.md>
10
#[derive(Debug)]
11
#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
12
pub struct Producers(
13
    #[cfg_attr(
14
        feature = "serde",
15
        serde(serialize_with = "indexmap::map::serde_seq::serialize")
16
    )]
17
    IndexMap<String, IndexMap<String, String>>,
18
);
19
20
impl Default for Producers {
21
0
    fn default() -> Self {
22
0
        Self::empty()
23
0
    }
24
}
25
26
impl Producers {
27
    /// Creates an empty producers section
28
19.2k
    pub fn empty() -> Self {
29
19.2k
        Producers(IndexMap::new())
30
19.2k
    }
31
32
    /// Indicates if section is empty
33
2.95k
    pub fn is_empty(&self) -> bool {
34
2.95k
        self.0.is_empty()
35
2.95k
    }
36
37
    /// Read the producers section from a Wasm binary. Supports both core
38
    /// Modules and Components. In the component case, only returns the
39
    /// producers section in the outer component, ignoring all interior
40
    /// components and modules.
41
2.95k
    pub fn from_wasm(bytes: &[u8]) -> Result<Option<Self>> {
42
2.95k
        let mut depth = 0;
43
14.7k
        for payload in Parser::new(0).parse_all(bytes) {
44
14.7k
            let payload = payload?;
45
            use wasmparser::Payload::*;
46
5.90k
            match payload {
47
0
                ModuleSection { .. } | ComponentSection { .. } => depth += 1,
48
0
                End { .. } => depth -= 1,
49
5.90k
                CustomSection(c) if depth == 0 => {
50
5.90k
                    if let KnownCustom::Producers(_) = c.as_known() {
51
2.95k
                        let producers = Self::from_bytes(c.data(), c.data_offset())?;
52
2.95k
                        return Ok(Some(producers));
53
2.95k
                    }
54
                }
55
8.85k
                _ => {}
56
            }
57
        }
58
0
        Ok(None)
59
2.95k
    }
60
    /// Read the producers section from a Wasm binary.
61
2.95k
    pub fn from_bytes(bytes: &[u8], offset: usize) -> Result<Self> {
62
2.95k
        let reader = BinaryReader::new(bytes, offset);
63
2.95k
        let section = ProducersSectionReader::new(reader)?;
64
2.95k
        let mut fields = IndexMap::new();
65
2.95k
        for field in section.into_iter() {
66
2.95k
            let field = field?;
67
2.95k
            let mut values = IndexMap::new();
68
2.95k
            for value in field.values.into_iter() {
69
2.95k
                let value = value?;
70
2.95k
                values.insert(value.name.to_owned(), value.version.to_owned());
71
            }
72
2.95k
            fields.insert(field.name.to_owned(), values);
73
        }
74
2.95k
        Ok(Producers(fields))
75
2.95k
    }
76
    /// Add a name & version value to a field.
77
    ///
78
    /// The spec says expected field names are "language", "processed-by", and "sdk".
79
    /// The version value should be left blank for languages.
80
19.2k
    pub fn add(&mut self, field: &str, name: &str, version: &str) {
81
19.2k
        match self.0.entry(field.to_string()) {
82
0
            Entry::Occupied(e) => {
83
0
                e.into_mut().insert(name.to_owned(), version.to_owned());
84
0
            }
85
19.2k
            Entry::Vacant(e) => {
86
19.2k
                let mut m = IndexMap::new();
87
19.2k
                m.insert(name.to_owned(), version.to_owned());
88
19.2k
                e.insert(m);
89
19.2k
            }
90
        }
91
19.2k
    }
92
93
    /// Add all values found in another `Producers` section. Values in `other` take
94
    /// precedence.
95
2.95k
    pub fn merge(&mut self, other: &Self) {
96
2.95k
        for (field, values) in other.iter() {
97
2.95k
            for (name, version) in values.iter() {
98
2.95k
                self.add(field, name, version);
99
2.95k
            }
100
        }
101
2.95k
    }
102
103
    /// Get the contents of a field
104
0
    pub fn get<'a>(&'a self, field: &str) -> Option<ProducersField<'a>> {
105
0
        self.0.get(&field.to_owned()).map(ProducersField)
106
0
    }
107
108
    /// Iterate through all fields
109
2.95k
    pub fn iter<'a>(&'a self) -> impl Iterator<Item = (&'a String, ProducersField<'a>)> + 'a {
110
2.95k
        self.0
111
2.95k
            .iter()
112
2.95k
            .map(|(name, field)| (name, ProducersField(field)))
113
2.95k
    }
114
115
    /// Construct the fields specified by [`AddMetadata`]
116
0
    pub(crate) fn from_meta(add: &AddMetadata) -> Self {
117
0
        let mut s = Self::empty();
118
0
        for (lang, version) in add.language.iter() {
119
0
            s.add("language", &lang, &version);
120
0
        }
121
0
        for (name, version) in add.processed_by.iter() {
122
0
            s.add("processed-by", &name, &version);
123
0
        }
124
0
        for (name, version) in add.sdk.iter() {
125
0
            s.add("sdk", &name, &version);
126
0
        }
127
0
        s
128
0
    }
129
130
    /// Serialize into [`wasm_encoder::ProducersSection`].
131
19.2k
    pub(crate) fn section(&self) -> wasm_encoder::ProducersSection {
132
19.2k
        let mut section = wasm_encoder::ProducersSection::new();
133
19.2k
        for (fieldname, fieldvalues) in self.0.iter() {
134
19.2k
            let mut field = wasm_encoder::ProducersField::new();
135
19.2k
            for (name, version) in fieldvalues {
136
19.2k
                field.value(&name, &version);
137
19.2k
            }
138
19.2k
            section.field(&fieldname, &field);
139
        }
140
19.2k
        section
141
19.2k
    }
142
143
    /// Serialize into the raw bytes of a wasm custom section.
144
16.3k
    pub fn raw_custom_section(&self) -> Vec<u8> {
145
16.3k
        let mut ret = Vec::new();
146
16.3k
        self.section().encode(&mut ret);
147
16.3k
        ret
148
16.3k
    }
149
150
    /// Merge into an existing wasm module. Rewrites the module with this producers section
151
    /// merged into its existing one, or adds this producers section if none is present.
152
2.95k
    pub fn add_to_wasm(&self, input: &[u8]) -> Result<Vec<u8>> {
153
2.95k
        rewrite_wasm(&Default::default(), self, input)
154
2.95k
    }
155
}
156
157
/// Contents of a producers field
158
#[derive(Debug)]
159
pub struct ProducersField<'a>(&'a IndexMap<String, String>);
160
161
impl<'a> ProducersField<'a> {
162
    /// Get the version associated with a name in the field
163
0
    pub fn get(&self, name: &str) -> Option<&'a String> {
164
0
        self.0.get(&name.to_owned())
165
0
    }
166
    /// Iterate through all name-version pairs in the field
167
2.95k
    pub fn iter(&self) -> impl Iterator<Item = (&'a String, &'a String)> + 'a {
168
2.95k
        self.0.iter()
169
2.95k
    }
170
}
171
172
#[cfg(test)]
173
mod test {
174
    use super::*;
175
    use crate::{Metadata, Payload};
176
    use wasm_encoder::Module;
177
178
    #[test]
179
    fn producers_empty_module() {
180
        let module = Module::new().finish();
181
        let mut producers = Producers::empty();
182
        producers.add("language", "bar", "");
183
        producers.add("processed-by", "baz", "1.0");
184
185
        let module = producers.add_to_wasm(&module).unwrap();
186
187
        match Payload::from_binary(&module).unwrap() {
188
            Payload::Module(Metadata {
189
                name, producers, ..
190
            }) => {
191
                assert_eq!(name, None);
192
                let producers = producers.expect("some producers");
193
                assert_eq!(producers.get("language").unwrap().get("bar").unwrap(), "");
194
                assert_eq!(
195
                    producers.get("processed-by").unwrap().get("baz").unwrap(),
196
                    "1.0"
197
                );
198
            }
199
            _ => panic!("metadata should be module"),
200
        }
201
    }
202
203
    #[test]
204
    fn producers_add_another_field() {
205
        let module = Module::new().finish();
206
        let mut producers = Producers::empty();
207
        producers.add("language", "bar", "");
208
        producers.add("processed-by", "baz", "1.0");
209
        let module = producers.add_to_wasm(&module).unwrap();
210
211
        let mut producers = Producers::empty();
212
        producers.add("language", "waaat", "");
213
        let module = producers.add_to_wasm(&module).unwrap();
214
215
        match Payload::from_binary(&module).unwrap() {
216
            Payload::Module(Metadata {
217
                name, producers, ..
218
            }) => {
219
                assert_eq!(name, None);
220
                let producers = producers.expect("some producers");
221
                assert_eq!(producers.get("language").unwrap().get("bar").unwrap(), "");
222
                assert_eq!(producers.get("language").unwrap().get("waaat").unwrap(), "");
223
                assert_eq!(
224
                    producers.get("processed-by").unwrap().get("baz").unwrap(),
225
                    "1.0"
226
                );
227
            }
228
            _ => panic!("metadata should be module"),
229
        }
230
    }
231
232
    #[test]
233
    fn producers_overwrite_field() {
234
        let module = Module::new().finish();
235
        let mut producers = Producers::empty();
236
        producers.add("processed-by", "baz", "1.0");
237
        let module = producers.add_to_wasm(&module).unwrap();
238
239
        let mut producers = Producers::empty();
240
        producers.add("processed-by", "baz", "420");
241
        let module = producers.add_to_wasm(&module).unwrap();
242
243
        match Payload::from_binary(&module).unwrap() {
244
            Payload::Module(Metadata { producers, .. }) => {
245
                let producers = producers.expect("some producers");
246
                assert_eq!(
247
                    producers.get("processed-by").unwrap().get("baz").unwrap(),
248
                    "420"
249
                );
250
            }
251
            _ => panic!("metadata should be module"),
252
        }
253
    }
254
}