1#![allow(dead_code)]
9
10use alloc::{
11 collections::BTreeMap,
12 format,
13 string::{String, ToString},
14 vec::Vec,
15};
16
17use super::{dom_bindings::DomApi, js_gc::GcHeap, js_vm::JsVm, tabs::TabId};
18
19#[derive(Debug, Clone)]
25pub enum TabError {
26 ProcessLimitReached,
28 ProcessAlreadyExists,
30 ProcessNotFound,
32 InvalidState { expected: &'static str },
34 CapabilityDenied { capability: &'static str },
36 ResourceLimitViolation { message: String },
38 ScriptError { message: String },
40}
41
42impl core::fmt::Display for TabError {
43 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44 match self {
45 Self::ProcessLimitReached => write!(f, "Maximum process limit reached"),
46 Self::ProcessAlreadyExists => write!(f, "Process already exists for this tab"),
47 Self::ProcessNotFound => write!(f, "No process for tab"),
48 Self::InvalidState { expected } => {
49 write!(f, "Tab is not in {} state", expected)
50 }
51 Self::CapabilityDenied { capability } => {
52 write!(f, "{} not permitted", capability)
53 }
54 Self::ResourceLimitViolation { message } => write!(f, "{}", message),
55 Self::ScriptError { message } => write!(f, "{}", message),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66pub enum TabProcessState {
67 #[default]
69 Created,
70 Running,
72 Suspended,
74 Crashed,
76 Terminated,
78}
79
80#[derive(Debug, Clone)]
86pub struct ResourceLimits {
87 pub max_heap_bytes: usize,
89 pub max_dom_nodes: usize,
91 pub max_timers: usize,
93 pub max_steps_per_tick: usize,
95 pub max_connections: usize,
97 pub cpu_budget_us: u64,
99}
100
101impl Default for ResourceLimits {
102 fn default() -> Self {
103 Self {
104 max_heap_bytes: 64 * 1024 * 1024, max_dom_nodes: 100_000,
106 max_timers: 1000,
107 max_steps_per_tick: 1_000_000,
108 max_connections: 16,
109 cpu_budget_us: 50_000, }
111 }
112}
113
114#[derive(Debug, Clone, Default)]
120pub struct ResourceUsage {
121 pub heap_bytes: usize,
123 pub dom_node_count: usize,
125 pub timer_count: usize,
127 pub total_steps: u64,
129 pub steps_this_tick: usize,
131 pub ticks_processed: u64,
133 pub gc_collections: usize,
135 pub crash_count: usize,
137}
138
139#[derive(Debug, Clone)]
145pub enum IpcMessage {
146 PostMessage {
148 source_tab: TabId,
149 target_tab: TabId,
150 origin: String,
151 data: String,
152 },
153 BroadcastMessage {
155 source_tab: TabId,
156 channel: String,
157 data: String,
158 },
159 StorageEvent {
161 key: String,
162 old_value: Option<String>,
163 new_value: Option<String>,
164 },
165 TabEvent {
167 tab_id: TabId,
168 event: TabLifecycleEvent,
169 },
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum TabLifecycleEvent {
175 Created,
177 Activated,
179 Deactivated,
181 BeforeUnload,
183 Closed,
185 Crashed,
187 Recovered,
189}
190
191pub struct TabProcess {
197 pub tab_id: TabId,
199 pub state: TabProcessState,
201 pub vm: JsVm,
203 pub gc: GcHeap,
205 pub dom_api: DomApi,
207 pub limits: ResourceLimits,
209 pub usage: ResourceUsage,
211 pub inbox: Vec<IpcMessage>,
213 pub capabilities: TabCapabilities,
215 pub last_error: Option<String>,
217 pub origin: String,
219}
220
221impl TabProcess {
222 pub fn new(tab_id: TabId) -> Self {
223 Self {
224 tab_id,
225 state: TabProcessState::Created,
226 vm: JsVm::new(),
227 gc: GcHeap::new(),
228 dom_api: DomApi::new(),
229 limits: ResourceLimits::default(),
230 usage: ResourceUsage::default(),
231 inbox: Vec::new(),
232 capabilities: TabCapabilities::default_web(),
233 last_error: None,
234 origin: String::new(),
235 }
236 }
237
238 pub fn start(&mut self) {
240 self.state = TabProcessState::Running;
241 }
242
243 pub fn suspend(&mut self) {
245 if self.state == TabProcessState::Running {
246 self.state = TabProcessState::Suspended;
247 }
248 }
249
250 pub fn resume(&mut self) {
252 if self.state == TabProcessState::Suspended {
253 self.state = TabProcessState::Running;
254 }
255 }
256
257 pub fn crash(&mut self, error: &str) {
259 self.state = TabProcessState::Crashed;
260 self.last_error = Some(error.to_string());
261 self.usage.crash_count += 1;
262 }
263
264 pub fn terminate(&mut self) {
266 self.state = TabProcessState::Terminated;
267 self.vm = JsVm::new(); self.gc = GcHeap::new(); self.dom_api = DomApi::new(); self.inbox.clear();
271 }
272
273 pub fn check_limits(&self) -> Option<TabError> {
275 if self.usage.heap_bytes > self.limits.max_heap_bytes {
276 return Some(TabError::ResourceLimitViolation {
277 message: String::from("Heap size limit exceeded"),
278 });
279 }
280 if self.usage.dom_node_count > self.limits.max_dom_nodes {
281 return Some(TabError::ResourceLimitViolation {
282 message: String::from("DOM node limit exceeded"),
283 });
284 }
285 if self.usage.timer_count > self.limits.max_timers {
286 return Some(TabError::ResourceLimitViolation {
287 message: String::from("Timer limit exceeded"),
288 });
289 }
290 if self.usage.steps_this_tick > self.limits.max_steps_per_tick {
291 return Some(TabError::ResourceLimitViolation {
292 message: String::from("CPU budget exceeded for this tick"),
293 });
294 }
295 None
296 }
297
298 pub fn update_usage(&mut self) {
300 self.usage.heap_bytes = self.gc.arena.bytes_allocated();
301 self.usage.dom_node_count = self.dom_api.node_count();
302 self.usage.timer_count = self.dom_api.timer_queue.pending_count();
303 }
304
305 pub fn tick(&mut self) -> Result<(), TabError> {
307 if self.state != TabProcessState::Running {
308 return Ok(());
309 }
310
311 self.usage.steps_this_tick = 0;
312 self.usage.ticks_processed += 1;
313
314 let expired = self.dom_api.timer_queue.tick();
316 for _callback_id in expired {
317 }
320
321 let messages = core::mem::take(&mut self.inbox);
323 for _msg in messages {
324 }
326
327 if self.gc.should_collect() {
329 self.gc.collect(&self.vm);
330 self.usage.gc_collections += 1;
331 }
332
333 self.update_usage();
335
336 if let Some(violation) = self.check_limits() {
338 let msg = format!("{}", violation);
339 self.crash(&msg);
340 return Err(violation);
341 }
342
343 Ok(())
344 }
345
346 pub fn execute_script(&mut self, source: &str) -> Result<(), TabError> {
348 if self.state != TabProcessState::Running {
349 return Err(TabError::InvalidState {
350 expected: "running",
351 });
352 }
353
354 if !self.capabilities.can_execute_js {
356 return Err(TabError::CapabilityDenied {
357 capability: "JavaScript execution",
358 });
359 }
360
361 let mut parser = super::js_parser::JsParser::from_source(source);
362 let root = parser.parse();
363
364 if !parser.errors.is_empty() {
365 let err = parser.errors.join("; ");
366 self.last_error = Some(err.clone());
367 return Err(TabError::ScriptError { message: err });
368 }
369
370 let mut compiler = super::js_compiler::Compiler::new();
371 let chunk = compiler.compile(&parser.arena, root);
372
373 match self.vm.run_chunk(&chunk) {
374 Ok(_) => {
375 self.update_usage();
376 if let Some(violation) = self.check_limits() {
377 let msg = format!("{}", violation);
378 self.crash(&msg);
379 return Err(violation);
380 }
381 Ok(())
382 }
383 Err(e) => {
384 let msg = format!("{}", e);
385 self.last_error = Some(msg.clone());
386 Err(TabError::ScriptError { message: msg })
387 }
388 }
389 }
390
391 pub fn set_origin(&mut self, origin: &str) {
393 self.origin = origin.to_string();
394 }
395
396 pub fn is_same_origin(&self, target_origin: &str) -> bool {
398 self.origin == target_origin
399 }
400}
401
402#[derive(Debug, Clone)]
408pub struct TabCapabilities {
409 pub can_execute_js: bool,
411 pub can_local_storage: bool,
413 pub can_timers: bool,
415 pub can_network: bool,
417 pub can_post_message: bool,
419 pub can_geolocation: bool,
421 pub can_clipboard: bool,
423 pub can_notifications: bool,
425 pub can_media_devices: bool,
427 pub can_popups: bool,
429}
430
431impl TabCapabilities {
432 pub fn default_web() -> Self {
434 Self {
435 can_execute_js: true,
436 can_local_storage: true,
437 can_timers: true,
438 can_network: true,
439 can_post_message: true,
440 can_geolocation: false,
441 can_clipboard: false,
442 can_notifications: false,
443 can_media_devices: false,
444 can_popups: false,
445 }
446 }
447
448 pub fn sandboxed() -> Self {
450 Self {
451 can_execute_js: false,
452 can_local_storage: false,
453 can_timers: false,
454 can_network: false,
455 can_post_message: false,
456 can_geolocation: false,
457 can_clipboard: false,
458 can_notifications: false,
459 can_media_devices: false,
460 can_popups: false,
461 }
462 }
463
464 pub fn trusted() -> Self {
466 Self {
467 can_execute_js: true,
468 can_local_storage: true,
469 can_timers: true,
470 can_network: true,
471 can_post_message: true,
472 can_geolocation: true,
473 can_clipboard: true,
474 can_notifications: true,
475 can_media_devices: true,
476 can_popups: true,
477 }
478 }
479
480 pub fn apply_sandbox_flags(&mut self, flags: &str) {
482 *self = Self::sandboxed();
484
485 for flag in flags.split_whitespace() {
487 match flag {
488 "allow-scripts" => self.can_execute_js = true,
489 "allow-same-origin" => self.can_local_storage = true,
490 "allow-popups" => self.can_popups = true,
491 "allow-forms" => {} "allow-top-navigation" => {} _ => {} }
495 }
496 }
497
498 pub fn enabled_count(&self) -> usize {
500 let bools = [
501 self.can_execute_js,
502 self.can_local_storage,
503 self.can_timers,
504 self.can_network,
505 self.can_post_message,
506 self.can_geolocation,
507 self.can_clipboard,
508 self.can_notifications,
509 self.can_media_devices,
510 self.can_popups,
511 ];
512 bools.iter().filter(|&&b| b).count()
513 }
514}
515
516pub struct ProcessIsolation {
522 processes: BTreeMap<TabId, TabProcess>,
524 message_queue: Vec<IpcMessage>,
526 shared_storage: BTreeMap<String, BTreeMap<String, String>>,
528 max_processes: usize,
530 broadcast_channels: BTreeMap<String, Vec<TabId>>,
532 total_crashes: usize,
534}
535
536impl Default for ProcessIsolation {
537 fn default() -> Self {
538 Self::new()
539 }
540}
541
542impl ProcessIsolation {
543 pub fn new() -> Self {
544 Self {
545 processes: BTreeMap::new(),
546 message_queue: Vec::new(),
547 shared_storage: BTreeMap::new(),
548 max_processes: 32,
549 broadcast_channels: BTreeMap::new(),
550 total_crashes: 0,
551 }
552 }
553
554 pub fn spawn_tab_process(&mut self, tab_id: TabId) -> Result<(), TabError> {
556 if self.processes.len() >= self.max_processes {
557 return Err(TabError::ProcessLimitReached);
558 }
559 if self.processes.contains_key(&tab_id) {
560 return Err(TabError::ProcessAlreadyExists);
561 }
562
563 let mut process = TabProcess::new(tab_id);
564 process.start();
565 self.processes.insert(tab_id, process);
566 Ok(())
567 }
568
569 pub fn spawn_with_capabilities(
571 &mut self,
572 tab_id: TabId,
573 capabilities: TabCapabilities,
574 ) -> Result<(), TabError> {
575 self.spawn_tab_process(tab_id)?;
576 if let Some(proc) = self.processes.get_mut(&tab_id) {
577 proc.capabilities = capabilities;
578 }
579 Ok(())
580 }
581
582 pub fn kill_tab_process(&mut self, tab_id: TabId) -> bool {
584 if let Some(mut proc) = self.processes.remove(&tab_id) {
585 proc.terminate();
586
587 for subscribers in self.broadcast_channels.values_mut() {
589 subscribers.retain(|&id| id != tab_id);
590 }
591
592 self.message_queue.push(IpcMessage::TabEvent {
594 tab_id,
595 event: TabLifecycleEvent::Closed,
596 });
597
598 true
599 } else {
600 false
601 }
602 }
603
604 pub fn recover_from_crash(&mut self, tab_id: TabId) -> Result<(), TabError> {
606 let proc = self
607 .processes
608 .get_mut(&tab_id)
609 .ok_or(TabError::ProcessNotFound)?;
610
611 if proc.state != TabProcessState::Crashed {
612 return Err(TabError::InvalidState {
613 expected: "crashed",
614 });
615 }
616
617 let limits = proc.limits.clone();
619 let capabilities = proc.capabilities.clone();
620 let origin = proc.origin.clone();
621 let crash_count = proc.usage.crash_count;
622
623 proc.vm = JsVm::new();
624 proc.gc = GcHeap::new();
625 proc.dom_api = DomApi::new();
626 proc.inbox.clear();
627 proc.state = TabProcessState::Running;
628 proc.limits = limits;
629 proc.capabilities = capabilities;
630 proc.origin = origin;
631 proc.usage = ResourceUsage::default();
632 proc.usage.crash_count = crash_count;
633 proc.last_error = None;
634
635 self.total_crashes += 1;
636
637 self.message_queue.push(IpcMessage::TabEvent {
639 tab_id,
640 event: TabLifecycleEvent::Recovered,
641 });
642
643 Ok(())
644 }
645
646 pub fn restrict_capabilities(&mut self, tab_id: TabId, capabilities: TabCapabilities) -> bool {
648 if let Some(proc) = self.processes.get_mut(&tab_id) {
649 proc.capabilities = capabilities;
650 true
651 } else {
652 false
653 }
654 }
655
656 pub fn get_process(&self, tab_id: TabId) -> Option<&TabProcess> {
658 self.processes.get(&tab_id)
659 }
660
661 pub fn get_process_mut(&mut self, tab_id: TabId) -> Option<&mut TabProcess> {
663 self.processes.get_mut(&tab_id)
664 }
665
666 pub fn post_message(
668 &mut self,
669 source: TabId,
670 target: TabId,
671 data: &str,
672 ) -> Result<(), TabError> {
673 let source_proc = self
675 .processes
676 .get(&source)
677 .ok_or(TabError::ProcessNotFound)?;
678 if !source_proc.capabilities.can_post_message {
679 return Err(TabError::CapabilityDenied {
680 capability: "postMessage",
681 });
682 }
683 let origin = source_proc.origin.clone();
684
685 let target_proc = self
687 .processes
688 .get_mut(&target)
689 .ok_or(TabError::ProcessNotFound)?;
690
691 target_proc.inbox.push(IpcMessage::PostMessage {
692 source_tab: source,
693 target_tab: target,
694 origin,
695 data: data.to_string(),
696 });
697
698 Ok(())
699 }
700
701 pub fn subscribe_broadcast(&mut self, tab_id: TabId, channel: &str) -> bool {
703 if !self.processes.contains_key(&tab_id) {
704 return false;
705 }
706 let subscribers = self
707 .broadcast_channels
708 .entry(channel.to_string())
709 .or_default();
710 if !subscribers.contains(&tab_id) {
711 subscribers.push(tab_id);
712 }
713 true
714 }
715
716 pub fn unsubscribe_broadcast(&mut self, tab_id: TabId, channel: &str) {
718 if let Some(subscribers) = self.broadcast_channels.get_mut(channel) {
719 subscribers.retain(|&id| id != tab_id);
720 }
721 }
722
723 pub fn broadcast_message(&mut self, source: TabId, channel: &str, data: &str) -> usize {
725 let subscribers: Vec<TabId> = self
726 .broadcast_channels
727 .get(channel)
728 .cloned()
729 .unwrap_or_default();
730
731 let mut delivered = 0;
732 for &sub_id in &subscribers {
733 if sub_id == source {
734 continue; }
736 if let Some(proc) = self.processes.get_mut(&sub_id) {
737 proc.inbox.push(IpcMessage::BroadcastMessage {
738 source_tab: source,
739 channel: channel.to_string(),
740 data: data.to_string(),
741 });
742 delivered += 1;
743 }
744 }
745 delivered
746 }
747
748 pub fn storage_set(&mut self, origin: &str, key: &str, value: &str) {
750 let old_value = self
751 .shared_storage
752 .entry(origin.to_string())
753 .or_default()
754 .insert(key.to_string(), value.to_string());
755
756 let msg = IpcMessage::StorageEvent {
758 key: key.to_string(),
759 old_value,
760 new_value: Some(value.to_string()),
761 };
762
763 let tab_ids: Vec<TabId> = self
764 .processes
765 .iter()
766 .filter(|(_, proc)| proc.origin == origin)
767 .map(|(&id, _)| id)
768 .collect();
769
770 for tab_id in tab_ids {
771 if let Some(proc) = self.processes.get_mut(&tab_id) {
772 proc.inbox.push(msg.clone());
773 }
774 }
775 }
776
777 pub fn storage_get(&self, origin: &str, key: &str) -> Option<&String> {
779 self.shared_storage.get(origin)?.get(key)
780 }
781
782 pub fn storage_remove(&mut self, origin: &str, key: &str) -> Option<String> {
784 self.shared_storage.get_mut(origin)?.remove(key)
785 }
786
787 pub fn tick_all(&mut self) {
789 let tab_ids: Vec<TabId> = self.processes.keys().copied().collect();
790 for tab_id in tab_ids {
791 if let Some(proc) = self.processes.get_mut(&tab_id) {
792 if proc.state == TabProcessState::Running {
793 if let Err(_e) = proc.tick() {
794 }
796 }
797 }
798 }
799 }
800
801 pub fn suspend_background_tabs(&mut self, active_tab: TabId) {
803 for (&id, proc) in self.processes.iter_mut() {
804 if id != active_tab && proc.state == TabProcessState::Running {
805 proc.suspend();
806 }
807 }
808 }
809
810 pub fn resume_tab(&mut self, tab_id: TabId) -> bool {
812 if let Some(proc) = self.processes.get_mut(&tab_id) {
813 proc.resume();
814 true
815 } else {
816 false
817 }
818 }
819
820 pub fn active_count(&self) -> usize {
822 self.processes
823 .values()
824 .filter(|p| p.state == TabProcessState::Running)
825 .count()
826 }
827
828 pub fn total_count(&self) -> usize {
830 self.processes.len()
831 }
832
833 pub fn total_crashes(&self) -> usize {
835 self.total_crashes
836 }
837
838 pub fn resource_usage(&self, tab_id: TabId) -> Option<&ResourceUsage> {
840 self.processes.get(&tab_id).map(|p| &p.usage)
841 }
842
843 pub fn aggregate_usage(&self) -> ResourceUsage {
845 let mut total = ResourceUsage::default();
846 for proc in self.processes.values() {
847 total.heap_bytes += proc.usage.heap_bytes;
848 total.dom_node_count += proc.usage.dom_node_count;
849 total.timer_count += proc.usage.timer_count;
850 total.total_steps += proc.usage.total_steps;
851 total.gc_collections += proc.usage.gc_collections;
852 total.crash_count += proc.usage.crash_count;
853 }
854 total
855 }
856}
857
858#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn test_tab_process_new() {
868 let proc = TabProcess::new(1);
869 assert_eq!(proc.tab_id, 1);
870 assert_eq!(proc.state, TabProcessState::Created);
871 }
872
873 #[test]
874 fn test_tab_process_lifecycle() {
875 let mut proc = TabProcess::new(1);
876 proc.start();
877 assert_eq!(proc.state, TabProcessState::Running);
878 proc.suspend();
879 assert_eq!(proc.state, TabProcessState::Suspended);
880 proc.resume();
881 assert_eq!(proc.state, TabProcessState::Running);
882 proc.terminate();
883 assert_eq!(proc.state, TabProcessState::Terminated);
884 }
885
886 #[test]
887 fn test_tab_process_crash() {
888 let mut proc = TabProcess::new(1);
889 proc.start();
890 proc.crash("out of memory");
891 assert_eq!(proc.state, TabProcessState::Crashed);
892 assert_eq!(proc.last_error.as_deref(), Some("out of memory"));
893 assert_eq!(proc.usage.crash_count, 1);
894 }
895
896 #[test]
897 fn test_tab_process_tick() {
898 let mut proc = TabProcess::new(1);
899 proc.start();
900 assert!(proc.tick().is_ok());
901 assert_eq!(proc.usage.ticks_processed, 1);
902 }
903
904 #[test]
905 fn test_tab_process_tick_not_running() {
906 let mut proc = TabProcess::new(1);
907 assert!(proc.tick().is_ok());
909 assert_eq!(proc.usage.ticks_processed, 0);
910 }
911
912 #[test]
913 fn test_tab_process_execute_script() {
914 let mut proc = TabProcess::new(1);
915 proc.start();
916 assert!(proc.execute_script("let x = 42;").is_ok());
917 }
918
919 #[test]
920 fn test_tab_process_execute_script_not_running() {
921 let mut proc = TabProcess::new(1);
922 assert!(proc.execute_script("let x = 1;").is_err());
923 }
924
925 #[test]
926 fn test_tab_process_execute_js_disabled() {
927 let mut proc = TabProcess::new(1);
928 proc.start();
929 proc.capabilities.can_execute_js = false;
930 assert!(proc.execute_script("let x = 1;").is_err());
931 }
932
933 #[test]
934 fn test_capabilities_default_web() {
935 let caps = TabCapabilities::default_web();
936 assert!(caps.can_execute_js);
937 assert!(caps.can_local_storage);
938 assert!(caps.can_timers);
939 assert!(caps.can_network);
940 assert!(!caps.can_geolocation);
941 assert!(!caps.can_clipboard);
942 }
943
944 #[test]
945 fn test_capabilities_sandboxed() {
946 let caps = TabCapabilities::sandboxed();
947 assert_eq!(caps.enabled_count(), 0);
948 }
949
950 #[test]
951 fn test_capabilities_trusted() {
952 let caps = TabCapabilities::trusted();
953 assert_eq!(caps.enabled_count(), 10);
954 }
955
956 #[test]
957 fn test_capabilities_sandbox_flags() {
958 let mut caps = TabCapabilities::default_web();
959 caps.apply_sandbox_flags("allow-scripts allow-popups");
960 assert!(caps.can_execute_js);
961 assert!(caps.can_popups);
962 assert!(!caps.can_network);
963 assert!(!caps.can_local_storage);
964 }
965
966 #[test]
967 fn test_process_isolation_spawn() {
968 let mut iso = ProcessIsolation::new();
969 assert!(iso.spawn_tab_process(1).is_ok());
970 assert_eq!(iso.total_count(), 1);
971 assert_eq!(iso.active_count(), 1);
972 }
973
974 #[test]
975 fn test_process_isolation_duplicate_spawn() {
976 let mut iso = ProcessIsolation::new();
977 iso.spawn_tab_process(1).unwrap();
978 assert!(iso.spawn_tab_process(1).is_err());
979 }
980
981 #[test]
982 fn test_process_isolation_kill() {
983 let mut iso = ProcessIsolation::new();
984 iso.spawn_tab_process(1).unwrap();
985 assert!(iso.kill_tab_process(1));
986 assert_eq!(iso.total_count(), 0);
987 assert!(!iso.kill_tab_process(1)); }
989
990 #[test]
991 fn test_process_isolation_recover() {
992 let mut iso = ProcessIsolation::new();
993 iso.spawn_tab_process(1).unwrap();
994 iso.get_process_mut(1).unwrap().crash("boom");
995 assert!(iso.recover_from_crash(1).is_ok());
996 assert_eq!(iso.get_process(1).unwrap().state, TabProcessState::Running);
997 assert_eq!(iso.total_crashes(), 1);
998 }
999
1000 #[test]
1001 fn test_process_isolation_recover_not_crashed() {
1002 let mut iso = ProcessIsolation::new();
1003 iso.spawn_tab_process(1).unwrap();
1004 assert!(iso.recover_from_crash(1).is_err());
1005 }
1006
1007 #[test]
1008 fn test_post_message() {
1009 let mut iso = ProcessIsolation::new();
1010 iso.spawn_tab_process(1).unwrap();
1011 iso.spawn_tab_process(2).unwrap();
1012 iso.get_process_mut(1)
1013 .unwrap()
1014 .set_origin("https://example.com");
1015 assert!(iso.post_message(1, 2, "hello").is_ok());
1016 assert_eq!(iso.get_process(2).unwrap().inbox.len(), 1);
1017 }
1018
1019 #[test]
1020 fn test_broadcast_channel() {
1021 let mut iso = ProcessIsolation::new();
1022 iso.spawn_tab_process(1).unwrap();
1023 iso.spawn_tab_process(2).unwrap();
1024 iso.spawn_tab_process(3).unwrap();
1025 iso.subscribe_broadcast(1, "updates");
1026 iso.subscribe_broadcast(2, "updates");
1027 iso.subscribe_broadcast(3, "updates");
1028
1029 let delivered = iso.broadcast_message(1, "updates", "data");
1030 assert_eq!(delivered, 2); }
1032
1033 #[test]
1034 fn test_shared_storage() {
1035 let mut iso = ProcessIsolation::new();
1036 iso.spawn_tab_process(1).unwrap();
1037 iso.get_process_mut(1)
1038 .unwrap()
1039 .set_origin("https://example.com");
1040
1041 iso.storage_set("https://example.com", "key1", "value1");
1042 assert_eq!(
1043 iso.storage_get("https://example.com", "key1"),
1044 Some(&"value1".to_string())
1045 );
1046
1047 iso.storage_remove("https://example.com", "key1");
1048 assert!(iso.storage_get("https://example.com", "key1").is_none());
1049 }
1050
1051 #[test]
1052 fn test_suspend_background_tabs() {
1053 let mut iso = ProcessIsolation::new();
1054 iso.spawn_tab_process(1).unwrap();
1055 iso.spawn_tab_process(2).unwrap();
1056 iso.spawn_tab_process(3).unwrap();
1057
1058 iso.suspend_background_tabs(2);
1059 assert_eq!(
1060 iso.get_process(1).unwrap().state,
1061 TabProcessState::Suspended
1062 );
1063 assert_eq!(iso.get_process(2).unwrap().state, TabProcessState::Running);
1064 assert_eq!(
1065 iso.get_process(3).unwrap().state,
1066 TabProcessState::Suspended
1067 );
1068 }
1069
1070 #[test]
1071 fn test_resume_tab() {
1072 let mut iso = ProcessIsolation::new();
1073 iso.spawn_tab_process(1).unwrap();
1074 iso.get_process_mut(1).unwrap().suspend();
1075 assert!(iso.resume_tab(1));
1076 assert_eq!(iso.get_process(1).unwrap().state, TabProcessState::Running);
1077 }
1078
1079 #[test]
1080 fn test_restrict_capabilities() {
1081 let mut iso = ProcessIsolation::new();
1082 iso.spawn_tab_process(1).unwrap();
1083 let caps = TabCapabilities::sandboxed();
1084 assert!(iso.restrict_capabilities(1, caps));
1085 assert_eq!(iso.get_process(1).unwrap().capabilities.enabled_count(), 0);
1086 }
1087
1088 #[test]
1089 fn test_aggregate_usage() {
1090 let mut iso = ProcessIsolation::new();
1091 iso.spawn_tab_process(1).unwrap();
1092 iso.spawn_tab_process(2).unwrap();
1093 let agg = iso.aggregate_usage();
1094 assert_eq!(agg.crash_count, 0);
1095 }
1096
1097 #[test]
1098 fn test_resource_limits_default() {
1099 let limits = ResourceLimits::default();
1100 assert_eq!(limits.max_heap_bytes, 64 * 1024 * 1024);
1101 assert_eq!(limits.max_dom_nodes, 100_000);
1102 }
1103
1104 #[test]
1105 fn test_same_origin() {
1106 let mut proc = TabProcess::new(1);
1107 proc.set_origin("https://example.com");
1108 assert!(proc.is_same_origin("https://example.com"));
1109 assert!(!proc.is_same_origin("https://other.com"));
1110 }
1111
1112 #[test]
1113 fn test_tick_all() {
1114 let mut iso = ProcessIsolation::new();
1115 iso.spawn_tab_process(1).unwrap();
1116 iso.spawn_tab_process(2).unwrap();
1117 iso.tick_all();
1118 assert_eq!(iso.get_process(1).unwrap().usage.ticks_processed, 1);
1119 assert_eq!(iso.get_process(2).unwrap().usage.ticks_processed, 1);
1120 }
1121
1122 #[test]
1123 fn test_max_processes() {
1124 let mut iso = ProcessIsolation::new();
1125 iso.max_processes = 2;
1126 iso.spawn_tab_process(1).unwrap();
1127 iso.spawn_tab_process(2).unwrap();
1128 assert!(iso.spawn_tab_process(3).is_err());
1129 }
1130
1131 #[test]
1132 fn test_spawn_with_capabilities() {
1133 let mut iso = ProcessIsolation::new();
1134 let caps = TabCapabilities::trusted();
1135 iso.spawn_with_capabilities(1, caps).unwrap();
1136 assert_eq!(iso.get_process(1).unwrap().capabilities.enabled_count(), 10);
1137 }
1138
1139 #[test]
1140 fn test_unsubscribe_broadcast() {
1141 let mut iso = ProcessIsolation::new();
1142 iso.spawn_tab_process(1).unwrap();
1143 iso.subscribe_broadcast(1, "ch");
1144 iso.unsubscribe_broadcast(1, "ch");
1145 let delivered = iso.broadcast_message(2, "ch", "data");
1146 assert_eq!(delivered, 0);
1147 }
1148}