Skip to main content

jsdet_core/
context.rs

1use std::collections::HashMap;
2
3use crate::observation::Value;
4
5/// Identifies an execution context within a multi-context sandbox.
6///
7/// Chrome extensions have multiple contexts: background service worker,
8/// content scripts (one per tab), popup, options page. Each gets its own
9/// `QuickJS` WASM instance with isolated memory, but they can exchange
10/// messages through the host-mediated message bus.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
12pub struct ContextId(pub String);
13
14impl ContextId {
15    #[must_use]
16    pub fn background() -> Self {
17        Self("background".into())
18    }
19
20    #[must_use]
21    pub fn content_script(tab_id: u32) -> Self {
22        Self(format!("content:{tab_id}"))
23    }
24
25    #[must_use]
26    pub fn popup() -> Self {
27        Self("popup".into())
28    }
29
30    #[must_use]
31    pub fn options() -> Self {
32        Self("options".into())
33    }
34
35    #[must_use]
36    pub fn service_worker() -> Self {
37        Self("service_worker".into())
38    }
39
40    #[must_use]
41    pub fn custom(name: impl Into<String>) -> Self {
42        Self(name.into())
43    }
44}
45
46impl std::fmt::Display for ContextId {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.0)
49    }
50}
51
52/// A message passed between execution contexts.
53///
54/// All inter-context communication goes through the host message bus.
55/// This makes every message observable and interceptable.
56#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
57pub struct ContextMessage {
58    pub from: ContextId,
59    pub to: ContextId,
60    pub payload: Value,
61    /// Channel identifier for routing (e.g., port name, message type).
62    pub channel: Option<String>,
63}
64
65/// Maximum total messages retained in the history ring buffer.
66/// Prevents memory exhaustion from adversarial JS flooding `chrome.runtime.sendMessage`.
67const MAX_MESSAGE_HISTORY: usize = 50_000;
68
69/// Maximum pending messages per recipient context queue.
70/// Prevents a single context from monopolizing memory.
71const MAX_QUEUE_PER_CONTEXT: usize = 1_000;
72
73/// The host-side message bus connecting execution contexts.
74///
75/// Messages are queued and delivered when the receiving context
76/// runs its next execution pass. The bus records all messages
77/// for the observation stream, up to a configurable cap.
78#[derive(Debug, Default)]
79pub struct MessageBus {
80    /// Queued messages waiting for delivery, keyed by recipient context.
81    queues: HashMap<ContextId, Vec<ContextMessage>>,
82    /// All messages ever sent, in order. The observation stream.
83    /// Capped at [`MAX_MESSAGE_HISTORY`]; oldest messages are discarded
84    /// when the limit is reached.
85    history: Vec<ContextMessage>,
86}
87
88impl MessageBus {
89    #[must_use]
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Queue a message for delivery to the target context.
95    ///
96    /// Messages beyond the history or per-queue cap are silently discarded
97    /// to prevent memory exhaustion from adversarial scripts.
98    pub fn send(&mut self, message: ContextMessage) {
99        if self.history.len() < MAX_MESSAGE_HISTORY {
100            self.history.push(message.clone());
101        }
102        let queue = self.queues.entry(message.to.clone()).or_default();
103        if queue.len() < MAX_QUEUE_PER_CONTEXT {
104            queue.push(message);
105        }
106    }
107
108    /// Drain all pending messages for a context.
109    /// Called when the context starts its next execution pass.
110    pub fn receive(&mut self, context: &ContextId) -> Vec<ContextMessage> {
111        self.queues.remove(context).unwrap_or_default()
112    }
113
114    /// Return the full message history for observation.
115    #[must_use]
116    pub fn history(&self) -> &[ContextMessage] {
117        &self.history
118    }
119
120    /// Whether any context has pending messages.
121    #[must_use]
122    pub fn has_pending(&self) -> bool {
123        self.queues.values().any(|q| !q.is_empty())
124    }
125
126    /// Number of messages recorded in the history.
127    #[must_use]
128    pub fn history_len(&self) -> usize {
129        self.history.len()
130    }
131}