1use std::collections::HashMap;
49use std::fs;
50use std::path::{Path, PathBuf};
51
52use serde_json::Value;
53
54pub trait StateStore: Send + Sync {
63 fn get(&self, ns: &str, key: &str) -> Result<Option<Value>, String>;
65
66 fn set(&self, ns: &str, key: &str, value: Value) -> Result<(), String>;
68
69 fn delete(&self, ns: &str, key: &str) -> Result<bool, String>;
71
72 fn keys(&self, ns: &str) -> Result<Vec<String>, String>;
74
75 fn has(&self, ns: &str, key: &str) -> Result<bool, String>;
81
82 fn set_nx(&self, ns: &str, key: &str, value: Value) -> Result<bool, String>;
91
92 fn incr(&self, ns: &str, key: &str, delta: f64, default: f64) -> Result<f64, String>;
108}
109
110pub struct JsonFileStore {
124 root: PathBuf,
125}
126
127impl JsonFileStore {
128 pub fn new(root: PathBuf) -> Self {
133 Self { root }
134 }
135
136 pub fn root(&self) -> &Path {
138 &self.root
139 }
140
141 fn ensure_root(&self) -> Result<&Path, String> {
143 if !self.root.exists() {
144 fs::create_dir_all(&self.root)
145 .map_err(|e| format!("Failed to create state dir: {e}"))?;
146 }
147 Ok(&self.root)
148 }
149
150 pub fn state_path(&self, ns: &str) -> Result<PathBuf, String> {
153 if ns.contains('/')
154 || ns.contains('\\')
155 || ns.contains("..")
156 || ns.contains('\0')
157 || ns.is_empty()
158 {
159 return Err(format!("Invalid namespace: '{ns}'"));
160 }
161 let dir = self.ensure_root()?;
162 Ok(dir.join(format!("{ns}.json")))
163 }
164
165 fn load(&self, ns: &str) -> Result<HashMap<String, Value>, String> {
166 let path = self.state_path(ns)?;
167 if !path.exists() {
168 return Ok(HashMap::new());
169 }
170 let content =
171 fs::read_to_string(&path).map_err(|e| format!("Failed to read state '{ns}': {e}"))?;
172 serde_json::from_str(&content).map_err(|e| format!("Failed to parse state '{ns}': {e}"))
173 }
174
175 fn save(&self, ns: &str, data: &HashMap<String, Value>) -> Result<(), String> {
176 let path = self.state_path(ns)?;
177 let tmp = path.with_extension("json.tmp");
178 let content = serde_json::to_string_pretty(data)
179 .map_err(|e| format!("Failed to serialize state: {e}"))?;
180 fs::write(&tmp, &content).map_err(|e| format!("Failed to write state tmp: {e}"))?;
181 fs::rename(&tmp, &path).map_err(|e| format!("Failed to rename state file: {e}"))?;
182 Ok(())
183 }
184}
185
186impl StateStore for JsonFileStore {
187 fn get(&self, ns: &str, key: &str) -> Result<Option<Value>, String> {
188 let state = self.load(ns)?;
189 Ok(state.get(key).cloned())
190 }
191
192 fn set(&self, ns: &str, key: &str, value: Value) -> Result<(), String> {
193 let mut state = self.load(ns)?;
194 state.insert(key.to_string(), value);
195 self.save(ns, &state)
196 }
197
198 fn delete(&self, ns: &str, key: &str) -> Result<bool, String> {
199 let mut state = self.load(ns)?;
200 let existed = state.remove(key).is_some();
201 if existed {
202 self.save(ns, &state)?;
203 }
204 Ok(existed)
205 }
206
207 fn keys(&self, ns: &str) -> Result<Vec<String>, String> {
208 let state = self.load(ns)?;
209 Ok(state.keys().cloned().collect())
210 }
211
212 fn has(&self, ns: &str, key: &str) -> Result<bool, String> {
213 let state = self.load(ns)?;
214 Ok(state.contains_key(key))
215 }
216
217 fn set_nx(&self, ns: &str, key: &str, value: Value) -> Result<bool, String> {
218 let mut state = self.load(ns)?;
219 if state.contains_key(key) {
220 return Ok(false);
221 }
222 state.insert(key.to_string(), value);
223 self.save(ns, &state)?;
224 Ok(true)
225 }
226
227 fn incr(&self, ns: &str, key: &str, delta: f64, default: f64) -> Result<f64, String> {
228 let mut state = self.load(ns)?;
229 let current = match state.get(key) {
230 Some(v) => v
231 .as_f64()
232 .ok_or_else(|| format!("incr: value at '{key}' is not a number"))?,
233 None => default,
234 };
235 let new_val = current + delta;
236 state.insert(key.to_string(), serde_json::json!(new_val));
237 self.save(ns, &state)?;
238 Ok(new_val)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use tempfile::TempDir;
246
247 fn new_store() -> (JsonFileStore, TempDir) {
250 let tmp = tempfile::tempdir().unwrap();
251 let store = JsonFileStore::new(tmp.path().to_path_buf());
252 (store, tmp)
253 }
254
255 #[test]
256 fn roundtrip() {
257 let (store, _tmp) = new_store();
258 let ns = "rt";
259
260 store.set(ns, "count", serde_json::json!(42)).unwrap();
261 store
262 .set(ns, "name", serde_json::json!("algocline"))
263 .unwrap();
264
265 assert_eq!(store.get(ns, "count").unwrap(), Some(serde_json::json!(42)));
266 assert_eq!(
267 store.get(ns, "name").unwrap(),
268 Some(serde_json::json!("algocline"))
269 );
270 assert_eq!(store.get(ns, "missing").unwrap(), None);
271
272 let k = store.keys(ns).unwrap();
273 assert!(k.contains(&"count".to_string()));
274 assert!(k.contains(&"name".to_string()));
275
276 assert!(store.delete(ns, "count").unwrap());
277 assert!(!store.delete(ns, "count").unwrap());
278 assert_eq!(store.get(ns, "count").unwrap(), None);
279 }
280
281 #[test]
282 fn invalid_namespace() {
283 let (store, _tmp) = new_store();
284 assert!(store.state_path("../evil").is_err());
285 assert!(store.state_path("foo/bar").is_err());
286 assert!(store.state_path("foo\\bar").is_err());
287 assert!(store.state_path("").is_err());
288 assert!(store.state_path("foo\0bar").is_err());
289 }
290
291 #[test]
292 fn get_nonexistent_namespace_returns_empty() {
293 let (store, _tmp) = new_store();
294 let result = store.get("ghost_ns", "any_key").unwrap();
295 assert_eq!(result, None);
296 }
297
298 #[test]
299 fn keys_nonexistent_namespace_returns_empty() {
300 let (store, _tmp) = new_store();
301 let result = store.keys("ghost_ns").unwrap();
302 assert!(result.is_empty());
303 }
304
305 #[test]
306 fn delete_nonexistent_key_returns_false() {
307 let (store, _tmp) = new_store();
308 assert!(!store.delete("delns", "nope").unwrap());
309 }
310
311 #[test]
312 fn set_overwrites_existing_value() {
313 let (store, _tmp) = new_store();
314 let ns = "ow";
315
316 store.set(ns, "k", serde_json::json!(1)).unwrap();
317 store.set(ns, "k", serde_json::json!(2)).unwrap();
318 assert_eq!(store.get(ns, "k").unwrap(), Some(serde_json::json!(2)));
319 }
320
321 #[test]
322 fn state_path_valid_namespaces() {
323 let (store, _tmp) = new_store();
324 assert!(store.state_path("default").is_ok());
325 assert!(store.state_path("my-app").is_ok());
326 assert!(store.state_path("test_123").is_ok());
327 }
328
329 #[test]
332 fn has_returns_existence() {
333 let (store, _tmp) = new_store();
334 let ns = "hasns";
335
336 assert!(!store.has(ns, "x").unwrap());
337 store.set(ns, "x", serde_json::json!(1)).unwrap();
338 assert!(store.has(ns, "x").unwrap());
339 }
340
341 #[test]
342 fn set_nx_only_sets_if_absent() {
343 let (store, _tmp) = new_store();
344 let ns = "snx";
345
346 assert!(store.set_nx(ns, "k", serde_json::json!("first")).unwrap());
347 assert!(!store.set_nx(ns, "k", serde_json::json!("second")).unwrap());
348 assert_eq!(
349 store.get(ns, "k").unwrap(),
350 Some(serde_json::json!("first")),
351 "set_nx should not overwrite"
352 );
353 }
354
355 #[test]
356 fn incr_initialises_and_increments() {
357 let (store, _tmp) = new_store();
358 let ns = "inc";
359
360 let v = store.incr(ns, "counter", 1.0, 0.0).unwrap();
362 assert!((v - 1.0).abs() < f64::EPSILON);
363
364 let v = store.incr(ns, "counter", 5.0, 0.0).unwrap();
366 assert!((v - 6.0).abs() < f64::EPSILON);
367
368 let v = store.incr(ns, "counter", -2.0, 0.0).unwrap();
370 assert!((v - 4.0).abs() < f64::EPSILON);
371 }
372
373 #[test]
374 fn incr_rejects_non_numeric() {
375 let (store, _tmp) = new_store();
376 let ns = "incerr";
377
378 store.set(ns, "s", serde_json::json!("hello")).unwrap();
379 let err = store.incr(ns, "s", 1.0, 0.0).unwrap_err();
380 assert!(err.contains("not a number"), "got: {err}");
381 }
382
383 #[test]
384 fn incr_custom_default() {
385 let (store, _tmp) = new_store();
386 let ns = "incdef";
387
388 let v = store.incr(ns, "score", 10.0, 100.0).unwrap();
389 assert!((v - 110.0).abs() < f64::EPSILON, "100 + 10 = 110");
390 }
391}
392
393#[cfg(test)]
394mod proptests {
395 use super::*;
396 use proptest::prelude::*;
397
398 fn new_store() -> (JsonFileStore, tempfile::TempDir) {
399 let tmp = tempfile::tempdir().unwrap();
400 let store = JsonFileStore::new(tmp.path().to_path_buf());
401 (store, tmp)
402 }
403
404 proptest! {
405 #[test]
407 fn roundtrip_arbitrary_values(
408 key in "[a-z]{1,20}",
409 val in any::<i64>(),
410 ) {
411 let (store, _tmp) = new_store();
412 let ns = "rt";
413 let json_val = serde_json::json!(val);
414 store.set(ns, &key, json_val.clone()).unwrap();
415 let got = store.get(ns, &key).unwrap();
416 prop_assert_eq!(got, Some(json_val));
417 let _ = store.delete(ns, &key);
418 }
419
420 #[test]
422 fn traversal_always_rejected(
423 prefix in "[a-z]{0,5}",
424 suffix in "[a-z]{0,5}",
425 ) {
426 let (store, _tmp) = new_store();
427 let evil = format!("{prefix}/../{suffix}");
428 prop_assert!(store.state_path(&evil).is_err());
429 }
430
431 #[test]
433 fn nul_byte_always_rejected(
434 prefix in "[a-z]{0,10}",
435 suffix in "[a-z]{0,10}",
436 ) {
437 let (store, _tmp) = new_store();
438 let evil = format!("{prefix}\0{suffix}");
439 prop_assert!(store.state_path(&evil).is_err());
440 }
441 }
442}