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