Skip to main content

navi_plugin_runtime/
component.rs

1/// WASM Component Model detection and dispatch layer.
2///
3/// A WASM component can be detected by checking for the magic prefix `\\0asm`
4/// followed by version 1 and type section markers. Raw modules are flat core
5/// wasm with the `run_tool` export pattern used by the pre-component path.
6/// Classification of a WASM binary: either a raw core module (flat ABI) or a
7/// Component Model module (component section present).
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ComponentKind {
10    /// Raw core WASM module using the flat `run_tool(name_ptr,name_len,input_ptr,input_len) -> i32` ABI.
11    Raw,
12    /// WASM Component Model module with a `navi-plugin` component section.
13    Component,
14}
15
16/// Detect whether a WASM binary is a Component Model module or a raw core
17/// module by inspecting its magic prefix and component section.
18///
19/// Component Model modules begin with the standard WASM magic (`\0asm`) and
20/// version 1 header, then contain a component-section (ID = 3 for
21/// core modules, but components use section ID 0x0b = 3 for the
22/// `component` section after the core module envelope). We detect the
23/// component section by checking for the `0x0b` (component section) byte
24/// at an appropriate position after the header.
25///
26/// If the binary is too short to contain a valid header, we return `Raw`
27/// (the safest default).
28pub fn detect_component_kind(wasm_bytes: &[u8]) -> ComponentKind {
29    // Minimum for any valid WASM: 8-byte header.
30    if wasm_bytes.len() < 8 {
31        return ComponentKind::Raw;
32    }
33
34    // Check magic: 0x00 0x61 0x73 0x6d (`\0asm`)
35    if wasm_bytes[0..4] != [0x00, 0x61, 0x73, 0x6d] {
36        return ComponentKind::Raw;
37    }
38
39    // Version must be 1 (0x01 0x00 0x00 0x00).
40    if wasm_bytes[4..8] != [0x01, 0x00, 0x00, 0x00] {
41        return ComponentKind::Raw;
42    }
43
44    // Component Model: after the core WASM envelope, a component module has
45    // a component section with ID = 0x0b. We scan for a section with ID 0x0b.
46    //
47    // However, the simpler heuristic used in practice is to look for the
48    // `producers` custom section or the component start. Since both are rare
49    // in raw modules, a more reliable approach is to check for the first
50    // occurrence of the `0x0b` byte after the WASM header as a section ID.
51    //
52    // The WASM spec section IDs:
53    //   1 = Type, 2 = Import, 3 = Function, 4 = Table, 5 = Memory,
54    //   6 = Global, 7 = Export, 8 = Start, 9 = Element, 10 = Code,
55    //   11 = Data, 12 = DataCount, 13 = Tag, 0x0b = Component (Component Model)
56    //
57    // For a component, the file begins with a core module envelope followed by
58    // additional sections including the component section. Since raw modules
59    // will never have a section with ID 0x0b (reserved for components), we
60    // scan the section headers after the header.
61
62    let mut pos = 8; // skip header
63    while pos + 1 < wasm_bytes.len() {
64        let section_id = wasm_bytes[pos];
65        pos += 1;
66
67        // Section length is a LEB128-encoded unsigned integer.
68        let (size, end) = match decode_leb128(wasm_bytes, pos) {
69            Some(v) => v,
70            None => break, // malformed — treat as raw
71        };
72        pos = end;
73
74        if section_id == 0x0b {
75            return ComponentKind::Component;
76        }
77
78        // Skip past the section body.
79        if pos + size as usize > wasm_bytes.len() {
80            break;
81        }
82        pos += size as usize;
83    }
84
85    ComponentKind::Raw
86}
87
88/// Decode a WASM LEB128 unsigned integer starting at `pos`.
89/// Returns `(value, new_pos)` or `None` if the input is too short.
90fn decode_leb128(bytes: &[u8], pos: usize) -> Option<(u64, usize)> {
91    let mut result: u64 = 0;
92    let mut shift: u32 = 0;
93    let mut current = pos;
94    loop {
95        if current >= bytes.len() {
96            return None;
97        }
98        let byte = bytes[current];
99        result |= ((byte & 0x7f) as u64) << shift;
100        current += 1;
101        if byte & 0x80 == 0 {
102            return Some((result, current));
103        }
104        shift += 7;
105        if shift > 63 {
106            return None; // overflow
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn raw_wasm_header() -> Vec<u8> {
116        vec![
117            0x00, 0x61, 0x73, 0x6d, // magic
118            0x01, 0x00, 0x00, 0x00, // version 1
119        ]
120    }
121
122    #[test]
123    fn detect_empty_is_raw() {
124        assert_eq!(detect_component_kind(&[]), ComponentKind::Raw);
125    }
126
127    #[test]
128    fn detect_too_short_is_raw() {
129        assert_eq!(detect_component_kind(&[0x00, 0x61]), ComponentKind::Raw);
130    }
131
132    #[test]
133    fn detect_header_only_is_raw() {
134        assert_eq!(
135            detect_component_kind(&raw_wasm_header()),
136            ComponentKind::Raw
137        );
138    }
139
140    #[test]
141    fn detect_header_with_code_section_is_raw() {
142        let mut bytes = raw_wasm_header();
143        // Section 10 (Code), 2 bytes of body: 0x01 0x00 (empty function)
144        bytes.extend_from_slice(&[10, 2, 1, 0]);
145        assert_eq!(detect_component_kind(&bytes), ComponentKind::Raw);
146    }
147
148    #[test]
149    fn detect_header_with_component_section() {
150        let mut bytes = raw_wasm_header();
151        // Section 0x0b (Component), 2 bytes of body: 0x00 0x00
152        bytes.extend_from_slice(&[0x0b, 2, 0x00, 0x00]);
153        assert_eq!(detect_component_kind(&bytes), ComponentKind::Component);
154    }
155
156    #[test]
157    fn detect_malformed_leb128_treated_as_raw() {
158        let mut bytes = raw_wasm_header();
159        // Section 10 with a truncated LEB128 length
160        bytes.extend_from_slice(&[10, 0xff, 0xff]);
161        assert_eq!(detect_component_kind(&bytes), ComponentKind::Raw);
162    }
163
164    #[test]
165    fn detect_section_body_overflows_treated_as_raw() {
166        let mut bytes = raw_wasm_header();
167        // Section 10 claiming length 0xff (255) but only 1 byte follows
168        bytes.extend_from_slice(&[10, 0xff, 0x42]);
169        assert_eq!(detect_component_kind(&bytes), ComponentKind::Raw);
170    }
171
172    #[test]
173    fn decode_leb128_single_byte() {
174        let bytes = [0x7f];
175        let (val, pos) = decode_leb128(&bytes, 0).unwrap();
176        assert_eq!(val, 0x7f);
177        assert_eq!(pos, 1);
178    }
179
180    #[test]
181    fn decode_leb128_two_bytes() {
182        let bytes = [0x80, 0x01];
183        let (val, pos) = decode_leb128(&bytes, 0).unwrap();
184        assert_eq!(val, 128);
185        assert_eq!(pos, 2);
186    }
187
188    #[test]
189    fn decode_leb128_truncated_returns_none() {
190        let bytes = [0x80];
191        assert!(decode_leb128(&bytes, 0).is_none());
192    }
193}