Skip to main content

baedeker_core/binary/
section.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4//! WASM section parsing.
5//!
6//! Sections are the top-level organizational unit of a WASM binary.
7//! See [Spec §5.5](https://webassembly.github.io/spec/core/binary/modules.html#sections).
8
9use alloc::vec::Vec;
10
11use crate::binary::leb128::{self, Cursor};
12use crate::error::{ByteOffset, DecodeContext, DecodeError, DecodeErrorKind};
13
14/// WASM section identifiers.
15/// See [Spec §5.5](https://webassembly.github.io/spec/core/binary/modules.html#sections).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u8)]
18pub enum SectionId {
19    Custom = 0,
20    Type = 1,
21    Import = 2,
22    Function = 3,
23    Table = 4,
24    Memory = 5,
25    Global = 6,
26    Export = 7,
27    Start = 8,
28    Element = 9,
29    Code = 10,
30    Data = 11,
31    DataCount = 12,
32}
33
34impl SectionId {
35    /// Try to construct a `SectionId` from a raw byte value.
36    pub fn from_byte(byte: u8) -> Option<Self> {
37        match byte {
38            0 => Some(SectionId::Custom),
39            1 => Some(SectionId::Type),
40            2 => Some(SectionId::Import),
41            3 => Some(SectionId::Function),
42            4 => Some(SectionId::Table),
43            5 => Some(SectionId::Memory),
44            6 => Some(SectionId::Global),
45            7 => Some(SectionId::Export),
46            8 => Some(SectionId::Start),
47            9 => Some(SectionId::Element),
48            10 => Some(SectionId::Code),
49            11 => Some(SectionId::Data),
50            12 => Some(SectionId::DataCount),
51            _ => None,
52        }
53    }
54
55    /// Human-readable name for this section.
56    pub fn name(self) -> &'static str {
57        match self {
58            SectionId::Custom => "custom",
59            SectionId::Type => "type",
60            SectionId::Import => "import",
61            SectionId::Function => "function",
62            SectionId::Table => "table",
63            SectionId::Memory => "memory",
64            SectionId::Global => "global",
65            SectionId::Export => "export",
66            SectionId::Start => "start",
67            SectionId::Element => "element",
68            SectionId::Code => "code",
69            SectionId::Data => "data",
70            SectionId::DataCount => "datacount",
71        }
72    }
73}
74
75/// A parsed but not yet interpreted section — just the labeled byte span.
76#[derive(Debug, Clone)]
77pub struct RawSection<'a> {
78    /// The section ID.
79    pub id: SectionId,
80    /// Byte offset of the section contents within the original binary.
81    pub offset: usize,
82    /// The raw section contents (after the section header).
83    pub data: &'a [u8],
84}
85
86/// The WASM binary magic number: `\0asm`.
87const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6D];
88
89/// The WASM binary version we support: 1.
90const WASM_VERSION: [u8; 4] = [0x01, 0x00, 0x00, 0x00];
91
92/// Validate the WASM preamble (magic number + version), returning the cursor
93/// positioned after the 8-byte header.
94pub fn parse_preamble<'a>(cursor: &mut Cursor<'a>) -> Result<(), DecodeError> {
95    let magic = cursor.read_bytes(4).map_err(|_| DecodeError {
96        offset: ByteOffset(0),
97        context: DecodeContext::Magic,
98        kind: DecodeErrorKind::UnexpectedEof,
99    })?;
100
101    if magic != WASM_MAGIC {
102        return Err(DecodeError {
103            offset: ByteOffset(0),
104            context: DecodeContext::Magic,
105            kind: DecodeErrorKind::InvalidMagic,
106        });
107    }
108
109    let version = cursor.read_bytes(4).map_err(|_| DecodeError {
110        offset: ByteOffset(4),
111        context: DecodeContext::Version,
112        kind: DecodeErrorKind::UnexpectedEof,
113    })?;
114
115    if version != WASM_VERSION {
116        let found = u32::from_le_bytes([version[0], version[1], version[2], version[3]]);
117        return Err(DecodeError {
118            offset: ByteOffset(4),
119            context: DecodeContext::Version,
120            kind: DecodeErrorKind::UnsupportedVersion { found },
121        });
122    }
123
124    Ok(())
125}
126
127/// Parse all sections from a cursor positioned after the preamble.
128///
129/// Returns the sections as raw byte spans. Non-custom sections must appear
130/// in order of their section IDs (custom sections may appear anywhere).
131fn section_order(id: SectionId) -> u8 {
132    match id {
133        SectionId::Custom => 0,
134        SectionId::Type => 1,
135        SectionId::Import => 2,
136        SectionId::Function => 3,
137        SectionId::Table => 4,
138        SectionId::Memory => 5,
139        SectionId::Global => 6,
140        SectionId::Export => 7,
141        SectionId::Start => 8,
142        SectionId::Element => 9,
143        SectionId::Code => 10,
144        SectionId::Data => 11,
145        SectionId::DataCount => 12,
146    }
147}
148
149fn sections_in_valid_order(prev: SectionId, current: SectionId) -> bool {
150    let prev_order = section_order(prev);
151    let current_order = section_order(current);
152
153    current_order >= prev_order
154        || matches!(
155            (prev, current),
156            (SectionId::Element, SectionId::DataCount)
157                | (SectionId::DataCount, SectionId::Code)
158                | (SectionId::Data, SectionId::DataCount)
159        )
160}
161
162fn validate_custom_section_name(data: &[u8], base_offset: usize) -> Result<(), DecodeError> {
163    let mut cursor = Cursor::new(data);
164    let name_len = leb128::decode_u32(&mut cursor).map_err(|mut e| {
165        e.context = DecodeContext::SectionBody { id: 0 };
166        e.offset = ByteOffset(base_offset + e.offset.0);
167        e
168    })? as usize;
169
170    let name_offset = cursor.position();
171    let name = cursor.read_bytes(name_len).map_err(|_| DecodeError {
172        offset: ByteOffset(base_offset + name_offset),
173        context: DecodeContext::SectionBody { id: 0 },
174        kind: DecodeErrorKind::UnexpectedEof,
175    })?;
176
177    core::str::from_utf8(name).map_err(|_| DecodeError {
178        offset: ByteOffset(base_offset + name_offset),
179        context: DecodeContext::SectionBody { id: 0 },
180        kind: DecodeErrorKind::InvalidUtf8,
181    })?;
182
183    Ok(())
184}
185
186pub fn parse_sections<'a>(cursor: &mut Cursor<'a>) -> Result<Vec<RawSection<'a>>, DecodeError> {
187    let mut sections = Vec::new();
188    let mut last_non_custom: Option<(u8, u8)> = None;
189
190    while !cursor.is_empty() {
191        let id_offset = cursor.position();
192        let id_byte = cursor.read_byte().map_err(|_| DecodeError {
193            offset: ByteOffset(id_offset),
194            context: DecodeContext::SectionHeader,
195            kind: DecodeErrorKind::UnexpectedEof,
196        })?;
197
198        let id = SectionId::from_byte(id_byte).ok_or(DecodeError {
199            offset: ByteOffset(id_offset),
200            context: DecodeContext::SectionHeader,
201            kind: DecodeErrorKind::UnknownSectionId { id: id_byte },
202        })?;
203
204        let size = leb128::decode_u32(cursor).map_err(|mut e| {
205            e.context = DecodeContext::SectionHeader;
206            e
207        })?;
208
209        let content_offset = cursor.position();
210
211        if content_offset + size as usize > cursor.position() + cursor.remaining().len() {
212            return Err(DecodeError {
213                offset: ByteOffset(id_offset),
214                context: DecodeContext::SectionHeader,
215                kind: DecodeErrorKind::SectionOverflow,
216            });
217        }
218
219        // Ordering check: non-custom sections must appear in spec order,
220        // with special handling for the data count section, and no duplicates.
221        if id != SectionId::Custom {
222            let current_order = section_order(id);
223            if let Some((prev_id, _prev_order)) = last_non_custom {
224                if id_byte == prev_id {
225                    return Err(DecodeError {
226                        offset: ByteOffset(id_offset),
227                        context: DecodeContext::SectionHeader,
228                        kind: DecodeErrorKind::DuplicateSection { id: id_byte },
229                    });
230                }
231                if !sections_in_valid_order(
232                    SectionId::from_byte(prev_id).expect("known section id"),
233                    id,
234                ) {
235                    return Err(DecodeError {
236                        offset: ByteOffset(id_offset),
237                        context: DecodeContext::SectionHeader,
238                        kind: DecodeErrorKind::SectionOutOfOrder {
239                            prev: prev_id,
240                            current: id_byte,
241                        },
242                    });
243                }
244            }
245            last_non_custom = Some((id_byte, current_order));
246        }
247
248        let data = cursor.read_bytes(size as usize).map_err(|_| DecodeError {
249            offset: ByteOffset(content_offset),
250            context: DecodeContext::SectionBody { id: id_byte },
251            kind: DecodeErrorKind::SectionOverflow,
252        })?;
253
254        if id == SectionId::Custom {
255            validate_custom_section_name(data, content_offset)?;
256        }
257
258        sections.push(RawSection {
259            id,
260            offset: content_offset,
261            data,
262        });
263    }
264
265    Ok(sections)
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    /// Minimal valid WASM module: just the 8-byte header.
273    const MINIMAL_MODULE: [u8; 8] = [
274        0x00, 0x61, 0x73, 0x6D, // \0asm
275        0x01, 0x00, 0x00, 0x00, // version 1
276    ];
277
278    #[test]
279    fn parse_minimal_module_preamble() {
280        let mut cursor = Cursor::new(&MINIMAL_MODULE);
281        parse_preamble(&mut cursor).unwrap();
282        assert!(cursor.is_empty());
283    }
284
285    #[test]
286    fn reject_bad_magic() {
287        let data = [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
288        let mut cursor = Cursor::new(&data);
289        let err = parse_preamble(&mut cursor).unwrap_err();
290        assert_eq!(err.kind, DecodeErrorKind::InvalidMagic);
291    }
292
293    #[test]
294    fn reject_bad_version() {
295        let data = [0x00, 0x61, 0x73, 0x6D, 0x02, 0x00, 0x00, 0x00];
296        let mut cursor = Cursor::new(&data);
297        let err = parse_preamble(&mut cursor).unwrap_err();
298        assert!(matches!(
299            err.kind,
300            DecodeErrorKind::UnsupportedVersion { found: 2 }
301        ));
302    }
303
304    #[test]
305    fn parse_empty_sections() {
306        let mut cursor = Cursor::new(&MINIMAL_MODULE);
307        parse_preamble(&mut cursor).unwrap();
308        let sections = parse_sections(&mut cursor).unwrap();
309        assert!(sections.is_empty());
310    }
311
312    #[test]
313    fn parse_single_type_section() {
314        // Header + type section (id=1) with 2 bytes of content
315        let data = [
316            0x00, 0x61, 0x73, 0x6D, // magic
317            0x01, 0x00, 0x00, 0x00, // version
318            0x01, // section id: type
319            0x02, // section size: 2 bytes
320            0xAA, 0xBB, // section content
321        ];
322        let mut cursor = Cursor::new(&data);
323        parse_preamble(&mut cursor).unwrap();
324        let sections = parse_sections(&mut cursor).unwrap();
325
326        assert_eq!(sections.len(), 1);
327        assert_eq!(sections[0].id, SectionId::Type);
328        assert_eq!(sections[0].data, &[0xAA, 0xBB]);
329    }
330
331    #[test]
332    fn parse_multiple_sections_in_order() {
333        let data = [
334            0x00, 0x61, 0x73, 0x6D, // magic
335            0x01, 0x00, 0x00, 0x00, // version
336            0x01, 0x01, 0xFF, // type section (1 byte)
337            0x03, 0x01, 0xEE, // function section (1 byte)
338            0x07, 0x01, 0xDD, // export section (1 byte)
339        ];
340        let mut cursor = Cursor::new(&data);
341        parse_preamble(&mut cursor).unwrap();
342        let sections = parse_sections(&mut cursor).unwrap();
343
344        assert_eq!(sections.len(), 3);
345        assert_eq!(sections[0].id, SectionId::Type);
346        assert_eq!(sections[1].id, SectionId::Function);
347        assert_eq!(sections[2].id, SectionId::Export);
348    }
349
350    #[test]
351    fn reject_duplicate_section() {
352        let data = [
353            0x00, 0x61, 0x73, 0x6D, // magic
354            0x01, 0x00, 0x00, 0x00, // version
355            0x01, 0x01, 0xFF, // type section
356            0x01, 0x01, 0xEE, // duplicate type section
357        ];
358        let mut cursor = Cursor::new(&data);
359        parse_preamble(&mut cursor).unwrap();
360        let err = parse_sections(&mut cursor).unwrap_err();
361        assert!(matches!(
362            err.kind,
363            DecodeErrorKind::DuplicateSection { id: 1 }
364        ));
365    }
366
367    #[test]
368    fn reject_out_of_order_sections() {
369        let data = [
370            0x00, 0x61, 0x73, 0x6D, // magic
371            0x01, 0x00, 0x00, 0x00, // version
372            0x03, 0x01, 0xFF, // function section (id=3)
373            0x01, 0x01, 0xEE, // type section (id=1) — out of order
374        ];
375        let mut cursor = Cursor::new(&data);
376        parse_preamble(&mut cursor).unwrap();
377        let err = parse_sections(&mut cursor).unwrap_err();
378        assert!(matches!(
379            err.kind,
380            DecodeErrorKind::SectionOutOfOrder {
381                prev: 3,
382                current: 1
383            }
384        ));
385    }
386
387    #[test]
388    fn allow_data_count_before_code_and_data() {
389        let data = [
390            0x00, 0x61, 0x73, 0x6D, // magic
391            0x01, 0x00, 0x00, 0x00, // version
392            0x09, 0x01, 0xAA, // element section
393            0x0C, 0x01, 0xBB, // data count section
394            0x0A, 0x01, 0xCC, // code section
395            0x0B, 0x01, 0xDD, // data section
396        ];
397        let mut cursor = Cursor::new(&data);
398        parse_preamble(&mut cursor).unwrap();
399        let sections = parse_sections(&mut cursor).unwrap();
400
401        assert_eq!(sections.len(), 4);
402        assert_eq!(sections[0].id, SectionId::Element);
403        assert_eq!(sections[1].id, SectionId::DataCount);
404        assert_eq!(sections[2].id, SectionId::Code);
405        assert_eq!(sections[3].id, SectionId::Data);
406    }
407
408    #[test]
409    fn custom_sections_allowed_anywhere() {
410        let data = [
411            0x00, 0x61, 0x73, 0x6D, // magic
412            0x01, 0x00, 0x00, 0x00, // version
413            0x00, 0x01, 0x00, // custom section with empty name
414            0x01, 0x01, 0xAA, // type section
415            0x00, 0x01, 0x00, // another custom section with empty name
416            0x03, 0x01, 0xCC, // function section
417            0x00, 0x01, 0x00, // yet another custom section with empty name
418        ];
419        let mut cursor = Cursor::new(&data);
420        parse_preamble(&mut cursor).unwrap();
421        let sections = parse_sections(&mut cursor).unwrap();
422
423        assert_eq!(sections.len(), 5);
424        assert_eq!(sections[0].id, SectionId::Custom);
425        assert_eq!(sections[1].id, SectionId::Type);
426        assert_eq!(sections[2].id, SectionId::Custom);
427        assert_eq!(sections[3].id, SectionId::Function);
428        assert_eq!(sections[4].id, SectionId::Custom);
429    }
430
431    #[test]
432    fn reject_section_overflow() {
433        let data = [
434            0x00, 0x61, 0x73, 0x6D, // magic
435            0x01, 0x00, 0x00, 0x00, // version
436            0x01, 0xFF, 0x01, // type section claiming 255 bytes, but none follow
437        ];
438        let mut cursor = Cursor::new(&data);
439        parse_preamble(&mut cursor).unwrap();
440        let err = parse_sections(&mut cursor).unwrap_err();
441        assert!(matches!(err.kind, DecodeErrorKind::SectionOverflow));
442    }
443
444    #[test]
445    fn reject_invalid_utf8_custom_section_name() {
446        let data = [
447            0x00, 0x61, 0x73, 0x6D, // magic
448            0x01, 0x00, 0x00, 0x00, // version
449            0x00, 0x02, 0x01, 0x80, // custom section with 1-byte invalid UTF-8 name
450        ];
451        let mut cursor = Cursor::new(&data);
452        parse_preamble(&mut cursor).unwrap();
453        let err = parse_sections(&mut cursor).unwrap_err();
454        assert!(matches!(err.kind, DecodeErrorKind::InvalidUtf8));
455        assert!(matches!(err.context, DecodeContext::SectionBody { id: 0 }));
456    }
457}