1use crate::event::InputEvent;
32use crate::result::{ProbarError, ProbarResult};
33use serde::{Deserialize, Serialize};
34use std::collections::{hash_map::DefaultHasher, VecDeque};
35use std::hash::{Hash, Hasher};
36
37#[cfg(feature = "runtime")]
38use wasmtime::{Caller, Engine, Instance, Linker, Module, Store};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub struct EntityId(pub u32);
43
44impl EntityId {
45 #[must_use]
47 pub const fn new(id: u32) -> Self {
48 Self(id)
49 }
50
51 #[must_use]
53 pub const fn raw(self) -> u32 {
54 self.0
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub struct ComponentId(u64);
61
62impl ComponentId {
63 #[must_use]
65 pub fn of<T: 'static>() -> Self {
66 let mut hasher = DefaultHasher::new();
67 std::any::TypeId::of::<T>().hash(&mut hasher);
68 Self(hasher.finish())
69 }
70
71 #[must_use]
73 pub const fn raw(self) -> u64 {
74 self.0
75 }
76}
77
78pub trait ProbarEntity: Copy {
98 fn entity_id(&self) -> EntityId;
100
101 fn entity_name(&self) -> &'static str;
103}
104
105pub trait ProbarComponent: Sized + Copy + 'static {
109 fn component_id() -> ComponentId;
111
112 fn layout() -> std::alloc::Layout;
114}
115
116#[derive(Debug, Clone)]
118pub struct FrameResult {
119 pub frame_number: u64,
121 pub state_hash: u64,
123 pub execution_time_ns: u64,
125}
126
127#[derive(Debug, Clone)]
132pub struct StateDelta {
133 pub base_frame: u64,
135 pub target_frame: u64,
137 pub changes: Vec<(usize, Vec<u8>)>,
139 pub checksum: u64,
141}
142
143impl StateDelta {
144 #[must_use]
146 pub fn empty(frame: u64) -> Self {
147 Self {
148 base_frame: frame,
149 target_frame: frame,
150 changes: Vec::new(),
151 checksum: 0,
152 }
153 }
154
155 #[must_use]
157 pub fn compute(base: &[u8], current: &[u8], base_frame: u64, target_frame: u64) -> Self {
158 let mut changes = Vec::new();
159 let mut i = 0;
160
161 while i < base.len().min(current.len()) {
162 if base.get(i) != current.get(i) {
164 let start = i;
165 while i < base.len().min(current.len()) && base.get(i) != current.get(i) {
167 i += 1;
168 }
169 changes.push((start, current[start..i].to_vec()));
171 } else {
172 i += 1;
173 }
174 }
175
176 if current.len() > base.len() {
178 changes.push((base.len(), current[base.len()..].to_vec()));
179 }
180
181 let checksum = Self::compute_checksum(current);
182
183 Self {
184 base_frame,
185 target_frame,
186 changes,
187 checksum,
188 }
189 }
190
191 #[must_use]
193 pub fn apply(&self, base: &[u8]) -> Vec<u8> {
194 let mut result = base.to_vec();
195 for (offset, data) in &self.changes {
196 let end = *offset + data.len();
197 if end > result.len() {
198 result.resize(end, 0);
199 }
200 result[*offset..end].copy_from_slice(data);
201 }
202 result
203 }
204
205 fn compute_checksum(data: &[u8]) -> u64 {
206 let mut hasher = DefaultHasher::new();
207 data.hash(&mut hasher);
208 hasher.finish()
209 }
210
211 #[must_use]
213 pub fn verify(&self, data: &[u8]) -> bool {
214 Self::compute_checksum(data) == self.checksum
215 }
216}
217
218#[derive(Debug, Default)]
223pub struct GameHostState {
224 pub input_queue: VecDeque<InputEvent>,
226 pub simulated_time: f64,
228 pub frame_count: u64,
230 pub snapshot_deltas: Vec<StateDelta>,
232 last_snapshot: Vec<u8>,
234}
235
236impl GameHostState {
237 #[must_use]
239 pub fn new() -> Self {
240 Self::default()
241 }
242
243 pub fn pop_input(&mut self) -> Option<InputEvent> {
245 self.input_queue.pop_front()
246 }
247
248 pub fn record_snapshot(&mut self, memory: &[u8]) {
250 let delta = StateDelta::compute(
251 &self.last_snapshot,
252 memory,
253 self.frame_count.saturating_sub(1),
254 self.frame_count,
255 );
256 self.snapshot_deltas.push(delta);
257 memory.clone_into(&mut self.last_snapshot);
258 }
259}
260
261#[derive(Debug)]
270pub struct MemoryView {
271 size: usize,
273 entity_table_offset: usize,
275 component_arrays_offset: usize,
277 entity_count: usize,
279}
280
281impl MemoryView {
282 #[must_use]
284 pub fn new(size: usize) -> Self {
285 Self {
286 size,
287 entity_table_offset: 0,
288 component_arrays_offset: 0,
289 entity_count: 0,
290 }
291 }
292
293 #[must_use]
295 pub fn with_entity_table(mut self, offset: usize, count: usize) -> Self {
296 self.entity_table_offset = offset;
297 self.entity_count = count;
298 self
299 }
300
301 #[must_use]
303 pub fn with_component_arrays(mut self, offset: usize) -> Self {
304 self.component_arrays_offset = offset;
305 self
306 }
307
308 #[must_use]
310 pub const fn size(&self) -> usize {
311 self.size
312 }
313
314 #[must_use]
316 pub const fn entity_count(&self) -> usize {
317 self.entity_count
318 }
319
320 #[must_use]
322 pub const fn entity_table_offset(&self) -> usize {
323 self.entity_table_offset
324 }
325
326 #[must_use]
328 pub const fn component_arrays_offset(&self) -> usize {
329 self.component_arrays_offset
330 }
331
332 #[inline]
340 pub unsafe fn read_at<T: Copy>(&self, memory: &[u8], offset: usize) -> ProbarResult<T> {
341 let size = core::mem::size_of::<T>();
342 if offset + size > memory.len() {
343 return Err(ProbarError::WasmError {
344 message: format!(
345 "Read out of bounds: offset {} + size {} > memory {}",
346 offset,
347 size,
348 memory.len()
349 ),
350 });
351 }
352 let ptr = memory.as_ptr().add(offset) as *const T;
353 Ok(core::ptr::read_unaligned(ptr))
354 }
355
356 #[inline]
362 pub fn read_slice<'a>(
363 &self,
364 memory: &'a [u8],
365 offset: usize,
366 len: usize,
367 ) -> ProbarResult<&'a [u8]> {
368 if offset + len > memory.len() {
369 return Err(ProbarError::WasmError {
370 message: format!(
371 "Slice out of bounds: offset {} + len {} > memory {}",
372 offset,
373 len,
374 memory.len()
375 ),
376 });
377 }
378 Ok(&memory[offset..offset + len])
379 }
380}
381
382#[derive(Debug, Clone, Copy)]
384pub struct RuntimeConfig {
385 pub wasm_threads: bool,
387 pub wasm_simd: bool,
389 pub wasm_reference_types: bool,
391 pub max_memory_pages: u32,
393 pub fuel_limit: u64,
395}
396
397impl Default for RuntimeConfig {
398 fn default() -> Self {
399 Self {
400 wasm_threads: false,
401 wasm_simd: true,
402 wasm_reference_types: true,
403 max_memory_pages: 256, fuel_limit: 0,
405 }
406 }
407}
408
409impl RuntimeConfig {
410 #[must_use]
412 pub fn new() -> Self {
413 Self::default()
414 }
415
416 #[must_use]
418 pub const fn with_threads(mut self, enabled: bool) -> Self {
419 self.wasm_threads = enabled;
420 self
421 }
422
423 #[must_use]
425 pub const fn with_fuel_limit(mut self, limit: u64) -> Self {
426 self.fuel_limit = limit;
427 self
428 }
429}
430
431#[cfg(feature = "runtime")]
445pub struct WasmRuntime {
446 engine: Engine,
447 store: Store<GameHostState>,
448 instance: Instance,
449 memory_view: MemoryView,
450}
451
452#[cfg(feature = "runtime")]
453impl std::fmt::Debug for WasmRuntime {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("WasmRuntime")
456 .field("memory_view", &self.memory_view)
457 .finish_non_exhaustive()
458 }
459}
460
461#[cfg(feature = "runtime")]
462impl WasmRuntime {
463 pub fn load(wasm_bytes: &[u8]) -> ProbarResult<Self> {
472 Self::load_with_config(wasm_bytes, RuntimeConfig::default())
473 }
474
475 pub fn load_with_config(wasm_bytes: &[u8], config: RuntimeConfig) -> ProbarResult<Self> {
481 let mut engine_config = wasmtime::Config::new();
482 engine_config.wasm_threads(config.wasm_threads);
483 engine_config.wasm_simd(config.wasm_simd);
484 engine_config.wasm_reference_types(config.wasm_reference_types);
485
486 if config.fuel_limit > 0 {
487 engine_config.consume_fuel(true);
488 }
489
490 let engine = Engine::new(&engine_config).map_err(|e| ProbarError::WasmError {
491 message: format!("Failed to create engine: {e}"),
492 })?;
493
494 let module = Module::new(&engine, wasm_bytes).map_err(|e| ProbarError::WasmError {
495 message: format!("Failed to load module: {e}"),
496 })?;
497
498 let mut store = Store::new(&engine, GameHostState::new());
499
500 if config.fuel_limit > 0 {
501 store
502 .set_fuel(config.fuel_limit)
503 .map_err(|e| ProbarError::WasmError {
504 message: format!("Failed to set fuel: {e}"),
505 })?;
506 }
507
508 let mut linker = Linker::new(&engine);
509
510 Self::register_host_functions(&mut linker)?;
512
513 let instance =
514 linker
515 .instantiate(&mut store, &module)
516 .map_err(|e| ProbarError::WasmError {
517 message: format!("Failed to instantiate: {e}"),
518 })?;
519
520 let memory =
522 instance
523 .get_memory(&mut store, "memory")
524 .ok_or_else(|| ProbarError::WasmError {
525 message: "Module does not export 'memory'".to_string(),
526 })?;
527
528 let memory_size = memory.data_size(&store);
529 let memory_view = MemoryView::new(memory_size);
530
531 Ok(Self {
532 engine,
533 store,
534 instance,
535 memory_view,
536 })
537 }
538
539 fn register_host_functions(linker: &mut Linker<GameHostState>) -> ProbarResult<()> {
540 linker
542 .func_wrap(
543 "probar",
544 "get_input_count",
545 #[allow(clippy::cast_possible_truncation)]
546 |caller: Caller<'_, GameHostState>| -> u32 {
547 caller.data().input_queue.len() as u32
548 },
549 )
550 .map_err(|e| ProbarError::WasmError {
551 message: format!("Failed to register get_input_count: {e}"),
552 })?;
553
554 linker
556 .func_wrap(
557 "probar",
558 "get_time",
559 |caller: Caller<'_, GameHostState>| -> f64 { caller.data().simulated_time },
560 )
561 .map_err(|e| ProbarError::WasmError {
562 message: format!("Failed to register get_time: {e}"),
563 })?;
564
565 linker
567 .func_wrap(
568 "probar",
569 "get_frame",
570 |caller: Caller<'_, GameHostState>| -> u64 { caller.data().frame_count },
571 )
572 .map_err(|e| ProbarError::WasmError {
573 message: format!("Failed to register get_frame: {e}"),
574 })?;
575
576 Ok(())
577 }
578
579 #[must_use]
581 pub const fn engine(&self) -> &Engine {
582 &self.engine
583 }
584
585 pub fn inject_input(&mut self, event: InputEvent) {
587 self.store.data_mut().input_queue.push_back(event);
588 }
589
590 pub fn inject_inputs(&mut self, events: impl IntoIterator<Item = InputEvent>) {
592 for event in events {
593 self.inject_input(event);
594 }
595 }
596
597 pub fn step(&mut self) -> ProbarResult<FrameResult> {
605 self.step_with_dt(1.0 / 60.0)
606 }
607
608 pub fn step_with_dt(&mut self, dt: f64) -> ProbarResult<FrameResult> {
614 let start = std::time::Instant::now();
615
616 self.store.data_mut().simulated_time += dt;
618 self.store.data_mut().frame_count += 1;
619
620 let update_fn = self
622 .instance
623 .get_typed_func::<f64, ()>(&mut self.store, "jugar_update")
624 .map_err(|e| ProbarError::WasmError {
625 message: format!("jugar_update not found: {e}"),
626 })?;
627
628 update_fn
629 .call(&mut self.store, dt)
630 .map_err(|e| ProbarError::WasmError {
631 message: format!("jugar_update failed: {e}"),
632 })?;
633
634 let execution_time = start.elapsed();
635 let state_hash = self.compute_state_hash();
636
637 #[allow(clippy::cast_possible_truncation)]
638 let execution_time_ns = execution_time.as_nanos() as u64;
639
640 Ok(FrameResult {
641 frame_number: self.store.data().frame_count,
642 state_hash,
643 execution_time_ns,
644 })
645 }
646
647 #[must_use]
649 pub fn compute_state_hash(&mut self) -> u64 {
650 let memory = self.get_memory();
651 let mut hasher = DefaultHasher::new();
652 memory.hash(&mut hasher);
653 hasher.finish()
654 }
655
656 #[must_use]
663 pub fn get_memory(&mut self) -> &[u8] {
664 let memory = self
665 .instance
666 .get_memory(&mut self.store, "memory")
667 .expect("memory export required");
668 memory.data(&self.store)
669 }
670
671 #[must_use]
673 pub const fn memory_view(&self) -> &MemoryView {
674 &self.memory_view
675 }
676
677 pub fn record_snapshot(&mut self) {
679 let memory = self.get_memory().to_vec();
680 self.store.data_mut().record_snapshot(&memory);
681 }
682
683 #[must_use]
685 pub fn frame_count(&self) -> u64 {
686 self.store.data().frame_count
687 }
688
689 #[must_use]
691 pub fn simulated_time(&self) -> f64 {
692 self.store.data().simulated_time
693 }
694}
695
696#[derive(Debug)]
698#[cfg(not(feature = "runtime"))]
699pub struct WasmRuntime {
700 _phantom: std::marker::PhantomData<()>,
701}
702
703#[cfg(not(feature = "runtime"))]
704impl WasmRuntime {
705 pub fn load(_wasm_bytes: &[u8]) -> ProbarResult<Self> {
711 Err(ProbarError::WasmError {
712 message: "WASM runtime requires 'runtime' feature".to_string(),
713 })
714 }
715}
716
717#[cfg(test)]
722#[allow(clippy::unwrap_used, clippy::expect_used)]
723mod tests {
724 use super::*;
725
726 mod entity_id_tests {
727 use super::*;
728
729 #[test]
730 fn test_entity_id_creation() {
731 let id = EntityId::new(42);
732 assert_eq!(id.raw(), 42);
733 }
734
735 #[test]
736 fn test_entity_id_equality() {
737 let id1 = EntityId::new(1);
738 let id2 = EntityId::new(1);
739 let id3 = EntityId::new(2);
740 assert_eq!(id1, id2);
741 assert_ne!(id1, id3);
742 }
743
744 #[test]
745 fn test_entity_id_hash() {
746 use std::collections::HashSet;
747 let mut set = HashSet::new();
748 set.insert(EntityId::new(1));
749 set.insert(EntityId::new(2));
750 set.insert(EntityId::new(1));
751 assert_eq!(set.len(), 2);
752 }
753 }
754
755 mod component_id_tests {
756 use super::*;
757
758 #[test]
759 fn test_component_id_of_type() {
760 let id1 = ComponentId::of::<u32>();
761 let id2 = ComponentId::of::<u32>();
762 let id3 = ComponentId::of::<f32>();
763 assert_eq!(id1, id2);
764 assert_ne!(id1, id3);
765 }
766
767 #[test]
768 fn test_component_id_raw() {
769 let id = ComponentId::of::<String>();
770 assert_ne!(id.raw(), 0);
771 }
772 }
773
774 mod state_delta_tests {
775 use super::*;
776
777 #[test]
778 fn test_empty_delta() {
779 let delta = StateDelta::empty(0);
780 assert_eq!(delta.base_frame, 0);
781 assert_eq!(delta.target_frame, 0);
782 assert!(delta.changes.is_empty());
783 }
784
785 #[test]
786 fn test_delta_compute_identical() {
787 let base = vec![1, 2, 3, 4, 5];
788 let current = vec![1, 2, 3, 4, 5];
789 let delta = StateDelta::compute(&base, ¤t, 0, 1);
790 assert!(delta.changes.is_empty());
791 }
792
793 #[test]
794 fn test_delta_compute_single_change() {
795 let base = vec![1, 2, 3, 4, 5];
796 let current = vec![1, 2, 99, 4, 5];
797 let delta = StateDelta::compute(&base, ¤t, 0, 1);
798 assert_eq!(delta.changes.len(), 1);
799 assert_eq!(delta.changes[0], (2, vec![99]));
800 }
801
802 #[test]
803 fn test_delta_compute_multiple_changes() {
804 let base = vec![1, 2, 3, 4, 5];
805 let current = vec![10, 2, 3, 40, 5];
806 let delta = StateDelta::compute(&base, ¤t, 0, 1);
807 assert_eq!(delta.changes.len(), 2);
808 }
809
810 #[test]
811 fn test_delta_compute_extension() {
812 let base = vec![1, 2, 3];
813 let current = vec![1, 2, 3, 4, 5];
814 let delta = StateDelta::compute(&base, ¤t, 0, 1);
815 assert!(!delta.changes.is_empty());
816 }
817
818 #[test]
819 fn test_delta_apply() {
820 let base = vec![1, 2, 3, 4, 5];
821 let current = vec![1, 99, 98, 4, 5];
822 let delta = StateDelta::compute(&base, ¤t, 0, 1);
823 let result = delta.apply(&base);
824 assert_eq!(result, current);
825 }
826
827 #[test]
828 fn test_delta_verify_checksum() {
829 let base = vec![1, 2, 3, 4, 5];
830 let current = vec![1, 99, 98, 4, 5];
831 let delta = StateDelta::compute(&base, ¤t, 0, 1);
832 let result = delta.apply(&base);
833 assert!(delta.verify(&result));
834 }
835
836 #[test]
837 fn test_delta_verify_checksum_fails() {
838 let base = vec![1, 2, 3, 4, 5];
839 let current = vec![1, 99, 98, 4, 5];
840 let delta = StateDelta::compute(&base, ¤t, 0, 1);
841 let wrong = vec![1, 2, 3, 4, 5];
842 assert!(!delta.verify(&wrong));
843 }
844 }
845
846 mod game_host_state_tests {
847 use super::*;
848
849 #[test]
850 fn test_host_state_default() {
851 let state = GameHostState::new();
852 assert!(state.input_queue.is_empty());
853 assert!((state.simulated_time - 0.0).abs() < f64::EPSILON);
854 assert_eq!(state.frame_count, 0);
855 }
856
857 #[test]
858 fn test_host_state_pop_input() {
859 let mut state = GameHostState::new();
860 state.input_queue.push_back(InputEvent::key_press("A"));
861 state.input_queue.push_back(InputEvent::key_press("B"));
862
863 let input1 = state.pop_input();
864 assert!(input1.is_some());
865
866 let input2 = state.pop_input();
867 assert!(input2.is_some());
868
869 let input3 = state.pop_input();
870 assert!(input3.is_none());
871 }
872
873 #[test]
874 fn test_host_state_record_snapshot() {
875 let mut state = GameHostState::new();
876 state.frame_count = 1;
877
878 let memory = vec![1, 2, 3, 4, 5];
879 state.record_snapshot(&memory);
880
881 assert_eq!(state.snapshot_deltas.len(), 1);
882 }
883
884 #[test]
885 fn test_host_state_multiple_snapshots() {
886 let mut state = GameHostState::new();
887
888 state.frame_count = 1;
889 state.record_snapshot(&[1, 2, 3]);
890
891 state.frame_count = 2;
892 state.record_snapshot(&[1, 2, 4]);
893
894 assert_eq!(state.snapshot_deltas.len(), 2);
895 }
896 }
897
898 mod memory_view_tests {
899 use super::*;
900
901 #[test]
902 fn test_memory_view_creation() {
903 let view = MemoryView::new(1024);
904 assert_eq!(view.size(), 1024);
905 }
906
907 #[test]
908 fn test_memory_view_with_entity_table() {
909 let view = MemoryView::new(1024).with_entity_table(100, 50);
910 assert_eq!(view.entity_table_offset(), 100);
911 assert_eq!(view.entity_count(), 50);
912 }
913
914 #[test]
915 fn test_memory_view_with_component_arrays() {
916 let view = MemoryView::new(1024).with_component_arrays(200);
917 assert_eq!(view.component_arrays_offset(), 200);
918 }
919
920 #[test]
921 fn test_memory_view_read_at() {
922 let view = MemoryView::new(1024);
923 let memory = vec![0u8, 0, 0, 0, 42, 0, 0, 0];
924 let value: u32 = unsafe { view.read_at(&memory, 4).unwrap() };
925 assert_eq!(value, 42);
926 }
927
928 #[test]
929 fn test_memory_view_read_at_out_of_bounds() {
930 let view = MemoryView::new(1024);
931 let memory = vec![0u8; 4];
932 let result: ProbarResult<u32> = unsafe { view.read_at(&memory, 8) };
933 assert!(result.is_err());
934 }
935
936 #[test]
937 fn test_memory_view_read_slice() {
938 let view = MemoryView::new(1024);
939 let memory = vec![1, 2, 3, 4, 5, 6, 7, 8];
940 let slice = view.read_slice(&memory, 2, 4).unwrap();
941 assert_eq!(slice, &[3, 4, 5, 6]);
942 }
943
944 #[test]
945 fn test_memory_view_read_slice_out_of_bounds() {
946 let view = MemoryView::new(1024);
947 let memory = vec![1, 2, 3, 4];
948 let result = view.read_slice(&memory, 2, 10);
949 assert!(result.is_err());
950 }
951 }
952
953 mod runtime_config_tests {
954 use super::*;
955
956 #[test]
957 fn test_config_default() {
958 let config = RuntimeConfig::default();
959 assert!(!config.wasm_threads);
960 assert!(config.wasm_simd);
961 assert!(config.wasm_reference_types);
962 assert_eq!(config.fuel_limit, 0);
963 }
964
965 #[test]
966 fn test_config_with_threads() {
967 let config = RuntimeConfig::new().with_threads(true);
968 assert!(config.wasm_threads);
969 }
970
971 #[test]
972 fn test_config_with_fuel_limit() {
973 let config = RuntimeConfig::new().with_fuel_limit(1000);
974 assert_eq!(config.fuel_limit, 1000);
975 }
976 }
977
978 mod frame_result_tests {
979 use super::*;
980
981 #[test]
982 fn test_frame_result_creation() {
983 let result = FrameResult {
984 frame_number: 100,
985 state_hash: 12345,
986 execution_time_ns: 1000,
987 };
988 assert_eq!(result.frame_number, 100);
989 assert_eq!(result.state_hash, 12345);
990 assert_eq!(result.execution_time_ns, 1000);
991 }
992 }
993
994 #[allow(clippy::useless_vec, clippy::items_after_statements, unused_imports)]
1003 mod wasm_module_loading_tests {
1004 #[allow(unused_imports)]
1005 use super::*;
1006
1007 #[test]
1009 fn test_wasm_invalid_corrupted_binary() {
1010 let corrupted_bytes = vec![0x00, 0x61, 0x73, 0x6D, 0xFF, 0xFF]; let result = std::panic::catch_unwind(|| {
1012 let is_valid =
1014 corrupted_bytes.len() >= 8 && corrupted_bytes[0..4] == [0x00, 0x61, 0x73, 0x6D];
1015 assert!(!is_valid || corrupted_bytes.len() < 8);
1016 });
1017 assert!(result.is_ok(), "Should not panic on corrupted binary");
1018 }
1019
1020 #[test]
1022 fn test_wasm_oversized_module_limit() {
1023 const MAX_MODULE_SIZE: usize = 100 * 1024 * 1024; let oversized_size = MAX_MODULE_SIZE + 1;
1025 assert!(oversized_size > MAX_MODULE_SIZE);
1027 }
1029
1030 #[test]
1032 fn test_wasm_missing_exports_detection() {
1033 let required_exports = ["__wasm_call_ctors", "update", "render"];
1034 let available_exports: Vec<&str> = vec!["update"]; let missing: Vec<_> = required_exports
1036 .iter()
1037 .filter(|e| !available_exports.contains(e))
1038 .collect();
1039 assert!(!missing.is_empty(), "Should detect missing exports");
1040 assert!(missing.contains(&&"render"));
1041 }
1042
1043 #[test]
1045 fn test_wasm_circular_import_detection() {
1046 let imports = vec![("a", "b"), ("b", "c"), ("c", "a")];
1048
1049 fn has_cycle(edges: &[(&str, &str)]) -> bool {
1050 use std::collections::{HashMap, HashSet};
1051 let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
1052 for (from, to) in edges {
1053 graph.entry(*from).or_default().push(*to);
1054 }
1055
1056 fn dfs<'a>(
1057 node: &'a str,
1058 graph: &HashMap<&'a str, Vec<&'a str>>,
1059 visited: &mut HashSet<&'a str>,
1060 rec_stack: &mut HashSet<&'a str>,
1061 ) -> bool {
1062 visited.insert(node);
1063 rec_stack.insert(node);
1064 if let Some(neighbors) = graph.get(node) {
1065 for &neighbor in neighbors {
1066 if !visited.contains(neighbor) {
1067 if dfs(neighbor, graph, visited, rec_stack) {
1068 return true;
1069 }
1070 } else if rec_stack.contains(neighbor) {
1071 return true;
1072 }
1073 }
1074 }
1075 rec_stack.remove(node);
1076 false
1077 }
1078
1079 let mut visited = HashSet::new();
1080 let mut rec_stack = HashSet::new();
1081 for (node, _) in edges {
1082 if !visited.contains(node) && dfs(node, &graph, &mut visited, &mut rec_stack) {
1083 return true;
1084 }
1085 }
1086 false
1087 }
1088
1089 assert!(has_cycle(&imports), "Should detect circular imports");
1090 }
1091
1092 #[test]
1094 fn test_wasm_concurrent_load_safety() {
1095 use std::sync::{
1096 atomic::{AtomicUsize, Ordering},
1097 Arc,
1098 };
1099 use std::thread;
1100
1101 let counter = Arc::new(AtomicUsize::new(0));
1102 let handles: Vec<_> = (0..10)
1103 .map(|_| {
1104 let c = Arc::clone(&counter);
1105 thread::spawn(move || {
1106 c.fetch_add(1, Ordering::SeqCst);
1107 })
1108 })
1109 .collect();
1110 for h in handles {
1111 h.join().unwrap();
1112 }
1113 assert_eq!(
1114 counter.load(Ordering::SeqCst),
1115 10,
1116 "All concurrent loads complete"
1117 );
1118 }
1119 }
1120
1121 #[allow(unused_imports, clippy::items_after_statements)]
1122 mod memory_safety_tests {
1123 #[allow(unused_imports)]
1124 use super::*;
1125
1126 #[test]
1128 fn test_stack_overflow_protection() {
1129 const MAX_RECURSION: usize = 1000;
1130 fn recursive_count(depth: usize, max: usize) -> usize {
1131 if depth >= max {
1132 depth
1133 } else {
1134 recursive_count(depth + 1, max)
1135 }
1136 }
1137 let result = recursive_count(0, MAX_RECURSION);
1138 assert_eq!(result, MAX_RECURSION, "Recursion limit enforced");
1139 }
1140
1141 #[test]
1143 fn test_memory_leak_detection() {
1144 let mut allocations: Vec<Vec<u8>> = Vec::new();
1145 const FRAMES: usize = 100;
1146 const ALLOC_SIZE: usize = 1024;
1147
1148 for _ in 0..FRAMES {
1149 allocations.push(vec![0u8; ALLOC_SIZE]);
1150 if allocations.len() > 10 {
1152 allocations.remove(0);
1153 }
1154 }
1155 assert!(allocations.len() <= 10, "Memory bounded over frames");
1157 }
1158
1159 #[test]
1161 fn test_no_double_free() {
1162 let data = Box::new(vec![1, 2, 3, 4, 5]);
1163 let raw = Box::into_raw(data);
1164 let recovered = unsafe { Box::from_raw(raw) };
1166 assert_eq!(recovered.len(), 5, "Single ownership prevents double-free");
1167 }
1169 }
1170
1171 #[allow(clippy::useless_vec, unused_imports)]
1172 mod execution_sandboxing_tests {
1173 #[allow(unused_imports)]
1174 use super::*;
1175
1176 #[test]
1178 fn test_wasm_fs_isolation() {
1179 let wasm_capabilities = vec!["memory", "table", "global"];
1182 assert!(!wasm_capabilities.contains(&"filesystem"));
1183 }
1184
1185 #[test]
1187 fn test_wasm_net_isolation() {
1188 let wasm_capabilities = vec!["memory", "table", "global"];
1189 assert!(!wasm_capabilities.contains(&"network"));
1190 }
1191
1192 #[test]
1194 fn test_wasm_proc_isolation() {
1195 let wasm_capabilities = vec!["memory", "table", "global"];
1196 assert!(!wasm_capabilities.contains(&"process"));
1197 }
1198
1199 #[test]
1201 fn test_timing_attack_mitigation() {
1202 let config = RuntimeConfig::new().with_fuel_limit(10000);
1203 assert!(
1204 config.fuel_limit > 0,
1205 "Fuel metering enabled for timing control"
1206 );
1207 }
1208 }
1209
1210 #[allow(clippy::useless_vec, unused_imports)]
1211 mod host_function_safety_tests {
1212 #[allow(unused_imports)]
1213 use super::*;
1214
1215 #[test]
1217 fn test_invalid_ptr_rejection() {
1218 let memory_size = 1024usize;
1219 let invalid_ptr = memory_size + 100; let is_valid = invalid_ptr < memory_size;
1221 assert!(!is_valid, "Invalid pointer detected and rejected");
1222 }
1223
1224 #[test]
1226 fn test_null_deref_handling() {
1227 let ptr: Option<&u32> = None;
1228 let result = ptr.copied();
1229 assert!(result.is_none(), "Null pointer safely handled via Option");
1230 }
1231
1232 #[test]
1234 fn test_buffer_overflow_prevention() {
1235 let buffer = vec![1u8, 2, 3, 4, 5];
1236 let offset = 10usize;
1237 let result = buffer.get(offset);
1238 assert!(result.is_none(), "Bounds checking prevents overflow");
1239 }
1240
1241 #[test]
1243 fn test_type_confusion_prevention() {
1244 let value: u32 = 42;
1246 let typed_value: u32 = value; assert_eq!(typed_value, 42, "Type safety enforced");
1248 }
1249
1250 #[test]
1252 fn test_reentrancy_prevention() {
1253 use std::cell::RefCell;
1254 use std::panic::AssertUnwindSafe;
1255
1256 let cell = RefCell::new(0);
1257 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1258 let _borrow1 = cell.borrow_mut();
1259 let _borrow2 = cell.borrow_mut(); }));
1261 assert!(result.is_err(), "Reentrancy detected via RefCell");
1262 }
1263 }
1264}