Skip to main content

jsdet_browser/
storage.rs

1/// Storage simulation — localStorage, sessionStorage, document.cookie.
2///
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6/// Simulated Web Storage (localStorage/sessionStorage).
7#[derive(Debug, Clone, Default)]
8pub struct WebStorage {
9    data: Arc<Mutex<HashMap<String, String>>>,
10    max_items: usize,
11}
12
13impl WebStorage {
14    pub fn new(max_items: usize) -> Self {
15        Self {
16            data: Arc::new(Mutex::new(HashMap::new())),
17            max_items,
18        }
19    }
20
21    pub fn get_item(&self, key: &str) -> Option<String> {
22        self.data.lock().ok()?.get(key).cloned()
23    }
24
25    pub fn set_item(&self, key: &str, value: &str) -> Result<(), String> {
26        let mut data = self.data.lock().map_err(|e| e.to_string())?;
27        if data.len() >= self.max_items && !data.contains_key(key) {
28            return Err("QuotaExceededError".into());
29        }
30        data.insert(key.to_string(), value.to_string());
31        Ok(())
32    }
33
34    pub fn remove_item(&self, key: &str) {
35        if let Ok(mut data) = self.data.lock() {
36            data.remove(key);
37        }
38    }
39
40    pub fn clear(&self) {
41        if let Ok(mut data) = self.data.lock() {
42            data.clear();
43        }
44    }
45
46    pub fn length(&self) -> usize {
47        self.data.lock().map(|d| d.len()).unwrap_or(0)
48    }
49
50    pub fn key(&self, index: usize) -> Option<String> {
51        self.data.lock().ok()?.keys().nth(index).cloned()
52    }
53}
54
55/// Simulated cookie jar.
56#[derive(Debug, Clone)]
57pub struct CookieJar {
58    cookies: Arc<Mutex<HashMap<String, CookieEntry>>>,
59}
60
61#[derive(Debug, Clone)]
62struct CookieEntry {
63    value: String,
64    domain: String,
65    path: String,
66    secure: bool,
67    http_only: bool,
68    same_site: String,
69}
70
71impl Default for CookieJar {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl CookieJar {
78    pub fn new() -> Self {
79        Self {
80            cookies: Arc::new(Mutex::new(HashMap::new())),
81        }
82    }
83
84    /// Pre-populate with cookies (for testing extension cookie access).
85    pub fn add(&self, name: &str, value: &str, domain: &str) {
86        if let Ok(mut jar) = self.cookies.lock() {
87            jar.insert(
88                name.to_string(),
89                CookieEntry {
90                    value: value.to_string(),
91                    domain: domain.to_string(),
92                    path: "/".into(),
93                    secure: false,
94                    http_only: false,
95                    same_site: "Lax".into(),
96                },
97            );
98        }
99    }
100
101    /// Parse and set a cookie from a `document.cookie = "..."` string.
102    pub fn set_from_string(&self, cookie_str: &str) {
103        let parts: Vec<&str> = cookie_str.split(';').map(|s| s.trim()).collect();
104        let Some(name_value) = parts.first() else {
105            return;
106        };
107        let (name, value) = name_value.split_once('=').unwrap_or((name_value, ""));
108
109        let mut entry = CookieEntry {
110            value: value.to_string(),
111            domain: String::new(),
112            path: "/".into(),
113            secure: false,
114            http_only: false,
115            same_site: "Lax".into(),
116        };
117
118        for part in &parts[1..] {
119            let lower = part.to_ascii_lowercase();
120            if let Some(domain) = lower.strip_prefix("domain=") {
121                entry.domain = domain.to_string();
122            } else if let Some(path) = lower.strip_prefix("path=") {
123                entry.path = path.to_string();
124            } else if lower == "secure" {
125                entry.secure = true;
126            } else if lower == "httponly" {
127                entry.http_only = true;
128            } else if lower.starts_with("samesite=") {
129                entry.same_site = part[9..].to_string();
130            }
131        }
132
133        if let Ok(mut jar) = self.cookies.lock() {
134            jar.insert(name.trim().to_string(), entry);
135        }
136    }
137
138    /// Get the `document.cookie` string (non-HttpOnly cookies).
139    pub fn to_cookie_string(&self) -> String {
140        let jar = match self.cookies.lock() {
141            Ok(j) => j,
142            Err(_) => return String::new(),
143        };
144        jar.iter()
145            .filter(|(_, e)| !e.http_only)
146            .map(|(name, entry)| format!("{}={}", name, entry.value))
147            .collect::<Vec<_>>()
148            .join("; ")
149    }
150
151    /// Get all cookies as JSON (for chrome.cookies API).
152    pub fn to_json(&self) -> String {
153        let jar = match self.cookies.lock() {
154            Ok(j) => j,
155            Err(_) => return "[]".into(),
156        };
157        let entries: Vec<serde_json::Value> = jar
158            .iter()
159            .map(|(name, entry)| {
160                serde_json::json!({
161                    "name": name,
162                    "value": entry.value,
163                    "domain": entry.domain,
164                    "path": entry.path,
165                    "secure": entry.secure,
166                    "httpOnly": entry.http_only,
167                    "sameSite": entry.same_site,
168                })
169            })
170            .collect();
171        serde_json::to_string(&entries).unwrap_or_else(|_| "[]".into())
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn local_storage_crud() {
181        let storage = WebStorage::new(100);
182        storage.set_item("key", "value").unwrap();
183        assert_eq!(storage.get_item("key"), Some("value".into()));
184        assert_eq!(storage.length(), 1);
185        storage.remove_item("key");
186        assert_eq!(storage.length(), 0);
187    }
188
189    #[test]
190    fn cookie_set_and_read() {
191        let jar = CookieJar::new();
192        jar.set_from_string("session=abc123; Path=/; Secure");
193        let cookies = jar.to_cookie_string();
194        assert!(cookies.contains("session=abc123"));
195    }
196
197    #[test]
198    fn http_only_cookies_hidden_from_document() {
199        let jar = CookieJar::new();
200        jar.set_from_string("visible=yes");
201        jar.set_from_string("secret=no; HttpOnly");
202        let cookies = jar.to_cookie_string();
203        assert!(cookies.contains("visible=yes"));
204        assert!(!cookies.contains("secret=no"));
205    }
206}