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}