1use std::cell::RefCell;
14use std::collections::HashMap;
15use std::rc::Rc;
16use std::time::Instant;
17
18use anyhow::anyhow;
19use rquickjs::function::Func;
20use rquickjs::{ArrayBuffer, Ctx, Value};
21use serde::Serialize;
22use tracing::debug;
23use wasmtime::{
24 Caller, Engine, ExternType, Instance as WasmInstance, Linker, Module as WasmModule, Store, Val,
25 ValType,
26};
27
28struct WasmHostData {
34 max_memory_pages: u64,
36 staged_files: HashMap<String, std::sync::Arc<Vec<u8>>>,
38 open_files: HashMap<u32, VirtualFileHandle>,
40 next_fd: u32,
42 started_at: Instant,
44}
45
46struct InstanceState {
48 store: Store<WasmHostData>,
49 instance: WasmInstance,
50}
51
52#[derive(Clone)]
53struct VirtualFileHandle {
54 path: String,
55 position: usize,
56 readable: bool,
57 writable: bool,
58 append: bool,
59}
60
61#[derive(Serialize)]
62struct WasmExportEntry {
63 name: String,
64 kind: &'static str,
65}
66
67pub(crate) struct WasmBridgeState {
69 engine: Engine,
70 modules: HashMap<u32, WasmModule>,
71 instances: HashMap<u32, InstanceState>,
72 staged_files: HashMap<String, std::sync::Arc<Vec<u8>>>,
73 next_id: u32,
74 max_modules: usize,
75 max_instances: usize,
76}
77
78impl WasmBridgeState {
79 pub fn new() -> Self {
80 let engine = Engine::default();
81 Self {
82 engine,
83 modules: HashMap::new(),
84 instances: HashMap::new(),
85 staged_files: HashMap::new(),
86 next_id: 1,
87 max_modules: DEFAULT_MAX_MODULES,
88 max_instances: DEFAULT_MAX_INSTANCES,
89 }
90 }
91
92 fn alloc_id(&mut self) -> Result<u32, String> {
93 let start = match self.next_id {
94 0 => 1,
95 id if id > MAX_JS_WASM_ID => 1,
96 id => id,
97 };
98 let mut candidate = start;
99
100 loop {
101 if !self.modules.contains_key(&candidate) && !self.instances.contains_key(&candidate) {
102 self.next_id = candidate.wrapping_add(1);
103 if self.next_id == 0 || self.next_id > MAX_JS_WASM_ID {
104 self.next_id = 1;
105 }
106 return Ok(candidate);
107 }
108
109 candidate = candidate.wrapping_add(1);
110 if candidate == 0 || candidate > MAX_JS_WASM_ID {
111 candidate = 1;
112 }
113 if candidate == start {
114 return Err("WASM instance/module id space exhausted".to_string());
115 }
116 }
117 }
118
119 #[cfg(test)]
120 fn set_limits_for_test(&mut self, max_modules: usize, max_instances: usize) {
121 self.max_modules = max_modules.max(1);
122 self.max_instances = max_instances.max(1);
123 }
124}
125
126const ERRNO_BADF: i32 = 8;
127const ERRNO_EXIST: i32 = 20;
128const ERRNO_FBIG: i32 = 27;
129const ERRNO_INVAL: i32 = 28;
130const ERRNO_NOENT: i32 = 44;
131
132const O_ACCMODE: i32 = 0o3;
133const O_WRONLY: i32 = 0o1;
134const O_RDWR: i32 = 0o2;
135const O_CREAT: i32 = 0o100;
136const O_EXCL: i32 = 0o200;
137const O_TRUNC: i32 = 0o1000;
138const O_APPEND: i32 = 0o2000;
139
140const MAX_VIRTUAL_FILE_BYTES: usize = 64 * 1024 * 1024;
142
143const fn descriptor_access(flags: i32) -> Option<(bool, bool)> {
144 match flags & O_ACCMODE {
145 0 => Some((true, false)),
146 O_WRONLY => Some((false, true)),
147 O_RDWR => Some((true, true)),
148 _ => None,
149 }
150}
151
152fn throw_wasm(ctx: &Ctx<'_>, class: &str, msg: &str) -> rquickjs::Error {
157 let text = format!("{class}: {msg}");
158 if let Ok(js_text) = rquickjs::String::from_str(ctx.clone(), &text) {
159 let _ = ctx.throw(js_text.into_value());
160 }
161 rquickjs::Error::Exception
162}
163
164fn extract_bytes(ctx: &Ctx<'_>, value: &Value<'_>) -> rquickjs::Result<Vec<u8>> {
169 if let Some(obj) = value.as_object() {
171 if let Some(ab) = obj.as_array_buffer() {
172 return ab
173 .as_bytes()
174 .map(<[u8]>::to_vec)
175 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Detached ArrayBuffer"));
176 }
177 if let Some(typed) = obj.as_typed_array::<u8>() {
178 return typed
179 .as_bytes()
180 .map(<[u8]>::to_vec)
181 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Detached TypedArray"));
182 }
183 }
184 if let Some(arr) = value.as_array() {
186 let mut bytes = Vec::with_capacity(arr.len().min(1024 * 1024));
187 for i in 0..arr.len() {
188 let v: i32 = arr.get(i)?;
189 bytes.push(
190 u8::try_from(v)
191 .map_err(|_| throw_wasm(ctx, "TypeError", "Byte value out of range"))?,
192 );
193 }
194 return Ok(bytes);
195 }
196 Err(throw_wasm(
197 ctx,
198 "TypeError",
199 "Expected ArrayBuffer or byte array",
200 ))
201}
202
203#[allow(clippy::cast_precision_loss)]
206fn val_to_f64(ctx: &Ctx<'_>, val: &Val) -> rquickjs::Result<f64> {
207 match val {
208 Val::I32(v) => Ok(f64::from(*v)),
209 Val::F32(bits) => Ok(f64::from(f32::from_bits(*bits))),
210 Val::F64(bits) => Ok(f64::from_bits(*bits)),
211 _ => Err(throw_wasm(
212 ctx,
213 "RuntimeError",
214 "Unsupported WASM return value type for PiJS bridge",
215 )),
216 }
217}
218
219#[allow(clippy::cast_possible_truncation)]
221fn js_to_i32(value: f64) -> i32 {
222 if !value.is_finite() || value == 0.0 {
223 return 0;
224 }
225
226 let mut wrapped = value.trunc() % TWO_POW_32;
227 if wrapped < 0.0 {
228 wrapped += TWO_POW_32;
229 }
230
231 if wrapped >= TWO_POW_31 {
232 (wrapped - TWO_POW_32) as i32
233 } else {
234 wrapped as i32
235 }
236}
237
238#[allow(clippy::cast_possible_truncation)]
239fn js_to_val(ctx: &Ctx<'_>, value: &Value<'_>, ty: &ValType) -> rquickjs::Result<Val> {
240 match ty {
241 ValType::I32 => {
242 let v: f64 = value
243 .as_number()
244 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for i32"))?;
245 Ok(Val::I32(js_to_i32(v)))
246 }
247 ValType::I64 => Err(throw_wasm(
248 ctx,
249 "TypeError",
250 "i64 parameters are not supported by PiJS WebAssembly bridge",
251 )),
252 ValType::F32 => {
253 let v: f64 = value
254 .as_number()
255 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f32"))?;
256 #[expect(clippy::cast_possible_truncation)]
257 Ok(Val::F32((v as f32).to_bits()))
258 }
259 ValType::F64 => {
260 let v: f64 = value
261 .as_number()
262 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f64"))?;
263 Ok(Val::F64(v.to_bits()))
264 }
265 _ => Err(throw_wasm(ctx, "TypeError", "Unsupported WASM value type")),
266 }
267}
268
269fn validate_call_result_types(ctx: &Ctx<'_>, result_types: &[ValType]) -> rquickjs::Result<()> {
270 if result_types.len() > 1 {
271 return Err(throw_wasm(
272 ctx,
273 "RuntimeError",
274 "Multi-value WASM results are not supported by PiJS WebAssembly bridge",
275 ));
276 }
277
278 if let Some(ty) = result_types.first() {
279 return match ty {
280 ValType::I32 | ValType::F32 | ValType::F64 => Ok(()),
281 ValType::I64 => Err(throw_wasm(
282 ctx,
283 "RuntimeError",
284 "i64 results are not supported by PiJS WebAssembly bridge",
285 )),
286 _ => Err(throw_wasm(
287 ctx,
288 "RuntimeError",
289 "Unsupported WASM return type for PiJS WebAssembly bridge",
290 )),
291 };
292 }
293
294 Ok(())
295}
296
297fn instance_memory(inst: &mut InstanceState, mem_name: &str) -> anyhow::Result<wasmtime::Memory> {
302 inst.instance
303 .get_memory(&mut inst.store, mem_name)
304 .ok_or_else(|| anyhow!("Memory '{mem_name}' not found"))
305}
306
307fn caller_memory(caller: &mut Caller<'_, WasmHostData>) -> anyhow::Result<wasmtime::Memory> {
308 caller
309 .get_export("memory")
310 .and_then(wasmtime::Extern::into_memory)
311 .ok_or_else(|| anyhow!("Exported memory 'memory' not found"))
312}
313
314fn checked_memory_range(
315 offset: usize,
316 len: usize,
317 memory_len: usize,
318) -> anyhow::Result<std::ops::Range<usize>> {
319 let end = offset
320 .checked_add(len)
321 .ok_or_else(|| anyhow!("Memory access overflow"))?;
322 if end > memory_len {
323 return Err(anyhow!("Memory access out of bounds"));
324 }
325 Ok(offset..end)
326}
327
328fn caller_read_bytes(
329 caller: &mut Caller<'_, WasmHostData>,
330 offset: usize,
331 len: usize,
332) -> anyhow::Result<Vec<u8>> {
333 let memory = caller_memory(caller)?;
334 let _ = checked_memory_range(offset, len, memory.data_size(&mut *caller))?;
335 let mut bytes = vec![0_u8; len];
336 memory
337 .read(&mut *caller, offset, &mut bytes)
338 .map_err(anyhow::Error::from)?;
339 Ok(bytes)
340}
341
342fn caller_write_bytes(
343 caller: &mut Caller<'_, WasmHostData>,
344 offset: usize,
345 bytes: &[u8],
346) -> anyhow::Result<()> {
347 let memory = caller_memory(caller)?;
348 let _ = checked_memory_range(offset, bytes.len(), memory.data_size(&mut *caller))?;
349 memory
350 .write(&mut *caller, offset, bytes)
351 .map_err(anyhow::Error::from)
352}
353
354fn caller_read_u32(caller: &mut Caller<'_, WasmHostData>, offset: usize) -> anyhow::Result<u32> {
355 let bytes = caller_read_bytes(caller, offset, 4)?;
356 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
357}
358
359fn caller_write_u32(
360 caller: &mut Caller<'_, WasmHostData>,
361 offset: usize,
362 value: u32,
363) -> anyhow::Result<()> {
364 caller_write_bytes(caller, offset, &value.to_le_bytes())
365}
366
367fn caller_write_u64(
368 caller: &mut Caller<'_, WasmHostData>,
369 offset: usize,
370 value: u64,
371) -> anyhow::Result<()> {
372 caller_write_bytes(caller, offset, &value.to_le_bytes())
373}
374
375fn caller_read_c_string(
376 caller: &mut Caller<'_, WasmHostData>,
377 offset: usize,
378) -> anyhow::Result<String> {
379 let memory = caller_memory(caller)?;
380 let bytes = memory.data(&mut *caller);
381 let mut end = offset;
382 while end < bytes.len() && bytes[end] != 0 {
383 end += 1;
384 }
385 if end >= bytes.len() {
386 return Err(anyhow!("Unterminated string in WASM memory"));
387 }
388 Ok(String::from_utf8_lossy(&bytes[offset..end]).into_owned())
389}
390
391fn val_i32(params: &[Val], idx: usize, label: &str) -> anyhow::Result<i32> {
392 match params.get(idx) {
393 Some(Val::I32(value)) => Ok(*value),
394 _ => Err(anyhow!("Expected i32 parameter '{label}' at index {idx}")),
395 }
396}
397
398fn val_i64(params: &[Val], idx: usize, label: &str) -> anyhow::Result<i64> {
399 match params.get(idx) {
400 Some(Val::I64(value)) => Ok(*value),
401 Some(Val::I32(value)) => Ok(i64::from(*value)),
402 _ => Err(anyhow!("Expected i64 parameter '{label}' at index {idx}")),
403 }
404}
405
406const fn set_i32_result(results: &mut [Val], value: i32) {
407 if !results.is_empty() {
408 results[0] = Val::I32(value);
409 }
410}
411
412const fn set_f64_result(results: &mut [Val], value: f64) {
413 if !results.is_empty() {
414 results[0] = Val::F64(value.to_bits());
415 }
416}
417
418fn stub_import(
419 linker: &mut Linker<WasmHostData>,
420 mod_name: &str,
421 imp_name: &str,
422 func_ty: &wasmtime::FuncType,
423) -> Result<(), String> {
424 let result_types: Vec<ValType> = func_ty.results().collect();
425 linker
426 .func_new(
427 mod_name,
428 imp_name,
429 func_ty.clone(),
430 move |_caller: Caller<'_, WasmHostData>, _params: &[Val], results: &mut [Val]| {
431 for (i, ty) in result_types.iter().enumerate() {
432 results[i] = Val::default_for_ty(ty).unwrap_or(Val::I32(0));
433 }
434 Ok(())
435 },
436 )
437 .map_err(|e| format!("Failed to stub import {mod_name}.{imp_name}: {e}"))?;
438 Ok(())
439}
440
441#[allow(clippy::too_many_lines)]
444fn register_host_imports(
445 linker: &mut Linker<WasmHostData>,
446 module: &WasmModule,
447) -> Result<(), String> {
448 for import in module.imports() {
449 let mod_name = import.module();
450 let imp_name = import.name();
451 if let ExternType::Func(func_ty) = import.ty() {
452 match imp_name {
453 "__syscall_openat" => {
454 linker
455 .func_new(
456 mod_name,
457 imp_name,
458 func_ty.clone(),
459 move |mut caller, params, results| {
460 let path_ptr = usize::try_from(val_i32(params, 1, "path")?)
461 .map_err(|_| anyhow!("Negative path pointer"))?;
462 let flags = val_i32(params, 2, "flags")?;
463 let path = caller_read_c_string(&mut caller, path_ptr)?;
464 let Some((readable, writable)) = descriptor_access(flags) else {
465 set_i32_result(results, -ERRNO_INVAL);
466 return Ok(());
467 };
468 if flags & O_TRUNC != 0 && !writable {
469 set_i32_result(results, -ERRNO_INVAL);
470 return Ok(());
471 }
472
473 let append = flags & O_APPEND != 0;
474 let (fd, bytes_len) = {
475 let host = caller.data_mut();
476 let path_exists = host.staged_files.contains_key(&path);
477 if !path_exists {
478 if flags & O_CREAT == 0 {
479 set_i32_result(results, -ERRNO_NOENT);
480 return Ok(());
481 }
482 host.staged_files
483 .insert(path.clone(), std::sync::Arc::new(Vec::new()));
484 } else if flags & O_CREAT != 0 && flags & O_EXCL != 0 {
485 set_i32_result(results, -ERRNO_EXIST);
486 return Ok(());
487 }
488
489 let (position, bytes_len) = {
490 let file_arc =
491 host.staged_files.get_mut(&path).ok_or_else(|| {
492 anyhow!("Virtual file disappeared during open")
493 })?;
494 if flags & O_TRUNC != 0 {
495 std::sync::Arc::make_mut(file_arc).clear();
496 }
497 let bytes_len = file_arc.len();
498 let position = if append { bytes_len } else { 0 };
499 (position, bytes_len)
500 };
501
502 if host.next_fd == u32::MAX {
503 return Err(anyhow!("Synthetic fd space exhausted"));
504 }
505 let fd = host.next_fd;
506 host.next_fd = host.next_fd.saturating_add(1);
507 host.open_files.insert(
508 fd,
509 VirtualFileHandle {
510 path: path.clone(),
511 position,
512 readable,
513 writable,
514 append,
515 },
516 );
517 (fd, bytes_len)
518 };
519 debug!(
520 path,
521 bytes = bytes_len,
522 fd,
523 readable,
524 writable,
525 append,
526 "wasm: staged file open"
527 );
528 set_i32_result(results, i32::try_from(fd).unwrap_or(i32::MAX));
529 Ok(())
530 },
531 )
532 .map_err(|e| {
533 format!("Failed to register import {mod_name}.{imp_name}: {e}")
534 })?;
535 }
536 "fd_read" => {
537 linker
538 .func_new(
539 mod_name,
540 imp_name,
541 func_ty.clone(),
542 move |mut caller, params, results| {
543 let fd = u32::try_from(val_i32(params, 0, "fd")?)
544 .map_err(|_| anyhow!("Negative fd"))?;
545 let iov = usize::try_from(val_i32(params, 1, "iov")?)
546 .map_err(|_| anyhow!("Negative iov pointer"))?;
547 let iovcnt = usize::try_from(val_i32(params, 2, "iovcnt")?)
548 .map_err(|_| anyhow!("Negative iov count"))?;
549 let pnum = usize::try_from(val_i32(params, 3, "pnum")?)
550 .map_err(|_| anyhow!("Negative pnum pointer"))?;
551
552 let (path, mut position) =
553 if let Some(handle) = caller.data().open_files.get(&fd) {
554 if !handle.readable {
555 set_i32_result(results, ERRNO_BADF);
556 return Ok(());
557 }
558 (handle.path.clone(), handle.position)
559 } else {
560 set_i32_result(results, ERRNO_BADF);
561 return Ok(());
562 };
563
564 let mut total = 0_usize;
565 for index in 0..iovcnt {
566 let base = iov
567 .checked_add(index.saturating_mul(8))
568 .ok_or_else(|| anyhow!("iov overflow"))?;
569 let ptr = usize::try_from(caller_read_u32(&mut caller, base)?)
570 .map_err(|_| anyhow!("iov ptr overflow"))?;
571 let len =
572 usize::try_from(caller_read_u32(&mut caller, base + 4)?)
573 .map_err(|_| anyhow!("iov len overflow"))?;
574 let chunk = {
575 let host = caller.data();
576 let Some(file) = host.staged_files.get(&path) else {
577 set_i32_result(results, ERRNO_NOENT);
578 return Ok(());
579 };
580 if position >= file.len() || len == 0 {
581 Vec::new()
582 } else {
583 let available = file.len().saturating_sub(position);
584 let to_copy = available.min(len);
585 file[position..position + to_copy].to_vec()
586 }
587 };
588 if chunk.is_empty() {
589 break;
590 }
591 caller_write_bytes(&mut caller, ptr, &chunk)?;
592 position += chunk.len();
593 total += chunk.len();
594 if chunk.len() < len {
595 break;
596 }
597 }
598
599 caller_write_u32(
600 &mut caller,
601 pnum,
602 u32::try_from(total).unwrap_or(u32::MAX),
603 )?;
604 if let Some(handle) = caller.data_mut().open_files.get_mut(&fd) {
605 handle.position = position;
606 } else {
607 set_i32_result(results, ERRNO_BADF);
608 return Ok(());
609 }
610 set_i32_result(results, 0);
611 Ok(())
612 },
613 )
614 .map_err(|e| {
615 format!("Failed to register import {mod_name}.{imp_name}: {e}")
616 })?;
617 }
618 "fd_seek" => {
619 linker
620 .func_new(
621 mod_name,
622 imp_name,
623 func_ty.clone(),
624 move |mut caller, params, results| {
625 let fd = u32::try_from(val_i32(params, 0, "fd")?)
626 .map_err(|_| anyhow!("Negative fd"))?;
627 let offset = val_i64(params, 1, "offset")?;
628 let whence = val_i32(params, 2, "whence")?;
629 let new_offset_ptr =
630 usize::try_from(val_i32(params, 3, "newOffset")?)
631 .map_err(|_| anyhow!("Negative newOffset pointer"))?;
632
633 let (path, current_position) =
634 if let Some(handle) = caller.data().open_files.get(&fd) {
635 (handle.path.clone(), handle.position)
636 } else {
637 set_i32_result(results, ERRNO_BADF);
638 return Ok(());
639 };
640 let Some(file_len) =
641 caller.data().staged_files.get(&path).map(|v| v.len())
642 else {
643 set_i32_result(results, ERRNO_NOENT);
644 return Ok(());
645 };
646 let base = match whence {
647 0 => 0_i64,
648 1 => i64::try_from(current_position).unwrap_or(i64::MAX),
649 2 => i64::try_from(file_len).unwrap_or(i64::MAX),
650 _ => {
651 set_i32_result(results, ERRNO_INVAL);
652 return Ok(());
653 }
654 };
655 let next = base
656 .checked_add(offset)
657 .ok_or_else(|| anyhow!("Seek overflow"))?;
658 if next < 0 {
659 set_i32_result(results, ERRNO_INVAL);
660 return Ok(());
661 }
662 let next =
663 usize::try_from(next).map_err(|_| anyhow!("Seek overflow"))?;
664 if let Some(handle) = caller.data_mut().open_files.get_mut(&fd) {
665 handle.position = next;
666 } else {
667 set_i32_result(results, ERRNO_BADF);
668 return Ok(());
669 }
670 caller_write_u64(
671 &mut caller,
672 new_offset_ptr,
673 u64::try_from(next).unwrap_or(u64::MAX),
674 )?;
675 set_i32_result(results, 0);
676 Ok(())
677 },
678 )
679 .map_err(|e| {
680 format!("Failed to register import {mod_name}.{imp_name}: {e}")
681 })?;
682 }
683 "fd_close" => {
684 linker
685 .func_new(
686 mod_name,
687 imp_name,
688 func_ty.clone(),
689 move |mut caller, params, results| {
690 let fd = u32::try_from(val_i32(params, 0, "fd")?)
691 .map_err(|_| anyhow!("Negative fd"))?;
692 let result = if caller.data_mut().open_files.remove(&fd).is_some() {
693 0
694 } else {
695 ERRNO_BADF
696 };
697 set_i32_result(results, result);
698 Ok(())
699 },
700 )
701 .map_err(|e| {
702 format!("Failed to register import {mod_name}.{imp_name}: {e}")
703 })?;
704 }
705 "fd_write" => {
706 linker
707 .func_new(
708 mod_name,
709 imp_name,
710 func_ty.clone(),
711 move |mut caller, params, results| {
712 let fd = u32::try_from(val_i32(params, 0, "fd")?)
713 .map_err(|_| anyhow!("Negative fd"))?;
714 let iov = usize::try_from(val_i32(params, 1, "iov")?)
715 .map_err(|_| anyhow!("Negative iov pointer"))?;
716 let iovcnt = usize::try_from(val_i32(params, 2, "iovcnt")?)
717 .map_err(|_| anyhow!("Negative iov count"))?;
718 let pnum = usize::try_from(val_i32(params, 3, "pnum")?)
719 .map_err(|_| anyhow!("Negative pnum pointer"))?;
720 let (path, mut position, append, file_len) = {
721 let host = caller.data();
722 if let Some(handle) = host.open_files.get(&fd) {
723 if !handle.writable {
724 set_i32_result(results, ERRNO_BADF);
725 return Ok(());
726 }
727 let Some(file_len) =
728 host.staged_files.get(&handle.path).map(|v| v.len())
729 else {
730 set_i32_result(results, ERRNO_NOENT);
731 return Ok(());
732 };
733 (
734 handle.path.clone(),
735 handle.position,
736 handle.append,
737 file_len,
738 )
739 } else {
740 set_i32_result(results, ERRNO_BADF);
741 return Ok(());
742 }
743 };
744 let base_position = if append { file_len } else { position };
745 let mut total = 0_usize;
746 let mut chunks = Vec::new();
747 for index in 0..iovcnt {
748 let base = iov
749 .checked_add(index.saturating_mul(8))
750 .ok_or_else(|| anyhow!("iov overflow"))?;
751 let ptr = usize::try_from(caller_read_u32(&mut caller, base)?)
752 .map_err(|_| anyhow!("iov ptr overflow"))?;
753 let len =
754 usize::try_from(caller_read_u32(&mut caller, base + 4)?)
755 .map_err(|_| anyhow!("iov len overflow"))?;
756 if len == 0 {
757 continue;
758 }
759 let next_total = total
760 .checked_add(len)
761 .ok_or_else(|| anyhow!("fd_write byte count overflow"))?;
762 if base_position
763 .checked_add(next_total)
764 .ok_or_else(|| anyhow!("fd_write overflow"))?
765 > MAX_VIRTUAL_FILE_BYTES
766 {
767 set_i32_result(results, ERRNO_FBIG);
768 return Ok(());
769 }
770
771 let bytes = caller_read_bytes(&mut caller, ptr, len)?;
772 total = next_total;
773 chunks.push(bytes);
774 }
775 if total == 0 {
776 caller_write_u32(&mut caller, pnum, 0)?;
777 if let Some(handle) = caller.data_mut().open_files.get_mut(&fd)
778 {
779 handle.position = position;
780 } else {
781 set_i32_result(results, ERRNO_BADF);
782 return Ok(());
783 }
784 set_i32_result(results, 0);
785 return Ok(());
786 }
787 {
788 let host = caller.data_mut();
789 let Some(file_arc) = host.staged_files.get_mut(&path) else {
790 set_i32_result(results, ERRNO_NOENT);
791 return Ok(());
792 };
793 let file = std::sync::Arc::make_mut(file_arc);
794 if append {
795 position = base_position;
796 }
797 let end = position
798 .checked_add(total)
799 .ok_or_else(|| anyhow!("fd_write overflow"))?;
800 if end > MAX_VIRTUAL_FILE_BYTES {
801 set_i32_result(results, ERRNO_FBIG);
802 return Ok(());
803 }
804 if position > file.len() {
805 file.resize(position, 0);
806 }
807 if end > file.len() {
808 file.resize(end, 0);
809 }
810
811 let mut write_position = position;
814 for bytes in &chunks {
815 let chunk_end = write_position
816 .checked_add(bytes.len())
817 .ok_or_else(|| anyhow!("fd_write overflow"))?;
818 file[write_position..chunk_end].copy_from_slice(bytes);
819 write_position = chunk_end;
820 }
821 position = write_position;
822 }
823 caller_write_u32(
824 &mut caller,
825 pnum,
826 u32::try_from(total).unwrap_or(u32::MAX),
827 )?;
828 if let Some(handle) = caller.data_mut().open_files.get_mut(&fd) {
829 handle.position = position;
830 } else {
831 set_i32_result(results, ERRNO_BADF);
832 return Ok(());
833 }
834 set_i32_result(results, 0);
835 Ok(())
836 },
837 )
838 .map_err(|e| {
839 format!("Failed to register import {mod_name}.{imp_name}: {e}")
840 })?;
841 }
842 "emscripten_get_now" => {
843 linker
844 .func_new(
845 mod_name,
846 imp_name,
847 func_ty.clone(),
848 move |caller, _params, results| {
849 let elapsed_ms =
850 caller.data().started_at.elapsed().as_secs_f64() * 1000.0;
851 set_f64_result(results, elapsed_ms);
852 Ok(())
853 },
854 )
855 .map_err(|e| {
856 format!("Failed to register import {mod_name}.{imp_name}: {e}")
857 })?;
858 }
859 "emscripten_resize_heap" => {
860 linker
861 .func_new(
862 mod_name,
863 imp_name,
864 func_ty.clone(),
865 move |mut caller, params, results| {
866 let requested_size =
867 usize::try_from(val_i32(params, 0, "requestedSize")?)
868 .map_err(|_| anyhow!("Negative heap size"))?;
869 let memory = caller_memory(&mut caller)?;
870 let current_size = memory.data_size(&mut caller);
871 if requested_size <= current_size {
872 set_i32_result(results, 1);
873 return Ok(());
874 }
875 let page_size =
876 usize::try_from(memory.page_size(&caller)).unwrap_or(65_536);
877 let needed_bytes = requested_size.saturating_sub(current_size);
878 let needed_pages =
879 (needed_bytes.saturating_add(page_size - 1)) / page_size;
880 let current_pages = memory.size(&caller);
881 let requested_pages = current_pages.saturating_add(
882 u64::try_from(needed_pages).unwrap_or(u64::MAX),
883 );
884 if requested_pages > caller.data().max_memory_pages {
885 set_i32_result(results, 0);
886 return Ok(());
887 }
888 let grown = memory
889 .grow(
890 &mut caller,
891 u64::try_from(needed_pages).unwrap_or(u64::MAX),
892 )
893 .is_ok();
894 set_i32_result(results, i32::from(grown));
895 Ok(())
896 },
897 )
898 .map_err(|e| {
899 format!("Failed to register import {mod_name}.{imp_name}: {e}")
900 })?;
901 }
902 "__syscall_fcntl64" | "__syscall_ioctl" | "__syscall_mkdirat"
903 | "__syscall_renameat" | "__syscall_rmdir" | "__syscall_unlinkat"
904 | "_emscripten_system" | "exit" => {
905 stub_import(linker, mod_name, imp_name, &func_ty)?;
906 }
907 _ => stub_import(linker, mod_name, imp_name, &func_ty)?,
908 }
909 } else {
910 }
912 }
913 Ok(())
914}
915
916const DEFAULT_MAX_MEMORY_PAGES: u64 = 1024;
922const DEFAULT_MAX_MODULES: usize = 256;
924const DEFAULT_MAX_INSTANCES: usize = 256;
926const MAX_JS_WASM_ID: u32 = i32::MAX as u32;
928const TWO_POW_32: f64 = 4_294_967_296.0;
930const TWO_POW_31: f64 = 2_147_483_648.0;
931
932#[allow(clippy::too_many_lines)]
934pub(crate) fn inject_wasm_globals(
935 ctx: &Ctx<'_>,
936 state: &Rc<RefCell<WasmBridgeState>>,
937) -> rquickjs::Result<()> {
938 let global = ctx.globals();
939
940 {
942 let st = Rc::clone(state);
943 global.set(
944 "__pi_wasm_compile_native",
945 Func::from(
946 move |ctx: Ctx<'_>, bytes_val: Value<'_>| -> rquickjs::Result<u32> {
947 let bytes = extract_bytes(&ctx, &bytes_val)?;
948 if bytes.len() > MAX_VIRTUAL_FILE_BYTES {
949 return Err(throw_wasm(
950 &ctx,
951 "CompileError",
952 &format!(
953 "Module exceeds PiWasm limit ({} > {} bytes)",
954 bytes.len(),
955 MAX_VIRTUAL_FILE_BYTES
956 ),
957 ));
958 }
959 let mut bridge = st.borrow_mut();
960 if bridge.modules.len() >= bridge.max_modules {
961 return Err(throw_wasm(
962 &ctx,
963 "CompileError",
964 &format!("Module limit reached ({})", bridge.max_modules),
965 ));
966 }
967 let module = WasmModule::from_binary(&bridge.engine, &bytes)
968 .map_err(|e| throw_wasm(&ctx, "CompileError", &e.to_string()))?;
969 let id = bridge
970 .alloc_id()
971 .map_err(|e| throw_wasm(&ctx, "CompileError", &e))?;
972 debug!(module_id = id, bytes_len = bytes.len(), "wasm: compiled");
973 bridge.modules.insert(id, module);
974 Ok(id)
975 },
976 ),
977 )?;
978 }
979
980 {
982 let st = Rc::clone(state);
983 global.set(
984 "__pi_wasm_validate_native",
985 Func::from(
986 move |ctx: Ctx<'_>, bytes_val: Value<'_>| -> rquickjs::Result<bool> {
987 let bytes = extract_bytes(&ctx, &bytes_val)?;
988 if bytes.len() > MAX_VIRTUAL_FILE_BYTES {
989 return Ok(false);
990 }
991 let bridge = st.borrow();
992 Ok(WasmModule::from_binary(&bridge.engine, &bytes).is_ok())
993 },
994 ),
995 )?;
996 }
997
998 {
1000 let st = Rc::clone(state);
1001 global.set(
1002 "__pi_wasm_stage_file_native",
1003 Func::from(
1004 move |ctx: Ctx<'_>, path: String, bytes_val: Value<'_>| -> rquickjs::Result<u32> {
1005 let bytes = extract_bytes(&ctx, &bytes_val)?;
1006 if bytes.len() > MAX_VIRTUAL_FILE_BYTES {
1007 return Err(throw_wasm(
1008 &ctx,
1009 "RangeError",
1010 &format!(
1011 "Virtual file exceeds PiWasm limit ({} > {} bytes)",
1012 bytes.len(),
1013 MAX_VIRTUAL_FILE_BYTES
1014 ),
1015 ));
1016 }
1017 let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
1018 debug!(path = %path, len_bytes = bytes.len(), "wasm: staged file");
1019 st.borrow_mut()
1020 .staged_files
1021 .insert(path, std::sync::Arc::new(bytes));
1022 Ok(len)
1023 },
1024 ),
1025 )?;
1026 }
1027
1028 {
1030 let st = Rc::clone(state);
1031 global.set(
1032 "__pi_wasm_instantiate_native",
1033 Func::from(
1034 move |ctx: Ctx<'_>, module_id: u32| -> rquickjs::Result<u32> {
1035 let mut bridge = st.borrow_mut();
1036 if bridge.instances.len() >= bridge.max_instances {
1037 return Err(throw_wasm(
1038 &ctx,
1039 "RuntimeError",
1040 &format!("Instance limit reached ({})", bridge.max_instances),
1041 ));
1042 }
1043 let module = bridge
1044 .modules
1045 .get(&module_id)
1046 .ok_or_else(|| throw_wasm(&ctx, "LinkError", "Module not found"))?
1047 .clone();
1048
1049 let mut linker = Linker::new(&bridge.engine);
1050 register_host_imports(&mut linker, &module)
1051 .map_err(|e| throw_wasm(&ctx, "LinkError", &e))?;
1052
1053 let mut store = Store::new(
1054 &bridge.engine,
1055 WasmHostData {
1056 max_memory_pages: DEFAULT_MAX_MEMORY_PAGES,
1057 staged_files: bridge.staged_files.clone(),
1058 open_files: HashMap::new(),
1059 next_fd: 3,
1060 started_at: Instant::now(),
1061 },
1062 );
1063 let instance = linker
1064 .instantiate(&mut store, &module)
1065 .map_err(|e| throw_wasm(&ctx, "LinkError", &e.to_string()))?;
1066
1067 let id = bridge
1068 .alloc_id()
1069 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e))?;
1070 debug!(instance_id = id, module_id, "wasm: instantiated");
1071 bridge
1072 .instances
1073 .insert(id, InstanceState { store, instance });
1074 Ok(id)
1075 },
1076 ),
1077 )?;
1078 }
1079
1080 {
1082 let st = Rc::clone(state);
1083 global.set(
1084 "__pi_wasm_get_exports_native",
1085 Func::from(
1086 move |ctx: Ctx<'_>, instance_id: u32| -> rquickjs::Result<String> {
1087 let mut bridge = st.borrow_mut();
1088 let inst = bridge
1089 .instances
1090 .get_mut(&instance_id)
1091 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1092
1093 let mut entries: Vec<WasmExportEntry> = Vec::new();
1094 for export in inst.instance.exports(&mut inst.store) {
1095 let name = export.name().to_string();
1096 let kind = match export.into_extern() {
1097 wasmtime::Extern::Func(_) => "func",
1098 wasmtime::Extern::Memory(_) => "memory",
1099 wasmtime::Extern::Table(_) => "table",
1100 wasmtime::Extern::Global(_) => "global",
1101 wasmtime::Extern::SharedMemory(_) => "shared-memory",
1102 wasmtime::Extern::Tag(_) => "tag",
1103 };
1104 entries.push(WasmExportEntry { name, kind });
1105 }
1106 serde_json::to_string(&entries)
1107 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))
1108 },
1109 ),
1110 )?;
1111 }
1112
1113 {
1115 let st = Rc::clone(state);
1116 global.set(
1117 "__pi_wasm_call_export_native",
1118 Func::from(
1119 move |ctx: Ctx<'_>,
1120 instance_id: u32,
1121 name: String,
1122 args_val: Value<'_>|
1123 -> rquickjs::Result<f64> {
1124 let mut bridge = st.borrow_mut();
1125 let inst = bridge
1126 .instances
1127 .get_mut(&instance_id)
1128 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1129
1130 let started = Instant::now();
1131 let func = inst
1132 .instance
1133 .get_func(&mut inst.store, &name)
1134 .ok_or_else(|| {
1135 throw_wasm(&ctx, "RuntimeError", &format!("Export '{name}' not found"))
1136 })?;
1137
1138 let func_ty = func.ty(&inst.store);
1139 let param_types: Vec<ValType> = func_ty.params().collect();
1140 if param_types.iter().any(|ty| matches!(ty, ValType::I64)) {
1141 return Err(throw_wasm(
1142 &ctx,
1143 "TypeError",
1144 "i64 parameters are not supported by PiJS WebAssembly bridge",
1145 ));
1146 }
1147
1148 let args_arr = args_val
1150 .as_array()
1151 .ok_or_else(|| throw_wasm(&ctx, "TypeError", "args must be an array"))?;
1152 let mut params = Vec::with_capacity(param_types.len());
1153 for (i, ty) in param_types.iter().enumerate() {
1154 let js_val: Value<'_> = args_arr.get(i)?;
1155 params.push(js_to_val(&ctx, &js_val, ty)?);
1156 }
1157
1158 let result_types: Vec<ValType> = func_ty.results().collect();
1160 validate_call_result_types(&ctx, &result_types)?;
1161 let mut results: Vec<Val> = result_types
1162 .iter()
1163 .map(|ty| Val::default_for_ty(ty).unwrap_or(Val::I32(0)))
1164 .collect();
1165
1166 debug!(
1167 instance_id,
1168 export = %name,
1169 argc = params.len(),
1170 "wasm: call export start"
1171 );
1172 func.call(&mut inst.store, ¶ms, &mut results)
1173 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1174 debug!(
1175 instance_id,
1176 export = %name,
1177 argc = params.len(),
1178 elapsed_ms = started.elapsed().as_millis(),
1179 "wasm: call export"
1180 );
1181
1182 results.first().map_or(Ok(0.0), |val| val_to_f64(&ctx, val))
1184 },
1185 ),
1186 )?;
1187 }
1188
1189 {
1191 let st = Rc::clone(state);
1192 global.set(
1193 "__pi_wasm_memory_read_native",
1194 Func::from(
1195 move |ctx: Ctx<'_>,
1196 instance_id: u32,
1197 mem_name: String,
1198 offset: u32,
1199 len: u32|
1200 -> rquickjs::Result<Vec<u8>> {
1201 let mut bridge = st.borrow_mut();
1202 let inst = bridge
1203 .instances
1204 .get_mut(&instance_id)
1205 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1206 let memory = instance_memory(inst, &mem_name)
1207 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1208 let start = usize::try_from(offset)
1209 .map_err(|_| throw_wasm(&ctx, "RuntimeError", "Offset overflow"))?;
1210 let len = usize::try_from(len)
1211 .map_err(|_| throw_wasm(&ctx, "RuntimeError", "Length overflow"))?;
1212 let data = memory.data(&inst.store);
1213 let range = checked_memory_range(start, len, data.len())
1214 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1215 Ok(data[range].to_vec())
1216 },
1217 ),
1218 )?;
1219 }
1220
1221 {
1223 let st = Rc::clone(state);
1224 global.set(
1225 "__pi_wasm_memory_write_native",
1226 Func::from(
1227 move |ctx: Ctx<'_>,
1228 instance_id: u32,
1229 mem_name: String,
1230 offset: u32,
1231 bytes_val: Value<'_>|
1232 -> rquickjs::Result<u32> {
1233 let bytes = extract_bytes(&ctx, &bytes_val)?;
1234 let mut bridge = st.borrow_mut();
1235 let inst = bridge
1236 .instances
1237 .get_mut(&instance_id)
1238 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1239 let memory = instance_memory(inst, &mem_name)
1240 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1241 let start = usize::try_from(offset)
1242 .map_err(|_| throw_wasm(&ctx, "RuntimeError", "Offset overflow"))?;
1243 let _ =
1244 checked_memory_range(start, bytes.len(), memory.data_size(&mut inst.store))
1245 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1246 memory
1247 .write(&mut inst.store, start, &bytes)
1248 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1249 Ok(u32::try_from(bytes.len()).unwrap_or(u32::MAX))
1250 },
1251 ),
1252 )?;
1253 }
1254
1255 {
1257 let st = Rc::clone(state);
1258 global.set(
1259 "__pi_wasm_get_buffer_native",
1260 Func::from(
1261 move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<i32> {
1262 let mut bridge = st.borrow_mut();
1263 let inst = bridge
1264 .instances
1265 .get_mut(&instance_id)
1266 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1267 let started = Instant::now();
1268 let memory = instance_memory(inst, &mem_name)
1269 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1270 let data = memory.data(&inst.store);
1271 let len = i32::try_from(data.len()).unwrap_or(i32::MAX);
1272 let buffer = ArrayBuffer::new_copy(ctx.clone(), data)?;
1273 ctx.globals().set("__pi_wasm_tmp_buf", buffer)?;
1274 debug!(
1275 instance_id,
1276 memory = %mem_name,
1277 len_bytes = data.len(),
1278 elapsed_ms = started.elapsed().as_millis(),
1279 "wasm: get memory buffer"
1280 );
1281 Ok(len)
1282 },
1283 ),
1284 )?;
1285 }
1286
1287 {
1289 let st = Rc::clone(state);
1290 global.set(
1291 "__pi_wasm_memory_grow_native",
1292 Func::from(
1293 move |ctx: Ctx<'_>,
1294 instance_id: u32,
1295 mem_name: String,
1296 delta: u32|
1297 -> rquickjs::Result<i32> {
1298 let mut bridge = st.borrow_mut();
1299 let inst = bridge
1300 .instances
1301 .get_mut(&instance_id)
1302 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1303
1304 let memory = inst
1306 .instance
1307 .get_memory(&mut inst.store, &mem_name)
1308 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
1309 let current = memory.size(&inst.store);
1310 let requested = current.saturating_add(u64::from(delta));
1311 if requested > inst.store.data().max_memory_pages {
1312 return Ok(-1); }
1314
1315 Ok(memory
1316 .grow(&mut inst.store, u64::from(delta))
1317 .map_or(-1, |prev| i32::try_from(prev).unwrap_or(-1)))
1318 },
1319 ),
1320 )?;
1321 }
1322
1323 {
1325 let st = Rc::clone(state);
1326 global.set(
1327 "__pi_wasm_memory_size_native",
1328 Func::from(
1329 move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<u32> {
1330 let mut bridge = st.borrow_mut();
1331 let inst = bridge
1332 .instances
1333 .get_mut(&instance_id)
1334 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1335 let memory = inst
1336 .instance
1337 .get_memory(&mut inst.store, &mem_name)
1338 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
1339 Ok(u32::try_from(memory.size(&inst.store)).unwrap_or(u32::MAX))
1340 },
1341 ),
1342 )?;
1343 }
1344
1345 ctx.eval::<(), _>(WASM_POLYFILL_JS)?;
1347
1348 debug!("wasm: globalThis.WebAssembly polyfill injected");
1349 Ok(())
1350}
1351
1352const WASM_POLYFILL_JS: &str = r#"
1357(function() {
1358 "use strict";
1359
1360 class CompileError extends Error {
1361 constructor(msg) { super(msg); this.name = "CompileError"; }
1362 }
1363 class LinkError extends Error {
1364 constructor(msg) { super(msg); this.name = "LinkError"; }
1365 }
1366 class RuntimeError extends Error {
1367 constructor(msg) { super(msg); this.name = "RuntimeError"; }
1368 }
1369
1370 // Synchronous thenable: behaves like syncResolve() but executes
1371 // .then() callbacks immediately. QuickJS doesn't auto-flush microtasks.
1372 function syncResolve(value) {
1373 return {
1374 then: function(resolve, _reject) {
1375 try {
1376 var r = resolve(value);
1377 return syncResolve(r);
1378 } catch(e) { return syncReject(e); }
1379 },
1380 "catch": function() { return syncResolve(value); }
1381 };
1382 }
1383 function syncReject(err) {
1384 return {
1385 then: function(_resolve, reject) {
1386 if (reject) { reject(err); return syncResolve(undefined); }
1387 return syncReject(err);
1388 },
1389 "catch": function(fn) { fn(err); return syncResolve(undefined); }
1390 };
1391 }
1392
1393 function normalizeBytes(source) {
1394 if (source instanceof ArrayBuffer) {
1395 return new Uint8Array(source);
1396 }
1397 if (ArrayBuffer.isView && ArrayBuffer.isView(source)) {
1398 return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
1399 }
1400 if (Array.isArray(source)) {
1401 return new Uint8Array(source);
1402 }
1403 throw new CompileError("Invalid source: expected ArrayBuffer, TypedArray, or byte array");
1404 }
1405
1406 function resolveStreamingBytes(source) {
1407 if (source && typeof source.arrayBuffer === "function") {
1408 return source.arrayBuffer();
1409 }
1410 if (source && typeof source.then === "function") {
1411 return source.then(function(resp) {
1412 if (resp && typeof resp.arrayBuffer === "function") {
1413 return resp.arrayBuffer();
1414 }
1415 return resp;
1416 });
1417 }
1418 return source;
1419 }
1420
1421 function buildExports(instanceId) {
1422 var info = JSON.parse(__pi_wasm_get_exports_native(instanceId));
1423 var exports = {};
1424 for (var i = 0; i < info.length; i++) {
1425 var exp = info[i];
1426 if (exp.kind === "func") {
1427 (function(name) {
1428 exports[name] = function() {
1429 var args = [];
1430 for (var j = 0; j < arguments.length; j++) args.push(arguments[j]);
1431 return __pi_wasm_call_export_native(instanceId, name, args);
1432 };
1433 })(exp.name);
1434 } else if (exp.kind === "memory") {
1435 (function(name) {
1436 var memObj = Object.create(WebAssembly.Memory.prototype);
1437 Object.defineProperty(memObj, "buffer", {
1438 get: function() {
1439 __pi_wasm_get_buffer_native(instanceId, name);
1440 return globalThis.__pi_wasm_tmp_buf;
1441 },
1442 configurable: true
1443 });
1444 memObj.grow = function(delta) {
1445 var prevPages = __pi_wasm_memory_grow_native(instanceId, name, delta);
1446 if (prevPages < 0) {
1447 throw new RangeError("WebAssembly.Memory.grow(): failed to grow memory");
1448 }
1449 return prevPages;
1450 };
1451 exports[name] = memObj;
1452 })(exp.name);
1453 }
1454 }
1455 return exports;
1456 }
1457
1458 globalThis.WebAssembly = {
1459 CompileError: CompileError,
1460 LinkError: LinkError,
1461 RuntimeError: RuntimeError,
1462
1463 compile: function(source) {
1464 try {
1465 var bytes = normalizeBytes(source);
1466 var arr = [];
1467 for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
1468 var moduleId = __pi_wasm_compile_native(arr);
1469 var wasmMod = { __wasm_module_id: moduleId };
1470 return syncResolve(wasmMod);
1471 } catch (e) {
1472 return syncReject(e);
1473 }
1474 },
1475
1476 instantiate: function(source, _imports) {
1477 try {
1478 var moduleId;
1479 if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
1480 moduleId = source.__wasm_module_id;
1481 } else {
1482 var bytes = normalizeBytes(source);
1483 var arr = [];
1484 for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
1485 moduleId = __pi_wasm_compile_native(arr);
1486 }
1487 var instanceId = __pi_wasm_instantiate_native(moduleId);
1488 var exports = buildExports(instanceId);
1489 var instance = { exports: exports };
1490 var wasmMod = { __wasm_module_id: moduleId };
1491 globalThis.__pi_wasm_last_instance_id = instanceId;
1492 instance.__pi_instance_id = instanceId;
1493 exports.__pi_instance_id = instanceId;
1494
1495 if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
1496 return syncResolve(instance);
1497 }
1498 return syncResolve({ module: wasmMod, instance: instance });
1499 } catch (e) {
1500 return syncReject(e);
1501 }
1502 },
1503
1504 validate: function(source) {
1505 var bytes = normalizeBytes(source);
1506 return __pi_wasm_validate_native(bytes);
1507 },
1508
1509 instantiateStreaming: function(source, imports) {
1510 try {
1511 var bytesOrPromise = resolveStreamingBytes(source);
1512 if (bytesOrPromise && typeof bytesOrPromise.then === "function") {
1513 return bytesOrPromise.then(
1514 function(bytes) { return WebAssembly.instantiate(bytes, imports); },
1515 function(err) { return syncReject(err); }
1516 );
1517 }
1518 return WebAssembly.instantiate(bytesOrPromise, imports);
1519 } catch (e) {
1520 return syncReject(e);
1521 }
1522 },
1523
1524 compileStreaming: function(source) {
1525 try {
1526 var bytesOrPromise = resolveStreamingBytes(source);
1527 if (bytesOrPromise && typeof bytesOrPromise.then === "function") {
1528 return bytesOrPromise.then(
1529 function(bytes) { return WebAssembly.compile(bytes); },
1530 function(err) { return syncReject(err); }
1531 );
1532 }
1533 return WebAssembly.compile(bytesOrPromise);
1534 } catch (e) {
1535 return syncReject(e);
1536 }
1537 },
1538
1539 Memory: function(descriptor) {
1540 if (!(this instanceof WebAssembly.Memory)) {
1541 throw new TypeError("WebAssembly.Memory must be called with new");
1542 }
1543 var initial = descriptor && descriptor.initial !== undefined ? descriptor.initial : 0;
1544 var maximum = descriptor && descriptor.maximum !== undefined ? descriptor.maximum : undefined;
1545 var initialInt = Number(initial);
1546 if (!Number.isFinite(initialInt) || initialInt < 0 || Math.floor(initialInt) !== initialInt) {
1547 throw new RangeError("WebAssembly.Memory: invalid initial size");
1548 }
1549 var maxInt = maximum === undefined ? undefined : Number(maximum);
1550 if (maxInt !== undefined) {
1551 if (!Number.isFinite(maxInt) || maxInt < 0 || Math.floor(maxInt) !== maxInt) {
1552 throw new RangeError("WebAssembly.Memory: invalid maximum size");
1553 }
1554 if (maxInt < initialInt) {
1555 throw new RangeError("WebAssembly.Memory: maximum size smaller than initial");
1556 }
1557 }
1558 this._pages = initialInt;
1559 this._maximum = maxInt;
1560 this._buffer = new ArrayBuffer(this._pages * 65536);
1561 Object.defineProperty(this, "buffer", {
1562 get: function() { return this._buffer; },
1563 configurable: true
1564 });
1565 this.grow = function(delta) {
1566 var deltaInt = Number(delta);
1567 if (!Number.isFinite(deltaInt) || deltaInt < 0 || Math.floor(deltaInt) !== deltaInt) {
1568 throw new RangeError("WebAssembly.Memory.grow(): invalid delta");
1569 }
1570 var old = this._pages;
1571 var next = old + deltaInt;
1572 if (this._maximum !== undefined && next > this._maximum) {
1573 throw new RangeError("WebAssembly.Memory.grow(): maximum size exceeded");
1574 }
1575 var nextBuffer = new ArrayBuffer(next * 65536);
1576 new Uint8Array(nextBuffer).set(new Uint8Array(this._buffer));
1577 this._buffer = nextBuffer;
1578 this._pages = next;
1579 return old;
1580 };
1581 },
1582
1583 Table: function(descriptor) {
1584 if (!(this instanceof WebAssembly.Table)) {
1585 throw new TypeError("WebAssembly.Table must be called with new");
1586 }
1587 var initial = descriptor && descriptor.initial !== undefined ? descriptor.initial : 0;
1588 var maximum = descriptor && descriptor.maximum !== undefined ? descriptor.maximum : undefined;
1589 var initialInt = Number(initial);
1590 if (!Number.isFinite(initialInt) || initialInt < 0 || Math.floor(initialInt) !== initialInt) {
1591 throw new RangeError("WebAssembly.Table: invalid initial size");
1592 }
1593 var maxInt = maximum === undefined ? undefined : Number(maximum);
1594 if (maxInt !== undefined) {
1595 if (!Number.isFinite(maxInt) || maxInt < 0 || Math.floor(maxInt) !== maxInt) {
1596 throw new RangeError("WebAssembly.Table: invalid maximum size");
1597 }
1598 if (maxInt < initialInt) {
1599 throw new RangeError("WebAssembly.Table: maximum size smaller than initial");
1600 }
1601 }
1602 var element = descriptor && descriptor.element ? String(descriptor.element) : "anyfunc";
1603 if (element !== "anyfunc" && element !== "funcref" && element !== "externref") {
1604 throw new TypeError("WebAssembly.Table: invalid element type");
1605 }
1606 this._element = element;
1607 this._maximum = maxInt;
1608 this._values = new Array(initialInt);
1609 for (var i = 0; i < initialInt; i++) this._values[i] = null;
1610 Object.defineProperty(this, "length", {
1611 get: function() { return this._values.length; },
1612 configurable: true
1613 });
1614 this.get = function(index) {
1615 var indexInt = Number(index);
1616 if (!Number.isFinite(indexInt) || indexInt < 0 || Math.floor(indexInt) !== indexInt) {
1617 throw new RangeError("WebAssembly.Table.get(): invalid index");
1618 }
1619 if (indexInt >= this._values.length) {
1620 throw new RangeError("WebAssembly.Table.get(): index out of bounds");
1621 }
1622 return this._values[indexInt];
1623 };
1624 this.set = function(index, value) {
1625 var indexInt = Number(index);
1626 if (!Number.isFinite(indexInt) || indexInt < 0 || Math.floor(indexInt) !== indexInt) {
1627 throw new RangeError("WebAssembly.Table.set(): invalid index");
1628 }
1629 if (indexInt >= this._values.length) {
1630 throw new RangeError("WebAssembly.Table.set(): index out of bounds");
1631 }
1632 this._values[indexInt] = value;
1633 };
1634 this.grow = function(delta, value) {
1635 var deltaInt = Number(delta);
1636 if (!Number.isFinite(deltaInt) || deltaInt < 0 || Math.floor(deltaInt) !== deltaInt) {
1637 throw new RangeError("WebAssembly.Table.grow(): invalid delta");
1638 }
1639 var old = this._values.length;
1640 var next = old + deltaInt;
1641 if (this._maximum !== undefined && next > this._maximum) {
1642 throw new RangeError("WebAssembly.Table.grow(): maximum size exceeded");
1643 }
1644 for (var i = old; i < next; i++) {
1645 this._values[i] = value === undefined ? null : value;
1646 }
1647 return old;
1648 };
1649 },
1650
1651 Global: function(descriptor, value) {
1652 if (!(this instanceof WebAssembly.Global)) {
1653 throw new TypeError("WebAssembly.Global must be called with new");
1654 }
1655 var valType = descriptor && descriptor.value ? String(descriptor.value) : "i32";
1656 var mutable = descriptor && descriptor.mutable ? true : false;
1657 this._type = valType;
1658 this._mutable = mutable;
1659 this._value = value;
1660 Object.defineProperty(this, "value", {
1661 get: function() { return this._value; },
1662 set: function(next) {
1663 if (!this._mutable) {
1664 throw new TypeError("WebAssembly.Global is immutable");
1665 }
1666 this._value = next;
1667 },
1668 configurable: true
1669 });
1670 this.valueOf = function() { return this._value; };
1671 }
1672 };
1673})();
1674"#;
1675
1676#[cfg(test)]
1681mod tests {
1682 use super::*;
1683
1684 fn run_wasm_test(f: impl FnOnce(&Ctx<'_>, Rc<RefCell<WasmBridgeState>>)) {
1686 let rt = rquickjs::Runtime::new().expect("create runtime");
1687 let ctx = rquickjs::Context::full(&rt).expect("create context");
1688 ctx.with(|ctx| {
1689 let state = Rc::new(RefCell::new(WasmBridgeState::new()));
1690 inject_wasm_globals(&ctx, &state).expect("inject globals");
1691 f(&ctx, state);
1692 });
1693 }
1694
1695 fn wat_to_wasm(wat: &str) -> Vec<u8> {
1697 wat::parse_str(wat).expect("parse WAT to WASM binary")
1698 }
1699
1700 #[test]
1701 fn js_to_i32_matches_javascript_wrapping_semantics() {
1702 assert_eq!(js_to_i32(2_147_483_648.0), -2_147_483_648);
1703 assert_eq!(js_to_i32(4_294_967_296.0), 0);
1704 assert_eq!(js_to_i32(-2_147_483_649.0), 2_147_483_647);
1705 assert_eq!(js_to_i32(-1.9), -1);
1706 assert_eq!(js_to_i32(1.9), 1);
1707 assert_eq!(js_to_i32(f64::NAN), 0);
1708 assert_eq!(js_to_i32(f64::INFINITY), 0);
1709 assert_eq!(js_to_i32(f64::NEG_INFINITY), 0);
1710 }
1711
1712 #[test]
1713 fn compile_and_instantiate_trivial_module() {
1714 let wasm_bytes = wat_to_wasm(
1715 r#"(module
1716 (func (export "add") (param i32 i32) (result i32)
1717 local.get 0 local.get 1 i32.add)
1718 (memory (export "memory") 1)
1719 )"#,
1720 );
1721 run_wasm_test(|ctx, _state| {
1722 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1724 for (i, &b) in wasm_bytes.iter().enumerate() {
1725 arr.set(i, i32::from(b)).unwrap();
1726 }
1727 ctx.globals().set("__test_bytes", arr).unwrap();
1728
1729 let module_id: u32 = ctx
1731 .eval("__pi_wasm_compile_native(__test_bytes)")
1732 .expect("compile");
1733 assert!(module_id > 0);
1734
1735 let instance_id: u32 = ctx
1737 .eval(format!("__pi_wasm_instantiate_native({module_id})"))
1738 .expect("instantiate");
1739 assert!(instance_id > 0);
1740 });
1741 }
1742
1743 #[test]
1744 fn call_export_add() {
1745 let wasm_bytes = wat_to_wasm(
1746 r#"(module
1747 (func (export "add") (param i32 i32) (result i32)
1748 local.get 0 local.get 1 i32.add)
1749 )"#,
1750 );
1751 run_wasm_test(|ctx, _state| {
1752 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1753 for (i, &b) in wasm_bytes.iter().enumerate() {
1754 arr.set(i, i32::from(b)).unwrap();
1755 }
1756 ctx.globals().set("__test_bytes", arr).unwrap();
1757
1758 let result: i32 = ctx
1759 .eval(
1760 r#"
1761 var mid = __pi_wasm_compile_native(__test_bytes);
1762 var iid = __pi_wasm_instantiate_native(mid);
1763 __pi_wasm_call_export_native(iid, "add", [3, 4]);
1764 "#,
1765 )
1766 .expect("call add");
1767 assert_eq!(result, 7);
1768 });
1769 }
1770
1771 #[test]
1772 fn call_export_multiply() {
1773 let wasm_bytes = wat_to_wasm(
1774 r#"(module
1775 (func (export "mul") (param i32 i32) (result i32)
1776 local.get 0 local.get 1 i32.mul)
1777 )"#,
1778 );
1779 run_wasm_test(|ctx, _state| {
1780 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1781 for (i, &b) in wasm_bytes.iter().enumerate() {
1782 arr.set(i, i32::from(b)).unwrap();
1783 }
1784 ctx.globals().set("__test_bytes", arr).unwrap();
1785
1786 let result: i32 = ctx
1787 .eval(
1788 r#"
1789 var mid = __pi_wasm_compile_native(__test_bytes);
1790 var iid = __pi_wasm_instantiate_native(mid);
1791 __pi_wasm_call_export_native(iid, "mul", [6, 7]);
1792 "#,
1793 )
1794 .expect("call mul");
1795 assert_eq!(result, 42);
1796 });
1797 }
1798
1799 #[test]
1800 fn get_exports_lists_func_and_memory() {
1801 let wasm_bytes = wat_to_wasm(
1802 r#"(module
1803 (func (export "f1") (result i32) i32.const 1)
1804 (func (export "f2") (param i32) (result i32) local.get 0)
1805 (memory (export "mem") 2)
1806 )"#,
1807 );
1808 run_wasm_test(|ctx, _state| {
1809 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1810 for (i, &b) in wasm_bytes.iter().enumerate() {
1811 arr.set(i, i32::from(b)).unwrap();
1812 }
1813 ctx.globals().set("__test_bytes", arr).unwrap();
1814
1815 let count: i32 = ctx
1816 .eval(
1817 r"
1818 var mid = __pi_wasm_compile_native(__test_bytes);
1819 var iid = __pi_wasm_instantiate_native(mid);
1820 var exps = JSON.parse(__pi_wasm_get_exports_native(iid));
1821 exps.length;
1822 ",
1823 )
1824 .expect("get exports count");
1825 assert_eq!(count, 3);
1826 });
1827 }
1828
1829 #[test]
1830 fn get_exports_json_handles_escaped_names() {
1831 let wasm_bytes = wat_to_wasm(
1832 r#"(module
1833 (func (export "name\"with_quote") (result i32) i32.const 1)
1834 )"#,
1835 );
1836 run_wasm_test(|ctx, _state| {
1837 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1838 for (i, &b) in wasm_bytes.iter().enumerate() {
1839 arr.set(i, i32::from(b)).unwrap();
1840 }
1841 ctx.globals().set("__test_bytes", arr).unwrap();
1842
1843 let name: String = ctx
1844 .eval(
1845 r"
1846 var mid = __pi_wasm_compile_native(__test_bytes);
1847 var iid = __pi_wasm_instantiate_native(mid);
1848 JSON.parse(__pi_wasm_get_exports_native(iid))[0].name;
1849 ",
1850 )
1851 .expect("parse export JSON");
1852 assert_eq!(name, "name\"with_quote");
1853 });
1854 }
1855
1856 #[test]
1857 fn memory_buffer_returns_arraybuffer() {
1858 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
1859 run_wasm_test(|ctx, _state| {
1860 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1861 for (i, &b) in wasm_bytes.iter().enumerate() {
1862 arr.set(i, i32::from(b)).unwrap();
1863 }
1864 ctx.globals().set("__test_bytes", arr).unwrap();
1865
1866 let size: i32 = ctx
1867 .eval(
1868 r#"
1869 var mid = __pi_wasm_compile_native(__test_bytes);
1870 var iid = __pi_wasm_instantiate_native(mid);
1871 var len = __pi_wasm_get_buffer_native(iid, "memory");
1872 len;
1873 "#,
1874 )
1875 .expect("get buffer size");
1876 assert_eq!(size, 65536);
1878
1879 let buf_size: i32 = ctx
1881 .eval("__pi_wasm_tmp_buf.byteLength")
1882 .expect("tmp buffer size");
1883 assert_eq!(buf_size, 65536);
1884 });
1885 }
1886
1887 #[test]
1888 fn memory_grow_succeeds() {
1889 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
1890 run_wasm_test(|ctx, _state| {
1891 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1892 for (i, &b) in wasm_bytes.iter().enumerate() {
1893 arr.set(i, i32::from(b)).unwrap();
1894 }
1895 ctx.globals().set("__test_bytes", arr).unwrap();
1896
1897 let prev: i32 = ctx
1898 .eval(
1899 r#"
1900 var mid = __pi_wasm_compile_native(__test_bytes);
1901 var iid = __pi_wasm_instantiate_native(mid);
1902 __pi_wasm_memory_grow_native(iid, "memory", 2);
1903 "#,
1904 )
1905 .expect("grow memory");
1906 assert_eq!(prev, 1);
1908
1909 let new_size: i32 = ctx
1910 .eval(r#"__pi_wasm_memory_size_native(iid, "memory")"#)
1911 .expect("memory size");
1912 assert_eq!(new_size, 3);
1913 });
1914 }
1915
1916 #[test]
1917 fn memory_grow_denied_by_policy() {
1918 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
1919 run_wasm_test(|ctx, state| {
1920 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1921 for (i, &b) in wasm_bytes.iter().enumerate() {
1922 arr.set(i, i32::from(b)).unwrap();
1923 }
1924 ctx.globals().set("__test_bytes", arr).unwrap();
1925
1926 let instance_id: u32 = ctx
1927 .eval(
1928 r"
1929 var mid = __pi_wasm_compile_native(__test_bytes);
1930 __pi_wasm_instantiate_native(mid);
1931 ",
1932 )
1933 .expect("instantiate");
1934
1935 {
1937 let mut bridge = state.borrow_mut();
1938 let inst = bridge.instances.get_mut(&instance_id).unwrap();
1939 inst.store.data_mut().max_memory_pages = 2;
1940 }
1941
1942 let result: i32 = ctx
1944 .eval(format!(
1945 "__pi_wasm_memory_grow_native({instance_id}, 'memory', 5)"
1946 ))
1947 .expect("grow denied");
1948 assert_eq!(result, -1);
1949 });
1950 }
1951
1952 #[test]
1953 fn compile_invalid_bytes_fails() {
1954 run_wasm_test(|ctx, _state| {
1955 let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native([0, 1, 2, 3])");
1956 assert!(result.is_err());
1957 });
1958 }
1959
1960 #[test]
1961 fn js_polyfill_webassembly_validate_accepts_valid_module() {
1962 let wasm_bytes = wat_to_wasm(r"(module)");
1963 run_wasm_test(|ctx, _state| {
1964 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1965 for (i, &b) in wasm_bytes.iter().enumerate() {
1966 arr.set(i, i32::from(b)).unwrap();
1967 }
1968 ctx.globals().set("__test_bytes", arr).unwrap();
1969
1970 let result: bool = ctx
1971 .eval("WebAssembly.validate(__test_bytes)")
1972 .expect("validate");
1973 assert!(result);
1974 });
1975 }
1976
1977 #[test]
1978 fn js_polyfill_webassembly_validate_rejects_invalid_module() {
1979 run_wasm_test(|ctx, _state| {
1980 let result: bool = ctx
1981 .eval("WebAssembly.validate([0, 1, 2, 3])")
1982 .expect("validate");
1983 assert!(!result);
1984 });
1985 }
1986
1987 #[test]
1988 fn js_polyfill_webassembly_validate_does_not_register_module() {
1989 let wasm_bytes = wat_to_wasm(r"(module)");
1990 run_wasm_test(|ctx, state| {
1991 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1992 for (i, &b) in wasm_bytes.iter().enumerate() {
1993 arr.set(i, i32::from(b)).unwrap();
1994 }
1995 ctx.globals().set("__test_bytes", arr).unwrap();
1996
1997 let before = state.borrow().modules.len();
1998 let result: bool = ctx
1999 .eval("WebAssembly.validate(__test_bytes)")
2000 .expect("validate");
2001 let after = state.borrow().modules.len();
2002
2003 assert!(result);
2004 assert_eq!(before, after);
2005 });
2006 }
2007
2008 #[test]
2009 fn js_polyfill_webassembly_table_basic() {
2010 run_wasm_test(|ctx, _state| {
2011 ctx.eval::<(), _>(
2012 r#"
2013 const table = new WebAssembly.Table({ element: "funcref", initial: 2, maximum: 3 });
2014 if (table.length !== 2) throw new Error("table length mismatch");
2015 table.set(0, function() { return 1; });
2016 if (typeof table.get(0) !== "function") throw new Error("table get failed");
2017 const old = table.grow(1);
2018 if (old !== 2 || table.length !== 3) throw new Error("table grow failed");
2019 let threw = false;
2020 try { table.grow(1); } catch (e) { threw = true; }
2021 if (!threw) throw new Error("table maximum not enforced");
2022 "#,
2023 )
2024 .expect("table polyfill");
2025 });
2026 }
2027
2028 #[test]
2029 fn js_polyfill_webassembly_table_validation() {
2030 run_wasm_test(|ctx, _state| {
2031 let result: bool = ctx
2032 .eval(
2033 r#"
2034 (() => {
2035 const table = new WebAssembly.Table({ element: "funcref", initial: 1, maximum: 2 });
2036 let ok = false;
2037 try { table.get(0.5); } catch (e) { ok = e instanceof RangeError; }
2038 if (!ok) return false;
2039 ok = false;
2040 try { table.grow(1.25); } catch (e) { ok = e instanceof RangeError; }
2041 if (!ok) return false;
2042 ok = false;
2043 try { table.grow(2); } catch (e) { ok = e instanceof RangeError; }
2044 return ok;
2045 })()
2046 "#,
2047 )
2048 .expect("table validation");
2049 assert!(result);
2050 });
2051 }
2052
2053 #[test]
2054 fn js_polyfill_webassembly_memory_validation() {
2055 run_wasm_test(|ctx, _state| {
2056 let result: bool = ctx
2057 .eval(
2058 r"
2059 (() => {
2060 const mem = new WebAssembly.Memory({ initial: 1, maximum: 1 });
2061 let ok = false;
2062 try { mem.grow(0.5); } catch (e) { ok = e instanceof RangeError; }
2063 if (!ok) return false;
2064 ok = false;
2065 try { mem.grow(1); } catch (e) { ok = e instanceof RangeError; }
2066 return ok;
2067 })()
2068 ",
2069 )
2070 .expect("memory validation");
2071 assert!(result);
2072 });
2073 }
2074
2075 #[test]
2076 fn js_polyfill_webassembly_global_basic() {
2077 run_wasm_test(|ctx, _state| {
2078 ctx.eval::<(), _>(
2079 r#"
2080 const g = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
2081 if (g.value !== 1) throw new Error("global value mismatch");
2082 g.value = 2;
2083 if (g.value !== 2) throw new Error("global set failed");
2084 const imm = new WebAssembly.Global({ value: "i32" }, 7);
2085 let threw = false;
2086 try { imm.value = 9; } catch (e) { threw = true; }
2087 if (!threw) throw new Error("immutable global should throw");
2088 "#,
2089 )
2090 .expect("global polyfill");
2091 });
2092 }
2093
2094 #[test]
2095 fn instantiate_nonexistent_module_fails() {
2096 run_wasm_test(|ctx, _state| {
2097 let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_instantiate_native(99999)");
2098 assert!(result.is_err());
2099 });
2100 }
2101
2102 #[test]
2103 fn compile_rejects_when_module_limit_reached() {
2104 let wasm_bytes = wat_to_wasm(r"(module)");
2105 run_wasm_test(|ctx, state| {
2106 state.borrow_mut().set_limits_for_test(1, 8);
2107
2108 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2109 for (i, &b) in wasm_bytes.iter().enumerate() {
2110 arr.set(i, i32::from(b)).unwrap();
2111 }
2112 ctx.globals().set("__test_bytes", arr).unwrap();
2113
2114 let first: u32 = ctx
2115 .eval("__pi_wasm_compile_native(__test_bytes)")
2116 .expect("first compile");
2117 assert!(first > 0);
2118
2119 let second: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native(__test_bytes)");
2120 assert!(second.is_err());
2121 });
2122 }
2123
2124 #[test]
2125 fn instantiate_rejects_when_instance_limit_reached() {
2126 let wasm_bytes = wat_to_wasm(r"(module)");
2127 run_wasm_test(|ctx, state| {
2128 state.borrow_mut().set_limits_for_test(8, 1);
2129
2130 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2131 for (i, &b) in wasm_bytes.iter().enumerate() {
2132 arr.set(i, i32::from(b)).unwrap();
2133 }
2134 ctx.globals().set("__test_bytes", arr).unwrap();
2135
2136 let module_id: u32 = ctx
2137 .eval("__pi_wasm_compile_native(__test_bytes)")
2138 .expect("compile");
2139
2140 let first: u32 = ctx
2141 .eval(format!("__pi_wasm_instantiate_native({module_id})"))
2142 .expect("first instantiate");
2143 assert!(first > 0);
2144
2145 let second: rquickjs::Result<u32> =
2146 ctx.eval(format!("__pi_wasm_instantiate_native({module_id})"));
2147 assert!(second.is_err());
2148 });
2149 }
2150
2151 #[test]
2152 fn alloc_id_skips_zero_on_wrap() {
2153 let wasm_bytes = wat_to_wasm(r"(module)");
2154 run_wasm_test(|ctx, state| {
2155 {
2156 let mut bridge = state.borrow_mut();
2157 bridge.set_limits_for_test(8, 8);
2158 bridge.next_id = MAX_JS_WASM_ID;
2159 }
2160
2161 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2162 for (i, &b) in wasm_bytes.iter().enumerate() {
2163 arr.set(i, i32::from(b)).unwrap();
2164 }
2165 ctx.globals().set("__test_bytes", arr).unwrap();
2166
2167 let first: i32 = ctx
2168 .eval("__pi_wasm_compile_native(__test_bytes)")
2169 .expect("first compile");
2170 let second: i32 = ctx
2171 .eval("__pi_wasm_compile_native(__test_bytes)")
2172 .expect("second compile");
2173
2174 assert_eq!(first, i32::MAX);
2175 assert_eq!(second, 1);
2176 });
2177 }
2178
2179 #[test]
2180 fn call_nonexistent_export_fails() {
2181 let wasm_bytes = wat_to_wasm(r#"(module (func (export "f") (result i32) i32.const 1))"#);
2182 run_wasm_test(|ctx, _state| {
2183 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2184 for (i, &b) in wasm_bytes.iter().enumerate() {
2185 arr.set(i, i32::from(b)).unwrap();
2186 }
2187 ctx.globals().set("__test_bytes", arr).unwrap();
2188
2189 let result: rquickjs::Result<i32> = ctx.eval(
2190 r#"
2191 var mid = __pi_wasm_compile_native(__test_bytes);
2192 var iid = __pi_wasm_instantiate_native(mid);
2193 __pi_wasm_call_export_native(iid, "nonexistent", []);
2194 "#,
2195 );
2196 assert!(result.is_err());
2197 });
2198 }
2199
2200 #[test]
2201 fn call_export_i64_param_is_rejected() {
2202 let wasm_bytes = wat_to_wasm(
2203 r#"(module
2204 (func (export "id64") (param i64) (result i64)
2205 local.get 0)
2206 )"#,
2207 );
2208 run_wasm_test(|ctx, _state| {
2209 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2210 for (i, &b) in wasm_bytes.iter().enumerate() {
2211 arr.set(i, i32::from(b)).unwrap();
2212 }
2213 ctx.globals().set("__test_bytes", arr).unwrap();
2214
2215 let result: rquickjs::Result<i32> = ctx.eval(
2216 r#"
2217 var mid = __pi_wasm_compile_native(__test_bytes);
2218 var iid = __pi_wasm_instantiate_native(mid);
2219 __pi_wasm_call_export_native(iid, "id64", [1]);
2220 "#,
2221 );
2222 assert!(result.is_err());
2223 });
2224 }
2225
2226 #[test]
2227 fn call_export_i64_result_is_rejected() {
2228 let wasm_bytes = wat_to_wasm(
2229 r#"(module
2230 (func (export "ret64") (result i64)
2231 i64.const 42)
2232 )"#,
2233 );
2234 run_wasm_test(|ctx, _state| {
2235 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2236 for (i, &b) in wasm_bytes.iter().enumerate() {
2237 arr.set(i, i32::from(b)).unwrap();
2238 }
2239 ctx.globals().set("__test_bytes", arr).unwrap();
2240
2241 let result: rquickjs::Result<i32> = ctx.eval(
2242 r#"
2243 var mid = __pi_wasm_compile_native(__test_bytes);
2244 var iid = __pi_wasm_instantiate_native(mid);
2245 __pi_wasm_call_export_native(iid, "ret64", []);
2246 "#,
2247 );
2248 assert!(result.is_err());
2249 });
2250 }
2251
2252 #[test]
2253 fn call_export_multivalue_result_is_rejected() {
2254 let wasm_bytes = wat_to_wasm(
2255 r#"(module
2256 (func (export "pair") (result i32 i32)
2257 i32.const 1
2258 i32.const 2)
2259 )"#,
2260 );
2261 run_wasm_test(|ctx, _state| {
2262 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2263 for (i, &b) in wasm_bytes.iter().enumerate() {
2264 arr.set(i, i32::from(b)).unwrap();
2265 }
2266 ctx.globals().set("__test_bytes", arr).unwrap();
2267
2268 let result: rquickjs::Result<i32> = ctx.eval(
2269 r#"
2270 var mid = __pi_wasm_compile_native(__test_bytes);
2271 var iid = __pi_wasm_instantiate_native(mid);
2272 __pi_wasm_call_export_native(iid, "pair", []);
2273 "#,
2274 );
2275 assert!(result.is_err());
2276 });
2277 }
2278
2279 #[test]
2280 fn call_export_externref_result_is_rejected() {
2281 let wasm_bytes = wat_to_wasm(
2282 r#"(module
2283 (func (export "retref") (result externref)
2284 ref.null extern)
2285 )"#,
2286 );
2287 run_wasm_test(|ctx, _state| {
2288 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2289 for (i, &b) in wasm_bytes.iter().enumerate() {
2290 arr.set(i, i32::from(b)).unwrap();
2291 }
2292 ctx.globals().set("__test_bytes", arr).unwrap();
2293
2294 let result: rquickjs::Result<i32> = ctx.eval(
2295 r#"
2296 var mid = __pi_wasm_compile_native(__test_bytes);
2297 var iid = __pi_wasm_instantiate_native(mid);
2298 __pi_wasm_call_export_native(iid, "retref", []);
2299 "#,
2300 );
2301 assert!(result.is_err());
2302 });
2303 }
2304
2305 #[test]
2306 fn js_polyfill_webassembly_instantiate() {
2307 let wasm_bytes = wat_to_wasm(
2308 r#"(module
2309 (func (export "add") (param i32 i32) (result i32)
2310 local.get 0 local.get 1 i32.add)
2311 )"#,
2312 );
2313 run_wasm_test(|ctx, _state| {
2314 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2315 for (i, &b) in wasm_bytes.iter().enumerate() {
2316 arr.set(i, i32::from(b)).unwrap();
2317 }
2318 ctx.globals().set("__test_bytes", arr).unwrap();
2319
2320 let has_wa: bool = ctx
2322 .eval("typeof globalThis.WebAssembly !== 'undefined'")
2323 .expect("check WebAssembly");
2324 assert!(has_wa);
2325
2326 let result: i32 = ctx
2329 .eval(
2330 r"
2331 var __test_result = -1;
2332 WebAssembly.instantiate(__test_bytes).then(function(r) {
2333 __test_result = r.instance.exports.add(10, 20);
2334 });
2335 __test_result;
2336 ",
2337 )
2338 .expect("polyfill instantiate");
2339 assert_eq!(result, 30);
2340 });
2341 }
2342
2343 #[test]
2344 fn js_polyfill_webassembly_compile_streaming() {
2345 let wasm_bytes = wat_to_wasm(
2346 r#"(module
2347 (func (export "add") (param i32 i32) (result i32)
2348 local.get 0 local.get 1 i32.add)
2349 )"#,
2350 );
2351 run_wasm_test(|ctx, _state| {
2352 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2353 for (i, &b) in wasm_bytes.iter().enumerate() {
2354 arr.set(i, i32::from(b)).unwrap();
2355 }
2356 ctx.globals().set("__test_bytes", arr).unwrap();
2357
2358 let result: i32 = ctx
2359 .eval(
2360 r"
2361 var __test_result = -1;
2362 var __resp = { arrayBuffer: function() { return new Uint8Array(__test_bytes).buffer; } };
2363 WebAssembly.compileStreaming(__resp).then(function(mod) {
2364 WebAssembly.instantiate(mod).then(function(r) {
2365 __test_result = r.exports.add(2, 3);
2366 });
2367 });
2368 __test_result;
2369 ",
2370 )
2371 .expect("polyfill compileStreaming");
2372 assert_eq!(result, 5);
2373 });
2374 }
2375
2376 #[test]
2377 fn js_polyfill_webassembly_instantiate_streaming() {
2378 let wasm_bytes = wat_to_wasm(
2379 r#"(module
2380 (func (export "add") (param i32 i32) (result i32)
2381 local.get 0 local.get 1 i32.add)
2382 )"#,
2383 );
2384 run_wasm_test(|ctx, _state| {
2385 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2386 for (i, &b) in wasm_bytes.iter().enumerate() {
2387 arr.set(i, i32::from(b)).unwrap();
2388 }
2389 ctx.globals().set("__test_bytes", arr).unwrap();
2390
2391 let result: i32 = ctx
2392 .eval(
2393 r"
2394 var __test_result = -1;
2395 var __resp = { arrayBuffer: function() { return new Uint8Array(__test_bytes).buffer; } };
2396 WebAssembly.instantiateStreaming(__resp).then(function(r) {
2397 __test_result = r.instance.exports.add(6, 7);
2398 });
2399 __test_result;
2400 ",
2401 )
2402 .expect("polyfill instantiateStreaming");
2403 assert_eq!(result, 13);
2404 });
2405 }
2406
2407 #[test]
2408 fn js_polyfill_memory_buffer_getter() {
2409 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
2410 run_wasm_test(|ctx, _state| {
2411 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2412 for (i, &b) in wasm_bytes.iter().enumerate() {
2413 arr.set(i, i32::from(b)).unwrap();
2414 }
2415 ctx.globals().set("__test_bytes", arr).unwrap();
2416
2417 let size: i32 = ctx
2418 .eval(
2419 r"
2420 var __test_size = -1;
2421 WebAssembly.instantiate(__test_bytes).then(function(r) {
2422 __test_size = r.instance.exports.memory.buffer.byteLength;
2423 });
2424 __test_size;
2425 ",
2426 )
2427 .expect("polyfill memory buffer");
2428 assert_eq!(size, 65536);
2429 });
2430 }
2431
2432 #[test]
2433 fn js_polyfill_exported_memory_is_webassembly_memory() {
2434 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
2435 run_wasm_test(|ctx, _state| {
2436 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2437 for (i, &b) in wasm_bytes.iter().enumerate() {
2438 arr.set(i, i32::from(b)).unwrap();
2439 }
2440 ctx.globals().set("__test_bytes", arr).unwrap();
2441
2442 let is_memory: bool = ctx
2443 .eval(
2444 r"
2445 var __is_memory = false;
2446 WebAssembly.instantiate(__test_bytes).then(function(r) {
2447 __is_memory = r.instance.exports.memory instanceof WebAssembly.Memory;
2448 });
2449 __is_memory;
2450 ",
2451 )
2452 .expect("exported memory instanceof WebAssembly.Memory");
2453 assert!(is_memory);
2454 });
2455 }
2456
2457 #[test]
2458 fn js_polyfill_memory_grow_returns_previous_pages() {
2459 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
2460 run_wasm_test(|ctx, _state| {
2461 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2462 for (i, &b) in wasm_bytes.iter().enumerate() {
2463 arr.set(i, i32::from(b)).unwrap();
2464 }
2465 ctx.globals().set("__test_bytes", arr).unwrap();
2466
2467 let prev_pages: i32 = ctx
2468 .eval(
2469 r"
2470 var __test_prev = -1;
2471 WebAssembly.instantiate(__test_bytes).then(function(r) {
2472 __test_prev = r.instance.exports.memory.grow(2);
2473 });
2474 __test_prev;
2475 ",
2476 )
2477 .expect("polyfill memory grow");
2478 assert_eq!(prev_pages, 1);
2479
2480 let new_size: i32 = ctx
2481 .eval(
2482 r"
2483 var __test_size = -1;
2484 WebAssembly.instantiate(__test_bytes).then(function(r) {
2485 r.instance.exports.memory.grow(2);
2486 __test_size = r.instance.exports.memory.buffer.byteLength;
2487 });
2488 __test_size;
2489 ",
2490 )
2491 .expect("polyfill memory size after grow");
2492 assert_eq!(new_size, 3 * 65536);
2493 });
2494 }
2495
2496 #[test]
2497 fn js_polyfill_memory_grow_failure_throws_range_error() {
2498 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 1))"#);
2499 run_wasm_test(|ctx, _state| {
2500 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2501 for (i, &b) in wasm_bytes.iter().enumerate() {
2502 arr.set(i, i32::from(b)).unwrap();
2503 }
2504 ctx.globals().set("__test_bytes", arr).unwrap();
2505
2506 let threw_range_error: bool = ctx
2507 .eval(
2508 r"
2509 var __threw_range_error = false;
2510 WebAssembly.instantiate(__test_bytes).then(function(r) {
2511 try {
2512 r.instance.exports.memory.grow(1);
2513 } catch (e) {
2514 __threw_range_error = e instanceof RangeError;
2515 }
2516 });
2517 __threw_range_error;
2518 ",
2519 )
2520 .expect("polyfill memory grow failure");
2521 assert!(threw_range_error);
2522 });
2523 }
2524
2525 #[test]
2526 fn js_memory_constructor_grow_preserves_existing_bytes() {
2527 run_wasm_test(|ctx, _state| {
2528 let summary: String = ctx
2529 .eval(
2530 r#"
2531 var mem = new WebAssembly.Memory({ initial: 1 });
2532 var before = new Uint8Array(mem.buffer);
2533 before[0] = 7;
2534 before[65535] = 9;
2535 var prev = mem.grow(1);
2536 var after = new Uint8Array(mem.buffer);
2537 [prev, after.byteLength, after[0], after[65535], after[65536]].join(",");
2538 "#,
2539 )
2540 .expect("memory constructor grow preserves bytes");
2541 assert_eq!(summary, "1,131072,7,9,0");
2542 });
2543 }
2544
2545 #[test]
2546 fn module_with_imports_instantiates_with_stubs() {
2547 let wasm_bytes = wat_to_wasm(
2548 r#"(module
2549 (import "env" "log" (func (param i32)))
2550 (func (export "run") (result i32)
2551 i32.const 42
2552 call 0
2553 i32.const 1)
2554 )"#,
2555 );
2556 run_wasm_test(|ctx, _state| {
2557 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2558 for (i, &b) in wasm_bytes.iter().enumerate() {
2559 arr.set(i, i32::from(b)).unwrap();
2560 }
2561 ctx.globals().set("__test_bytes", arr).unwrap();
2562
2563 let result: i32 = ctx
2564 .eval(
2565 r#"
2566 var mid = __pi_wasm_compile_native(__test_bytes);
2567 var iid = __pi_wasm_instantiate_native(mid);
2568 __pi_wasm_call_export_native(iid, "run", []);
2569 "#,
2570 )
2571 .expect("call with import stubs");
2572 assert_eq!(result, 1);
2573 });
2574 }
2575
2576 #[test]
2577 fn native_memory_helpers_round_trip_live_wasm_memory() {
2578 let wasm_bytes = wat_to_wasm(
2579 r#"(module
2580 (memory (export "memory") 1)
2581 (func (export "read32") (param i32) (result i32)
2582 local.get 0
2583 i32.load)
2584 (func (export "write32") (param i32 i32)
2585 local.get 0
2586 local.get 1
2587 i32.store)
2588 )"#,
2589 );
2590 run_wasm_test(|ctx, _state| {
2591 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2592 for (i, &b) in wasm_bytes.iter().enumerate() {
2593 arr.set(i, i32::from(b)).unwrap();
2594 }
2595 ctx.globals().set("__test_bytes", arr).unwrap();
2596
2597 let summary: String = ctx
2598 .eval(
2599 r#"
2600 var mid = __pi_wasm_compile_native(__test_bytes);
2601 var iid = __pi_wasm_instantiate_native(mid);
2602 __pi_wasm_memory_write_native(iid, "memory", 32, [1, 2, 3, 4]);
2603 var readBack = __pi_wasm_call_export_native(iid, "read32", [32]);
2604 __pi_wasm_call_export_native(iid, "write32", [40, 0x11223344]);
2605 var bytes = __pi_wasm_memory_read_native(iid, "memory", 40, 4);
2606 [readBack, bytes[0], bytes[1], bytes[2], bytes[3]].join(",");
2607 "#,
2608 )
2609 .expect("memory helpers round-trip");
2610 assert_eq!(summary, "67305985,68,51,34,17");
2611 });
2612 }
2613
2614 #[test]
2615 fn staged_file_host_imports_can_open_and_read_wad_bytes() {
2616 let wasm_bytes = wat_to_wasm(
2617 r#"(module
2618 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2619 (import "env" "fd_read" (func $fd_read (param i32 i32 i32 i32) (result i32)))
2620 (import "env" "fd_close" (func $fd_close (param i32) (result i32)))
2621 (memory (export "memory") 1)
2622 (data (i32.const 64) "/doom/doom1.wad\00")
2623 (func (export "readfirst4") (result i32)
2624 (local $fd i32)
2625 i32.const 96
2626 i32.const 128
2627 i32.store
2628 i32.const 100
2629 i32.const 4
2630 i32.store
2631 i32.const -100
2632 i32.const 64
2633 i32.const 0
2634 i32.const 0
2635 call $openat
2636 local.tee $fd
2637 i32.const 96
2638 i32.const 1
2639 i32.const 104
2640 call $fd_read
2641 drop
2642 local.get $fd
2643 call $fd_close
2644 drop
2645 i32.const 128
2646 i32.load)
2647 (func (export "bytes_read") (result i32)
2648 i32.const 104
2649 i32.load)
2650 )"#,
2651 );
2652 run_wasm_test(|ctx, _state| {
2653 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2654 for (i, &b) in wasm_bytes.iter().enumerate() {
2655 arr.set(i, i32::from(b)).unwrap();
2656 }
2657 ctx.globals().set("__test_bytes", arr).unwrap();
2658
2659 let summary: String = ctx
2660 .eval(
2661 r#"
2662 __pi_wasm_stage_file_native("/doom/doom1.wad", [1, 2, 3, 4]);
2663 var mid = __pi_wasm_compile_native(__test_bytes);
2664 var iid = __pi_wasm_instantiate_native(mid);
2665 var first = __pi_wasm_call_export_native(iid, "readfirst4", []);
2666 var bytes = __pi_wasm_call_export_native(iid, "bytes_read", []);
2667 [first, bytes].join(",");
2668 "#,
2669 )
2670 .expect("staged file read");
2671 assert_eq!(summary, "67305985,4");
2672 });
2673 }
2674
2675 #[test]
2676 fn staged_file_host_imports_can_create_and_write_virtual_files() {
2677 let wasm_bytes = wat_to_wasm(
2678 r#"(module
2679 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2680 (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2681 (memory (export "memory") 1)
2682 (data (i32.const 64) "/tmp/out.bin\00")
2683 (data (i32.const 160) "\05\06\07")
2684 (func (export "write_new") (result i32)
2685 (local $fd i32)
2686 i32.const 128
2687 i32.const 160
2688 i32.store
2689 i32.const 132
2690 i32.const 3
2691 i32.store
2692 i32.const -100
2693 i32.const 64
2694 i32.const 577
2695 i32.const 0
2696 call $openat
2697 local.set $fd
2698 local.get $fd
2699 i32.const 128
2700 i32.const 1
2701 i32.const 136
2702 call $fd_write
2703 drop
2704 i32.const 136
2705 i32.load)
2706 )"#,
2707 );
2708 run_wasm_test(|ctx, state| {
2709 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2710 for (i, &b) in wasm_bytes.iter().enumerate() {
2711 arr.set(i, i32::from(b)).unwrap();
2712 }
2713 ctx.globals().set("__test_bytes", arr).unwrap();
2714
2715 let instance_id: u32 = ctx
2716 .eval(
2717 r"
2718 var mid = __pi_wasm_compile_native(__test_bytes);
2719 __pi_wasm_instantiate_native(mid);
2720 ",
2721 )
2722 .expect("instantiate writable virtual file module");
2723
2724 let bytes_written: i32 = ctx
2725 .eval(format!(
2726 r#"__pi_wasm_call_export_native({instance_id}, "write_new", [])"#
2727 ))
2728 .expect("write newly created virtual file");
2729 assert_eq!(bytes_written, 3);
2730
2731 let bridge = state.borrow();
2732 let contents = bridge
2733 .instances
2734 .get(&instance_id)
2735 .and_then(|inst| inst.store.data().staged_files.get("/tmp/out.bin"))
2736 .map(|arc| (**arc).clone());
2737 assert_eq!(contents, Some(vec![5, 6, 7]));
2738 });
2739 }
2740
2741 #[test]
2742 fn staged_file_host_imports_honor_truncate_flag_for_writes() {
2743 let wasm_bytes = wat_to_wasm(
2744 r#"(module
2745 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2746 (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2747 (memory (export "memory") 1)
2748 (data (i32.const 64) "/tmp/existing.bin\00")
2749 (data (i32.const 160) "\09")
2750 (func (export "truncate_then_write") (result i32)
2751 (local $fd i32)
2752 i32.const 128
2753 i32.const 160
2754 i32.store
2755 i32.const 132
2756 i32.const 1
2757 i32.store
2758 i32.const -100
2759 i32.const 64
2760 i32.const 513
2761 i32.const 0
2762 call $openat
2763 local.set $fd
2764 local.get $fd
2765 i32.const 128
2766 i32.const 1
2767 i32.const 136
2768 call $fd_write
2769 drop
2770 i32.const 136
2771 i32.load)
2772 )"#,
2773 );
2774 run_wasm_test(|ctx, state| {
2775 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2776 for (i, &b) in wasm_bytes.iter().enumerate() {
2777 arr.set(i, i32::from(b)).unwrap();
2778 }
2779 ctx.globals().set("__test_bytes", arr).unwrap();
2780
2781 let instance_id: u32 = ctx
2782 .eval(
2783 r#"
2784 __pi_wasm_stage_file_native("/tmp/existing.bin", [1, 2, 3, 4]);
2785 var mid = __pi_wasm_compile_native(__test_bytes);
2786 __pi_wasm_instantiate_native(mid);
2787 "#,
2788 )
2789 .expect("instantiate truncate virtual file module");
2790
2791 let bytes_written: i32 = ctx
2792 .eval(format!(
2793 r#"__pi_wasm_call_export_native({instance_id}, "truncate_then_write", [])"#
2794 ))
2795 .expect("truncate existing virtual file");
2796 assert_eq!(bytes_written, 1);
2797
2798 let bridge = state.borrow();
2799 let contents = bridge
2800 .instances
2801 .get(&instance_id)
2802 .and_then(|inst| inst.store.data().staged_files.get("/tmp/existing.bin"))
2803 .map(|arc| (**arc).clone());
2804 assert_eq!(contents, Some(vec![9]));
2805 });
2806 }
2807
2808 #[test]
2809 fn staged_file_host_imports_reject_write_on_read_only_descriptor() {
2810 let wasm_bytes = wat_to_wasm(
2811 r#"(module
2812 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2813 (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2814 (memory (export "memory") 1)
2815 (data (i32.const 64) "/doom/doom1.wad\00")
2816 (data (i32.const 160) "\09\08")
2817 (func (export "write_read_only") (result i32)
2818 (local $fd i32)
2819 i32.const 128
2820 i32.const 160
2821 i32.store
2822 i32.const 132
2823 i32.const 2
2824 i32.store
2825 i32.const -100
2826 i32.const 64
2827 i32.const 0
2828 i32.const 0
2829 call $openat
2830 local.set $fd
2831 local.get $fd
2832 i32.const 128
2833 i32.const 1
2834 i32.const 136
2835 call $fd_write)
2836 (func (export "bytes_written") (result i32)
2837 i32.const 136
2838 i32.load)
2839 )"#,
2840 );
2841 run_wasm_test(|ctx, _state| {
2842 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2843 for (i, &b) in wasm_bytes.iter().enumerate() {
2844 arr.set(i, i32::from(b)).unwrap();
2845 }
2846 ctx.globals().set("__test_bytes", arr).unwrap();
2847
2848 let summary: String = ctx
2849 .eval(
2850 r#"
2851 __pi_wasm_stage_file_native("/doom/doom1.wad", [1, 2, 3, 4]);
2852 var mid = __pi_wasm_compile_native(__test_bytes);
2853 var iid = __pi_wasm_instantiate_native(mid);
2854 var result = __pi_wasm_call_export_native(iid, "write_read_only", []);
2855 var bytes = __pi_wasm_call_export_native(iid, "bytes_written", []);
2856 [result, bytes].join(",");
2857 "#,
2858 )
2859 .expect("read-only descriptor write rejection");
2860 assert_eq!(summary, "8,0");
2861 });
2862 }
2863
2864 #[test]
2865 fn staged_file_host_imports_reject_write_past_virtual_file_limit() {
2866 let wasm_bytes = wat_to_wasm(&format!(
2867 r#"(module
2868 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2869 (import "env" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
2870 (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2871 (memory (export "memory") 1)
2872 (data (i32.const 64) "/tmp/too-big.bin\00")
2873 (data (i32.const 160) "\07")
2874 (func (export "seek_then_write_too_large") (result i32)
2875 (local $fd i32)
2876 i32.const 128
2877 i32.const 160
2878 i32.store
2879 i32.const 132
2880 i32.const 1
2881 i32.store
2882 i32.const -100
2883 i32.const 64
2884 i32.const 577
2885 i32.const 0
2886 call $openat
2887 local.set $fd
2888 local.get $fd
2889 i64.const {MAX_VIRTUAL_FILE_BYTES}
2890 i32.const 0
2891 i32.const 144
2892 call $fd_seek
2893 drop
2894 local.get $fd
2895 i32.const 128
2896 i32.const 1
2897 i32.const 136
2898 call $fd_write)
2899 (func (export "bytes_written") (result i32)
2900 i32.const 136
2901 i32.load)
2902 )"#,
2903 ));
2904 run_wasm_test(|ctx, state| {
2905 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2906 for (i, &b) in wasm_bytes.iter().enumerate() {
2907 arr.set(i, i32::from(b)).unwrap();
2908 }
2909 ctx.globals().set("__test_bytes", arr).unwrap();
2910
2911 let instance_id: u32 = ctx
2912 .eval(
2913 r"
2914 var mid = __pi_wasm_compile_native(__test_bytes);
2915 __pi_wasm_instantiate_native(mid);
2916 ",
2917 )
2918 .expect("instantiate large seek virtual file module");
2919
2920 let summary: String = ctx
2921 .eval(format!(
2922 r#"
2923 var result = __pi_wasm_call_export_native({instance_id}, "seek_then_write_too_large", []);
2924 var bytes = __pi_wasm_call_export_native({instance_id}, "bytes_written", []);
2925 [result, bytes].join(",");
2926 "#
2927 ))
2928 .expect("reject oversize virtual file write");
2929 assert_eq!(summary, "27,0");
2930
2931 let bridge = state.borrow();
2932 let len = bridge
2933 .instances
2934 .get(&instance_id)
2935 .and_then(|inst| inst.store.data().staged_files.get("/tmp/too-big.bin"))
2936 .map(|v| v.len());
2937 assert_eq!(len, Some(0));
2938 });
2939 }
2940
2941 #[test]
2942 fn staged_file_host_imports_reject_multi_iov_limit_overflow_atomically() {
2943 let near_limit = MAX_VIRTUAL_FILE_BYTES - 1;
2944 let wasm_bytes = wat_to_wasm(&format!(
2945 r#"(module
2946 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2947 (import "env" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
2948 (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2949 (memory (export "memory") 1)
2950 (data (i32.const 64) "/tmp/too-big-split.bin\00")
2951 (data (i32.const 160) "\07\08")
2952 (func (export "split_write_too_large") (result i32)
2953 (local $fd i32)
2954 i32.const 128
2955 i32.const 160
2956 i32.store
2957 i32.const 132
2958 i32.const 1
2959 i32.store
2960 i32.const 136
2961 i32.const 161
2962 i32.store
2963 i32.const 140
2964 i32.const 1
2965 i32.store
2966 i32.const -100
2967 i32.const 64
2968 i32.const 577
2969 i32.const 0
2970 call $openat
2971 local.set $fd
2972 local.get $fd
2973 i64.const {near_limit}
2974 i32.const 0
2975 i32.const 152
2976 call $fd_seek
2977 drop
2978 local.get $fd
2979 i32.const 128
2980 i32.const 2
2981 i32.const 144
2982 call $fd_write)
2983 (func (export "bytes_written") (result i32)
2984 i32.const 144
2985 i32.load)
2986 )"#,
2987 ));
2988 run_wasm_test(|ctx, state| {
2989 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2990 for (i, &b) in wasm_bytes.iter().enumerate() {
2991 arr.set(i, i32::from(b)).unwrap();
2992 }
2993 ctx.globals().set("__test_bytes", arr).unwrap();
2994
2995 let instance_id: u32 = ctx
2996 .eval(
2997 r"
2998 var mid = __pi_wasm_compile_native(__test_bytes);
2999 __pi_wasm_instantiate_native(mid);
3000 ",
3001 )
3002 .expect("instantiate split oversize virtual file module");
3003
3004 let summary: String = ctx
3005 .eval(format!(
3006 r#"
3007 var result = __pi_wasm_call_export_native({instance_id}, "split_write_too_large", []);
3008 var bytes = __pi_wasm_call_export_native({instance_id}, "bytes_written", []);
3009 [result, bytes].join(",");
3010 "#
3011 ))
3012 .expect("reject split oversize virtual file write");
3013 assert_eq!(summary, "27,0");
3014
3015 let bridge = state.borrow();
3016 let len = bridge
3017 .instances
3018 .get(&instance_id)
3019 .and_then(|inst| inst.store.data().staged_files.get("/tmp/too-big-split.bin"))
3020 .map(|v| v.len());
3021 assert_eq!(len, Some(0));
3022 });
3023 }
3024
3025 #[test]
3026 fn staged_file_host_imports_allow_zero_length_write_past_virtual_file_limit() {
3027 let past_limit = MAX_VIRTUAL_FILE_BYTES + 1;
3028 let wasm_bytes = wat_to_wasm(&format!(
3029 r#"(module
3030 (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
3031 (import "env" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
3032 (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
3033 (memory (export "memory") 1)
3034 (data (i32.const 64) "/tmp/too-big-zero.bin\00")
3035 (func (export "zero_write_after_large_seek") (result i32)
3036 (local $fd i32)
3037 i32.const 128
3038 i32.const 160
3039 i32.store
3040 i32.const 132
3041 i32.const 0
3042 i32.store
3043 i32.const -100
3044 i32.const 64
3045 i32.const 577
3046 i32.const 0
3047 call $openat
3048 local.set $fd
3049 local.get $fd
3050 i64.const {past_limit}
3051 i32.const 0
3052 i32.const 144
3053 call $fd_seek
3054 drop
3055 local.get $fd
3056 i32.const 128
3057 i32.const 1
3058 i32.const 136
3059 call $fd_write)
3060 (func (export "bytes_written") (result i32)
3061 i32.const 136
3062 i32.load)
3063 )"#,
3064 ));
3065 run_wasm_test(|ctx, state| {
3066 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
3067 for (i, &b) in wasm_bytes.iter().enumerate() {
3068 arr.set(i, i32::from(b)).unwrap();
3069 }
3070 ctx.globals().set("__test_bytes", arr).unwrap();
3071
3072 let instance_id: u32 = ctx
3073 .eval(
3074 r"
3075 var mid = __pi_wasm_compile_native(__test_bytes);
3076 __pi_wasm_instantiate_native(mid);
3077 ",
3078 )
3079 .expect("instantiate zero-write virtual file module");
3080
3081 let summary: String = ctx
3082 .eval(format!(
3083 r#"
3084 var result = __pi_wasm_call_export_native({instance_id}, "zero_write_after_large_seek", []);
3085 var bytes = __pi_wasm_call_export_native({instance_id}, "bytes_written", []);
3086 [result, bytes].join(",");
3087 "#
3088 ))
3089 .expect("allow zero-length write after large seek");
3090 assert_eq!(summary, "0,0");
3091
3092 let bridge = state.borrow();
3093 let len = bridge
3094 .instances
3095 .get(&instance_id)
3096 .and_then(|inst| inst.store.data().staged_files.get("/tmp/too-big-zero.bin"))
3097 .map(|v| v.len());
3098 assert_eq!(len, Some(0));
3099 });
3100 }
3101}