1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6#[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#[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 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 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 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 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}