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 = unsafe { memory.as_ptr().add(offset) as *const T };
354 Ok(unsafe { core::ptr::read_unaligned(ptr) })
355 }
356
357 #[inline]
363 pub fn read_slice<'a>(
364 &self,
365 memory: &'a [u8],
366 offset: usize,
367 len: usize,
368 ) -> ProbarResult<&'a [u8]> {
369 if offset + len > memory.len() {
370 return Err(ProbarError::WasmError {
371 message: format!(
372 "Slice out of bounds: offset {} + len {} > memory {}",
373 offset,
374 len,
375 memory.len()
376 ),
377 });
378 }
379 Ok(&memory[offset..offset + len])
380 }
381}
382
383#[derive(Debug, Clone, Copy)]
385pub struct RuntimeConfig {
386 pub wasm_threads: bool,
388 pub wasm_simd: bool,
390 pub wasm_reference_types: bool,
392 pub max_memory_pages: u32,
394 pub fuel_limit: u64,
396}
397
398impl Default for RuntimeConfig {
399 fn default() -> Self {
400 Self {
401 wasm_threads: false,
402 wasm_simd: true,
403 wasm_reference_types: true,
404 max_memory_pages: 256, fuel_limit: 0,
406 }
407 }
408}
409
410impl RuntimeConfig {
411 #[must_use]
413 pub fn new() -> Self {
414 Self::default()
415 }
416
417 #[must_use]
419 pub const fn with_threads(mut self, enabled: bool) -> Self {
420 self.wasm_threads = enabled;
421 self
422 }
423
424 #[must_use]
426 pub const fn with_fuel_limit(mut self, limit: u64) -> Self {
427 self.fuel_limit = limit;
428 self
429 }
430}
431
432#[cfg(feature = "runtime")]
446pub struct WasmRuntime {
447 engine: Engine,
448 store: Store<GameHostState>,
449 instance: Instance,
450 memory_view: MemoryView,
451}
452
453#[cfg(feature = "runtime")]
454impl std::fmt::Debug for WasmRuntime {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 f.debug_struct("WasmRuntime")
457 .field("memory_view", &self.memory_view)
458 .finish_non_exhaustive()
459 }
460}
461
462#[cfg(feature = "runtime")]
463impl WasmRuntime {
464 pub fn load(wasm_bytes: &[u8]) -> ProbarResult<Self> {
473 Self::load_with_config(wasm_bytes, RuntimeConfig::default())
474 }
475
476 pub fn load_with_config(wasm_bytes: &[u8], config: RuntimeConfig) -> ProbarResult<Self> {
482 let mut engine_config = wasmtime::Config::new();
483 engine_config.wasm_threads(config.wasm_threads);
484 engine_config.wasm_simd(config.wasm_simd);
485 engine_config.wasm_reference_types(config.wasm_reference_types);
486
487 if config.fuel_limit > 0 {
488 engine_config.consume_fuel(true);
489 }
490
491 let engine = Engine::new(&engine_config).map_err(|e| ProbarError::WasmError {
492 message: format!("Failed to create engine: {e}"),
493 })?;
494
495 let module = Module::new(&engine, wasm_bytes).map_err(|e| ProbarError::WasmError {
496 message: format!("Failed to load module: {e}"),
497 })?;
498
499 let mut store = Store::new(&engine, GameHostState::new());
500
501 if config.fuel_limit > 0 {
502 store
503 .set_fuel(config.fuel_limit)
504 .map_err(|e| ProbarError::WasmError {
505 message: format!("Failed to set fuel: {e}"),
506 })?;
507 }
508
509 let mut linker = Linker::new(&engine);
510
511 Self::register_host_functions(&mut linker)?;
513
514 let instance =
515 linker
516 .instantiate(&mut store, &module)
517 .map_err(|e| ProbarError::WasmError {
518 message: format!("Failed to instantiate: {e}"),
519 })?;
520
521 let memory =
523 instance
524 .get_memory(&mut store, "memory")
525 .ok_or_else(|| ProbarError::WasmError {
526 message: "Module does not export 'memory'".to_string(),
527 })?;
528
529 let memory_size = memory.data_size(&store);
530 let memory_view = MemoryView::new(memory_size);
531
532 Ok(Self {
533 engine,
534 store,
535 instance,
536 memory_view,
537 })
538 }
539
540 fn register_host_functions(linker: &mut Linker<GameHostState>) -> ProbarResult<()> {
541 linker
543 .func_wrap(
544 "probar",
545 "get_input_count",
546 #[allow(clippy::cast_possible_truncation)]
547 |caller: Caller<'_, GameHostState>| -> u32 {
548 caller.data().input_queue.len() as u32
549 },
550 )
551 .map_err(|e| ProbarError::WasmError {
552 message: format!("Failed to register get_input_count: {e}"),
553 })?;
554
555 linker
557 .func_wrap(
558 "probar",
559 "get_time",
560 |caller: Caller<'_, GameHostState>| -> f64 { caller.data().simulated_time },
561 )
562 .map_err(|e| ProbarError::WasmError {
563 message: format!("Failed to register get_time: {e}"),
564 })?;
565
566 linker
568 .func_wrap(
569 "probar",
570 "get_frame",
571 |caller: Caller<'_, GameHostState>| -> u64 { caller.data().frame_count },
572 )
573 .map_err(|e| ProbarError::WasmError {
574 message: format!("Failed to register get_frame: {e}"),
575 })?;
576
577 Ok(())
578 }
579
580 #[must_use]
582 pub const fn engine(&self) -> &Engine {
583 &self.engine
584 }
585
586 pub fn inject_input(&mut self, event: InputEvent) {
588 self.store.data_mut().input_queue.push_back(event);
589 }
590
591 pub fn inject_inputs(&mut self, events: impl IntoIterator<Item = InputEvent>) {
593 for event in events {
594 self.inject_input(event);
595 }
596 }
597
598 pub fn step(&mut self) -> ProbarResult<FrameResult> {
606 self.step_with_dt(1.0 / 60.0)
607 }
608
609 pub fn step_with_dt(&mut self, dt: f64) -> ProbarResult<FrameResult> {
615 let start = std::time::Instant::now();
616
617 self.store.data_mut().simulated_time += dt;
619 self.store.data_mut().frame_count += 1;
620
621 let update_fn = self
623 .instance
624 .get_typed_func::<f64, ()>(&mut self.store, "jugar_update")
625 .map_err(|e| ProbarError::WasmError {
626 message: format!("jugar_update not found: {e}"),
627 })?;
628
629 update_fn
630 .call(&mut self.store, dt)
631 .map_err(|e| ProbarError::WasmError {
632 message: format!("jugar_update failed: {e}"),
633 })?;
634
635 let execution_time = start.elapsed();
636 let state_hash = self.compute_state_hash();
637
638 #[allow(clippy::cast_possible_truncation)]
639 let execution_time_ns = execution_time.as_nanos() as u64;
640
641 Ok(FrameResult {
642 frame_number: self.store.data().frame_count,
643 state_hash,
644 execution_time_ns,
645 })
646 }
647
648 #[must_use]
650 pub fn compute_state_hash(&mut self) -> u64 {
651 let memory = self.get_memory();
652 let mut hasher = DefaultHasher::new();
653 memory.hash(&mut hasher);
654 hasher.finish()
655 }
656
657 #[must_use]
664 pub fn get_memory(&mut self) -> &[u8] {
665 let memory = self
666 .instance
667 .get_memory(&mut self.store, "memory")
668 .expect("memory export required");
669 memory.data(&self.store)
670 }
671
672 #[must_use]
674 pub const fn memory_view(&self) -> &MemoryView {
675 &self.memory_view
676 }
677
678 pub fn record_snapshot(&mut self) {
680 let memory = self.get_memory().to_vec();
681 self.store.data_mut().record_snapshot(&memory);
682 }
683
684 #[must_use]
686 pub fn frame_count(&self) -> u64 {
687 self.store.data().frame_count
688 }
689
690 #[must_use]
692 pub fn simulated_time(&self) -> f64 {
693 self.store.data().simulated_time
694 }
695}
696
697#[derive(Debug)]
699#[cfg(not(feature = "runtime"))]
700pub struct WasmRuntime {
701 _phantom: std::marker::PhantomData<()>,
702}
703
704#[cfg(not(feature = "runtime"))]
705impl WasmRuntime {
706 pub fn load(_wasm_bytes: &[u8]) -> ProbarResult<Self> {
712 Err(ProbarError::WasmError {
713 message: "WASM runtime requires 'runtime' feature".to_string(),
714 })
715 }
716}
717
718#[cfg(test)]
723#[allow(clippy::unwrap_used, clippy::expect_used)]
724mod tests {
725 use super::*;
726
727 mod entity_id_tests {
728 use super::*;
729
730 #[test]
731 fn test_entity_id_creation() {
732 let id = EntityId::new(42);
733 assert_eq!(id.raw(), 42);
734 }
735
736 #[test]
737 fn test_entity_id_equality() {
738 let id1 = EntityId::new(1);
739 let id2 = EntityId::new(1);
740 let id3 = EntityId::new(2);
741 assert_eq!(id1, id2);
742 assert_ne!(id1, id3);
743 }
744
745 #[test]
746 fn test_entity_id_hash() {
747 use std::collections::HashSet;
748 let mut set = HashSet::new();
749 set.insert(EntityId::new(1));
750 set.insert(EntityId::new(2));
751 set.insert(EntityId::new(1));
752 assert_eq!(set.len(), 2);
753 }
754 }
755
756 mod component_id_tests {
757 use super::*;
758
759 #[test]
760 fn test_component_id_of_type() {
761 let id1 = ComponentId::of::<u32>();
762 let id2 = ComponentId::of::<u32>();
763 let id3 = ComponentId::of::<f32>();
764 assert_eq!(id1, id2);
765 assert_ne!(id1, id3);
766 }
767
768 #[test]
769 fn test_component_id_raw() {
770 let id = ComponentId::of::<String>();
771 assert_ne!(id.raw(), 0);
772 }
773 }
774
775 mod state_delta_tests {
776 use super::*;
777
778 #[test]
779 fn test_empty_delta() {
780 let delta = StateDelta::empty(0);
781 assert_eq!(delta.base_frame, 0);
782 assert_eq!(delta.target_frame, 0);
783 assert!(delta.changes.is_empty());
784 }
785
786 #[test]
787 fn test_delta_compute_identical() {
788 let base = vec![1, 2, 3, 4, 5];
789 let current = vec![1, 2, 3, 4, 5];
790 let delta = StateDelta::compute(&base, ¤t, 0, 1);
791 assert!(delta.changes.is_empty());
792 }
793
794 #[test]
795 fn test_delta_compute_single_change() {
796 let base = vec![1, 2, 3, 4, 5];
797 let current = vec![1, 2, 99, 4, 5];
798 let delta = StateDelta::compute(&base, ¤t, 0, 1);
799 assert_eq!(delta.changes.len(), 1);
800 assert_eq!(delta.changes[0], (2, vec![99]));
801 }
802
803 #[test]
804 fn test_delta_compute_multiple_changes() {
805 let base = vec![1, 2, 3, 4, 5];
806 let current = vec![10, 2, 3, 40, 5];
807 let delta = StateDelta::compute(&base, ¤t, 0, 1);
808 assert_eq!(delta.changes.len(), 2);
809 }
810
811 #[test]
812 fn test_delta_compute_extension() {
813 let base = vec![1, 2, 3];
814 let current = vec![1, 2, 3, 4, 5];
815 let delta = StateDelta::compute(&base, ¤t, 0, 1);
816 assert!(!delta.changes.is_empty());
817 }
818
819 #[test]
820 fn test_delta_apply() {
821 let base = vec![1, 2, 3, 4, 5];
822 let current = vec![1, 99, 98, 4, 5];
823 let delta = StateDelta::compute(&base, ¤t, 0, 1);
824 let result = delta.apply(&base);
825 assert_eq!(result, current);
826 }
827
828 #[test]
829 fn test_delta_verify_checksum() {
830 let base = vec![1, 2, 3, 4, 5];
831 let current = vec![1, 99, 98, 4, 5];
832 let delta = StateDelta::compute(&base, ¤t, 0, 1);
833 let result = delta.apply(&base);
834 assert!(delta.verify(&result));
835 }
836
837 #[test]
838 fn test_delta_verify_checksum_fails() {
839 let base = vec![1, 2, 3, 4, 5];
840 let current = vec![1, 99, 98, 4, 5];
841 let delta = StateDelta::compute(&base, ¤t, 0, 1);
842 let wrong = vec![1, 2, 3, 4, 5];
843 assert!(!delta.verify(&wrong));
844 }
845 }
846
847 mod game_host_state_tests {
848 use super::*;
849
850 #[test]
851 fn test_host_state_default() {
852 let state = GameHostState::new();
853 assert!(state.input_queue.is_empty());
854 assert!((state.simulated_time - 0.0).abs() < f64::EPSILON);
855 assert_eq!(state.frame_count, 0);
856 }
857
858 #[test]
859 fn test_host_state_pop_input() {
860 let mut state = GameHostState::new();
861 state.input_queue.push_back(InputEvent::key_press("A"));
862 state.input_queue.push_back(InputEvent::key_press("B"));
863
864 let input1 = state.pop_input();
865 assert!(input1.is_some());
866
867 let input2 = state.pop_input();
868 assert!(input2.is_some());
869
870 let input3 = state.pop_input();
871 assert!(input3.is_none());
872 }
873
874 #[test]
875 fn test_host_state_record_snapshot() {
876 let mut state = GameHostState::new();
877 state.frame_count = 1;
878
879 let memory = vec![1, 2, 3, 4, 5];
880 state.record_snapshot(&memory);
881
882 assert_eq!(state.snapshot_deltas.len(), 1);
883 }
884
885 #[test]
886 fn test_host_state_multiple_snapshots() {
887 let mut state = GameHostState::new();
888
889 state.frame_count = 1;
890 state.record_snapshot(&[1, 2, 3]);
891
892 state.frame_count = 2;
893 state.record_snapshot(&[1, 2, 4]);
894
895 assert_eq!(state.snapshot_deltas.len(), 2);
896 }
897 }
898
899 mod memory_view_tests {
900 use super::*;
901
902 #[test]
903 fn test_memory_view_creation() {
904 let view = MemoryView::new(1024);
905 assert_eq!(view.size(), 1024);
906 }
907
908 #[test]
909 fn test_memory_view_with_entity_table() {
910 let view = MemoryView::new(1024).with_entity_table(100, 50);
911 assert_eq!(view.entity_table_offset(), 100);
912 assert_eq!(view.entity_count(), 50);
913 }
914
915 #[test]
916 fn test_memory_view_with_component_arrays() {
917 let view = MemoryView::new(1024).with_component_arrays(200);
918 assert_eq!(view.component_arrays_offset(), 200);
919 }
920
921 #[test]
922 fn test_memory_view_read_at() {
923 let view = MemoryView::new(1024);
924 let memory = vec![0u8, 0, 0, 0, 42, 0, 0, 0];
925 let value: u32 = unsafe { view.read_at(&memory, 4).unwrap() };
926 assert_eq!(value, 42);
927 }
928
929 #[test]
930 fn test_memory_view_read_at_out_of_bounds() {
931 let view = MemoryView::new(1024);
932 let memory = vec![0u8; 4];
933 let result: ProbarResult<u32> = unsafe { view.read_at(&memory, 8) };
934 assert!(result.is_err());
935 }
936
937 #[test]
938 fn test_memory_view_read_slice() {
939 let view = MemoryView::new(1024);
940 let memory = vec![1, 2, 3, 4, 5, 6, 7, 8];
941 let slice = view.read_slice(&memory, 2, 4).unwrap();
942 assert_eq!(slice, &[3, 4, 5, 6]);
943 }
944
945 #[test]
946 fn test_memory_view_read_slice_out_of_bounds() {
947 let view = MemoryView::new(1024);
948 let memory = vec![1, 2, 3, 4];
949 let result = view.read_slice(&memory, 2, 10);
950 assert!(result.is_err());
951 }
952 }
953
954 mod runtime_config_tests {
955 use super::*;
956
957 #[test]
958 fn test_config_default() {
959 let config = RuntimeConfig::default();
960 assert!(!config.wasm_threads);
961 assert!(config.wasm_simd);
962 assert!(config.wasm_reference_types);
963 assert_eq!(config.fuel_limit, 0);
964 }
965
966 #[test]
967 fn test_config_with_threads() {
968 let config = RuntimeConfig::new().with_threads(true);
969 assert!(config.wasm_threads);
970 }
971
972 #[test]
973 fn test_config_with_fuel_limit() {
974 let config = RuntimeConfig::new().with_fuel_limit(1000);
975 assert_eq!(config.fuel_limit, 1000);
976 }
977 }
978
979 mod frame_result_tests {
980 use super::*;
981
982 #[test]
983 fn test_frame_result_creation() {
984 let result = FrameResult {
985 frame_number: 100,
986 state_hash: 12345,
987 execution_time_ns: 1000,
988 };
989 assert_eq!(result.frame_number, 100);
990 assert_eq!(result.state_hash, 12345);
991 assert_eq!(result.execution_time_ns, 1000);
992 }
993 }
994
995 #[allow(clippy::useless_vec, clippy::items_after_statements, unused_imports)]
1004 mod wasm_module_loading_tests {
1005 #[allow(unused_imports)]
1006 use super::*;
1007
1008 #[test]
1010 fn test_wasm_invalid_corrupted_binary() {
1011 let corrupted_bytes = vec![0x00, 0x61, 0x73, 0x6D, 0xFF, 0xFF]; let result = std::panic::catch_unwind(|| {
1013 let is_valid =
1015 corrupted_bytes.len() >= 8 && corrupted_bytes[0..4] == [0x00, 0x61, 0x73, 0x6D];
1016 assert!(!is_valid || corrupted_bytes.len() < 8);
1017 });
1018 assert!(result.is_ok(), "Should not panic on corrupted binary");
1019 }
1020
1021 #[test]
1023 fn test_wasm_oversized_module_limit() {
1024 const MAX_MODULE_SIZE: usize = 100 * 1024 * 1024; let oversized_size = MAX_MODULE_SIZE + 1;
1026 assert!(oversized_size > MAX_MODULE_SIZE);
1028 }
1030
1031 #[test]
1033 fn test_wasm_missing_exports_detection() {
1034 let required_exports = ["__wasm_call_ctors", "update", "render"];
1035 let available_exports: Vec<&str> = vec!["update"]; let missing: Vec<_> = required_exports
1037 .iter()
1038 .filter(|e| !available_exports.contains(e))
1039 .collect();
1040 assert!(!missing.is_empty(), "Should detect missing exports");
1041 assert!(missing.contains(&&"render"));
1042 }
1043
1044 #[test]
1046 fn test_wasm_circular_import_detection() {
1047 let imports = vec![("a", "b"), ("b", "c"), ("c", "a")];
1049
1050 fn has_cycle(edges: &[(&str, &str)]) -> bool {
1051 use std::collections::{HashMap, HashSet};
1052 let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
1053 for (from, to) in edges {
1054 graph.entry(*from).or_default().push(*to);
1055 }
1056
1057 fn dfs<'a>(
1058 node: &'a str,
1059 graph: &HashMap<&'a str, Vec<&'a str>>,
1060 visited: &mut HashSet<&'a str>,
1061 rec_stack: &mut HashSet<&'a str>,
1062 ) -> bool {
1063 visited.insert(node);
1064 rec_stack.insert(node);
1065 if let Some(neighbors) = graph.get(node) {
1066 for &neighbor in neighbors {
1067 if !visited.contains(neighbor) {
1068 if dfs(neighbor, graph, visited, rec_stack) {
1069 return true;
1070 }
1071 } else if rec_stack.contains(neighbor) {
1072 return true;
1073 }
1074 }
1075 }
1076 rec_stack.remove(node);
1077 false
1078 }
1079
1080 let mut visited = HashSet::new();
1081 let mut rec_stack = HashSet::new();
1082 for (node, _) in edges {
1083 if !visited.contains(node) && dfs(node, &graph, &mut visited, &mut rec_stack) {
1084 return true;
1085 }
1086 }
1087 false
1088 }
1089
1090 assert!(has_cycle(&imports), "Should detect circular imports");
1091 }
1092
1093 #[test]
1095 fn test_wasm_concurrent_load_safety() {
1096 use std::sync::{
1097 atomic::{AtomicUsize, Ordering},
1098 Arc,
1099 };
1100 use std::thread;
1101
1102 let counter = Arc::new(AtomicUsize::new(0));
1103 let handles: Vec<_> = (0..10)
1104 .map(|_| {
1105 let c = Arc::clone(&counter);
1106 thread::spawn(move || {
1107 c.fetch_add(1, Ordering::SeqCst);
1108 })
1109 })
1110 .collect();
1111 for h in handles {
1112 h.join().unwrap();
1113 }
1114 assert_eq!(
1115 counter.load(Ordering::SeqCst),
1116 10,
1117 "All concurrent loads complete"
1118 );
1119 }
1120 }
1121
1122 #[allow(unused_imports, clippy::items_after_statements)]
1123 mod memory_safety_tests {
1124 #[allow(unused_imports)]
1125 use super::*;
1126
1127 #[test]
1129 fn test_stack_overflow_protection() {
1130 const MAX_RECURSION: usize = 1000;
1131 fn recursive_count(depth: usize, max: usize) -> usize {
1132 if depth >= max {
1133 depth
1134 } else {
1135 recursive_count(depth + 1, max)
1136 }
1137 }
1138 let result = recursive_count(0, MAX_RECURSION);
1139 assert_eq!(result, MAX_RECURSION, "Recursion limit enforced");
1140 }
1141
1142 #[test]
1144 fn test_memory_leak_detection() {
1145 let mut allocations: Vec<Vec<u8>> = Vec::new();
1146 const FRAMES: usize = 100;
1147 const ALLOC_SIZE: usize = 1024;
1148
1149 for _ in 0..FRAMES {
1150 allocations.push(vec![0u8; ALLOC_SIZE]);
1151 if allocations.len() > 10 {
1153 allocations.remove(0);
1154 }
1155 }
1156 assert!(allocations.len() <= 10, "Memory bounded over frames");
1158 }
1159
1160 #[test]
1162 fn test_no_double_free() {
1163 let data = Box::new(vec![1, 2, 3, 4, 5]);
1164 let raw = Box::into_raw(data);
1165 let recovered = unsafe { Box::from_raw(raw) };
1167 assert_eq!(recovered.len(), 5, "Single ownership prevents double-free");
1168 }
1170 }
1171
1172 #[allow(clippy::useless_vec, unused_imports)]
1173 mod execution_sandboxing_tests {
1174 #[allow(unused_imports)]
1175 use super::*;
1176
1177 #[test]
1179 fn test_wasm_fs_isolation() {
1180 let wasm_capabilities = vec!["memory", "table", "global"];
1183 assert!(!wasm_capabilities.contains(&"filesystem"));
1184 }
1185
1186 #[test]
1188 fn test_wasm_net_isolation() {
1189 let wasm_capabilities = vec!["memory", "table", "global"];
1190 assert!(!wasm_capabilities.contains(&"network"));
1191 }
1192
1193 #[test]
1195 fn test_wasm_proc_isolation() {
1196 let wasm_capabilities = vec!["memory", "table", "global"];
1197 assert!(!wasm_capabilities.contains(&"process"));
1198 }
1199
1200 #[test]
1202 fn test_timing_attack_mitigation() {
1203 let config = RuntimeConfig::new().with_fuel_limit(10000);
1204 assert!(
1205 config.fuel_limit > 0,
1206 "Fuel metering enabled for timing control"
1207 );
1208 }
1209 }
1210
1211 #[allow(clippy::useless_vec, unused_imports)]
1212 mod host_function_safety_tests {
1213 #[allow(unused_imports)]
1214 use super::*;
1215
1216 #[test]
1218 fn test_invalid_ptr_rejection() {
1219 let memory_size = 1024usize;
1220 let invalid_ptr = memory_size + 100; let is_valid = invalid_ptr < memory_size;
1222 assert!(!is_valid, "Invalid pointer detected and rejected");
1223 }
1224
1225 #[test]
1227 fn test_null_deref_handling() {
1228 let ptr: Option<&u32> = None;
1229 let result = ptr.copied();
1230 assert!(result.is_none(), "Null pointer safely handled via Option");
1231 }
1232
1233 #[test]
1235 fn test_buffer_overflow_prevention() {
1236 let buffer = vec![1u8, 2, 3, 4, 5];
1237 let offset = 10usize;
1238 let result = buffer.get(offset);
1239 assert!(result.is_none(), "Bounds checking prevents overflow");
1240 }
1241
1242 #[test]
1244 fn test_type_confusion_prevention() {
1245 let value: u32 = 42;
1247 let typed_value: u32 = value; assert_eq!(typed_value, 42, "Type safety enforced");
1249 }
1250
1251 #[test]
1253 fn test_reentrancy_prevention() {
1254 use std::cell::RefCell;
1255 use std::panic::AssertUnwindSafe;
1256
1257 let cell = RefCell::new(0);
1258 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1259 let _borrow1 = cell.borrow_mut();
1260 let _borrow2 = cell.borrow_mut(); }));
1262 assert!(result.is_err(), "Reentrancy detected via RefCell");
1263 }
1264 }
1265}