1use anyhow::{Context, anyhow};
2use camino::Utf8Path;
3use wasm_encoder::reencode::{Error, Reencode, ReencodeComponent};
4
5pub const SLOT_MAGIC: &[u8; 16] = b"WASM_RQJS_SLOT\x01\x00";
7
8pub const SLOT_END_MAGIC: &[u8; 16] = b"WASM_RQJS_SLTND\x00";
10
11const MARKER_SIZE: usize = 40;
15
16const WASM_PAGE_SIZE: u32 = 65536;
17
18pub fn create_marker_file(module_index: u32) -> Vec<u8> {
29 let mut data = Vec::with_capacity(MARKER_SIZE);
30 data.extend_from_slice(SLOT_MAGIC);
31 data.extend_from_slice(&module_index.to_le_bytes());
32 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(SLOT_END_MAGIC);
34 data
35}
36
37pub fn inject_js_into_component(
51 input: &Utf8Path,
52 output: &Utf8Path,
53 js_sources: &[&str],
54) -> anyhow::Result<()> {
55 let wasm_bytes = std::fs::read(input.as_std_path())
56 .with_context(|| format!("Failed to read input component: {input}"))?;
57
58 let patched = inject_js_into_bytes(&wasm_bytes, js_sources)?;
59
60 std::fs::write(output.as_std_path(), &patched)
61 .with_context(|| format!("Failed to write output component: {output}"))?;
62
63 Ok(())
64}
65
66pub fn inject_js_into_bytes(wasm_bytes: &[u8], js_sources: &[&str]) -> anyhow::Result<Vec<u8>> {
70 if js_sources.is_empty() {
71 return Err(anyhow!("No JS sources provided for injection"));
72 }
73
74 let js_payloads: Vec<Vec<u8>> = js_sources
76 .iter()
77 .map(|src| {
78 let js_bytes = src.as_bytes();
79 let mut payload = Vec::with_capacity(4 + js_bytes.len());
80 payload.extend_from_slice(&(js_bytes.len() as u32).to_le_bytes());
81 payload.extend_from_slice(js_bytes);
82 payload
83 })
84 .collect();
85
86 let total_payload_size: usize = js_payloads.iter().map(|p| p.len()).sum();
87
88 let mut rewriter = MarkerRewriter {
89 js_payloads,
90 total_payload_size,
91 markers_found: Vec::new(),
92 max_data_end: 0,
93 js_mem_offsets: Vec::new(),
94 original_memory_min: 0,
95 };
96
97 let parser = wasmparser_encoder::Parser::new(0);
98 let mut component = wasm_encoder::Component::new();
99 rewriter
100 .parse_component(&mut component, parser, wasm_bytes)
101 .map_err(|e| match e {
102 Error::UserError(e) => e,
103 Error::ParseError(e) => anyhow!("Failed to parse WASM component: {e}"),
104 other => anyhow!("Failed to reencode WASM component: {other}"),
105 })?;
106
107 if rewriter.markers_found.is_empty() {
108 return Err(anyhow!(
109 "No JS injection markers found in the WASM component. \
110 Was it compiled with EmbeddingMode::BinarySlot?"
111 ));
112 }
113
114 for i in 0..js_sources.len() as u32 {
116 if !rewriter.markers_found.contains(&i) {
117 return Err(anyhow!(
118 "JS injection marker with MODULE_INDEX={i} not found in the WASM component. \
119 Expected {expected} markers but only found: {found:?}",
120 expected = js_sources.len(),
121 found = rewriter.markers_found,
122 ));
123 }
124 }
125
126 let mut output = component.finish();
127
128 patch_js_offsets_in_output(&mut output, &rewriter.js_mem_offsets)?;
130
131 Ok(output)
132}
133
134fn is_marker_at(data: &[u8], offset: usize) -> bool {
137 offset + MARKER_SIZE <= data.len()
138 && &data[offset..offset + 16] == SLOT_MAGIC
139 && &data[offset + 24..offset + MARKER_SIZE] == SLOT_END_MAGIC
140}
141
142fn marker_module_index(data: &[u8], offset: usize) -> u32 {
144 u32::from_le_bytes(data[offset + 16..offset + 20].try_into().unwrap())
145}
146
147fn marker_js_offset(data: &[u8], offset: usize) -> u32 {
149 u32::from_le_bytes(data[offset + 20..offset + 24].try_into().unwrap())
150}
151
152fn find_marker_in_data(data: &[u8]) -> Option<usize> {
154 if data.len() < MARKER_SIZE {
155 return None;
156 }
157 (0..=data.len() - MARKER_SIZE).find(|&i| is_marker_at(data, i))
158}
159
160struct MarkerRewriter {
161 js_payloads: Vec<Vec<u8>>,
163 total_payload_size: usize,
165 markers_found: Vec<u32>,
167 max_data_end: u32,
169 js_mem_offsets: Vec<(u32, u32)>,
171 original_memory_min: u32,
173}
174
175impl Reencode for MarkerRewriter {
176 type Error = anyhow::Error;
177
178 fn parse_data(
179 &mut self,
180 data: &mut wasm_encoder::DataSection,
181 datum: wasmparser_encoder::Data<'_>,
182 ) -> Result<(), Error<Self::Error>> {
183 if let wasmparser_encoder::DataKind::Active {
185 memory_index: 0,
186 offset_expr,
187 } = &datum.kind
188 && let Some(offset) = eval_const_i32(offset_expr)
189 {
190 let end = offset.saturating_add(datum.data.len() as u32);
191 self.max_data_end = self.max_data_end.max(end);
192 }
193
194 if let Some(marker_offset) = find_marker_in_data(datum.data) {
196 let module_index = marker_module_index(datum.data, marker_offset);
197 if self.markers_found.contains(&module_index) {
198 return Err(Error::UserError(anyhow!(
199 "Found duplicate JS injection marker with MODULE_INDEX={module_index}"
200 )));
201 }
202 self.markers_found.push(module_index);
203 }
204
205 wasm_encoder::reencode::utils::parse_data(self, data, datum)
208 }
209
210 fn parse_data_section(
211 &mut self,
212 data: &mut wasm_encoder::DataSection,
213 section: wasmparser_encoder::DataSectionReader<'_>,
214 ) -> Result<(), Error<Self::Error>> {
215 wasm_encoder::reencode::utils::parse_data_section(self, data, section)?;
217
218 let mut current_offset =
233 page_align(self.max_data_end).max(self.original_memory_min * WASM_PAGE_SIZE);
234 let mut sorted_indices = self.markers_found.clone();
235 sorted_indices.sort();
236
237 for module_index in sorted_indices {
238 if let Some(payload) = self.js_payloads.get(module_index as usize) {
239 let offset_expr = wasm_encoder::ConstExpr::i32_const(current_offset as i32);
240 data.active(0, &offset_expr, payload.iter().copied());
241 self.js_mem_offsets.push((module_index, current_offset));
242 current_offset = page_align(current_offset + payload.len() as u32);
243 }
244 }
245
246 Ok(())
247 }
248
249 fn data_count(&mut self, count: u32) -> Result<u32, Error<Self::Error>> {
250 Ok(count + self.js_payloads.len() as u32)
252 }
253
254 fn parse_memory_section(
255 &mut self,
256 memories: &mut wasm_encoder::MemorySection,
257 section: wasmparser_encoder::MemorySectionReader<'_>,
258 ) -> Result<(), Error<Self::Error>> {
259 for memory in section {
260 let memory = memory.map_err(Error::ParseError)?;
261
262 self.original_memory_min = memory.initial as u32;
263
264 let max_padding = self.js_payloads.len() as u32 * WASM_PAGE_SIZE;
267 let js_end_upper = self.original_memory_min * WASM_PAGE_SIZE
268 + self.total_payload_size as u32
269 + max_padding;
270 let pages_needed = js_end_upper.div_ceil(WASM_PAGE_SIZE);
271 let new_min = pages_needed.max(memory.initial as u32);
272 let new_max = memory.maximum.map(|m| m.max(new_min as u64));
273
274 memories.memory(wasm_encoder::MemoryType {
275 minimum: new_min as u64,
276 maximum: new_max,
277 memory64: memory.memory64,
278 shared: memory.shared,
279 page_size_log2: memory.page_size_log2,
280 });
281 }
282 Ok(())
283 }
284}
285
286impl ReencodeComponent for MarkerRewriter {}
287
288fn eval_const_i32(expr: &wasmparser_encoder::ConstExpr<'_>) -> Option<u32> {
290 let mut reader = expr.get_operators_reader();
291 if let Ok(wasmparser_encoder::Operator::I32Const { value }) = reader.read() {
292 return Some(value as u32);
293 }
294 None
295}
296
297fn page_align(addr: u32) -> u32 {
298 (addr + WASM_PAGE_SIZE - 1) & !(WASM_PAGE_SIZE - 1)
299}
300
301fn patch_js_offsets_in_output(output: &mut [u8], offsets: &[(u32, u32)]) -> anyhow::Result<()> {
304 let mut marker_positions: Vec<(usize, u32)> = Vec::new();
306 for i in 0..output.len().saturating_sub(MARKER_SIZE) {
307 if is_marker_at(output, i) {
308 let module_idx = marker_module_index(output, i);
309 let js_off = marker_js_offset(output, i);
310 if js_off == 0 {
312 marker_positions.push((i, module_idx));
313 }
314 }
315 }
316
317 for &(module_index, js_mem_offset) in offsets {
318 let pos = marker_positions
319 .iter()
320 .find(|(_, idx)| *idx == module_index)
321 .map(|(pos, _)| *pos)
322 .ok_or_else(|| {
323 anyhow!(
324 "Could not find unpatched marker with MODULE_INDEX={module_index} \
325 in reencoded output"
326 )
327 })?;
328 output[pos + 20..pos + 24].copy_from_slice(&js_mem_offset.to_le_bytes());
330 }
331
332 Ok(())
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_create_marker_file() {
341 let marker = create_marker_file(0);
342 assert_eq!(marker.len(), MARKER_SIZE);
343 assert_eq!(&marker[..16], SLOT_MAGIC.as_slice());
344 assert_eq!(u32::from_le_bytes(marker[16..20].try_into().unwrap()), 0); assert_eq!(u32::from_le_bytes(marker[20..24].try_into().unwrap()), 0); assert_eq!(&marker[24..], SLOT_END_MAGIC.as_slice());
347
348 let marker1 = create_marker_file(1);
349 assert_eq!(u32::from_le_bytes(marker1[16..20].try_into().unwrap()), 1);
350 assert_eq!(u32::from_le_bytes(marker1[20..24].try_into().unwrap()), 0);
351 }
352
353 #[test]
354 fn test_find_marker_in_data() {
355 let marker = create_marker_file(0);
356 assert_eq!(find_marker_in_data(&marker), Some(0));
357
358 let mut data = vec![0xAA; 100];
360 data.extend_from_slice(&marker);
361 data.extend_from_slice(&[0xBB; 50]);
362 assert_eq!(find_marker_in_data(&data), Some(100));
363
364 assert_eq!(find_marker_in_data(&[0u8; 100]), None);
366 assert_eq!(find_marker_in_data(&[0u8; 10]), None);
367 }
368
369 #[test]
370 fn test_inject_no_marker() {
371 let component = wasm_encoder::Component::new();
372 let bytes = component.finish();
373 let result = inject_js_into_bytes(&bytes, &["x"]);
374 assert!(result.is_err());
375 assert!(
376 result
377 .unwrap_err()
378 .to_string()
379 .contains("No JS injection markers found")
380 );
381 }
382
383 #[test]
384 fn test_page_align() {
385 assert_eq!(page_align(0), 0);
386 assert_eq!(page_align(1), WASM_PAGE_SIZE);
387 assert_eq!(page_align(WASM_PAGE_SIZE), WASM_PAGE_SIZE);
388 assert_eq!(page_align(WASM_PAGE_SIZE + 1), 2 * WASM_PAGE_SIZE);
389 }
390}