1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Mutex;
4use std::sync::OnceLock;
5use std::io;
6
7use crate::bucket::Bucket;
8
9#[derive(Default)]
10pub struct Store {
11 pub(crate) buckets: HashSet<Bucket>,
12 pub(crate) entries: HashMap<Bucket, HashMap<String, String>>,
13}
14
15static STORE: OnceLock<Mutex<Store>> = OnceLock::new();
16
17pub(crate) fn store() -> &'static Mutex<Store> {
18 STORE.get_or_init(|| Mutex::new(Store::default()))
19}
20
21pub fn create(bucket: &Bucket) -> io::Result<()> {
22 let mut st: std::sync::MutexGuard<'_, Store> = store().lock().map_err(|_| {
23 io::Error::new(io::ErrorKind::Other, "ram store poisoned")
24 })?;
25
26 st.buckets.insert(bucket.clone());
27 st.entries.entry(bucket.clone()).or_insert_with(HashMap::new);
28 Ok(())
29}
30
31pub fn remove_if_exists(bucket: &Bucket) -> io::Result<()> {
32 let mut st: std::sync::MutexGuard<'_, Store> = store().lock().map_err(|_| {
33 io::Error::new(io::ErrorKind::Other, "ram store poisoned")
34 })?;
35 st.buckets.remove(bucket);
36 st.entries.remove(bucket);
37 Ok(())
38}
39
40pub fn exists(bucket: &Bucket) -> io::Result<bool> {
41 let st: std::sync::MutexGuard<'_, Store> = store().lock().map_err(|_| {
42 io::Error::new(io::ErrorKind::Other, "ram store poisoned")
43 })?;
44 Ok(st.buckets.contains(bucket))
45}
46
47pub fn list_keys(bucket: &Bucket) -> io::Result<Vec<String>> {
48 let st: std::sync::MutexGuard<'_, Store> = store().lock().map_err(|_| {
49 io::Error::new(io::ErrorKind::Other, "ram store poisoned")
50 })?;
51
52 ensure_bucket_exists_locked(&st, bucket)?;
53
54 let map: &HashMap<String, String> = st
55 .entries
56 .get(bucket)
57 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Bucket not found"))?;
58
59 Ok(map.keys().cloned().collect())
60}
61
62pub(crate) fn ensure_bucket_exists_locked(st: &Store, bucket: &Bucket) -> io::Result<()> {
63 if !st.buckets.contains(bucket) {
64 return Err(io::Error::new(io::ErrorKind::NotFound, "Bucket not found"));
65 }
66 Ok(())
67}
68
69pub(crate) fn validate_key(key: &str) -> io::Result<()> {
70 if key.is_empty() {
71 return Err(io::Error::new(
72 io::ErrorKind::InvalidInput,
73 "Entry key is empty",
74 ));
75 }
76 if key == "." || key == ".." {
77 return Err(io::Error::new(
78 io::ErrorKind::InvalidInput,
79 "Entry key cannot be '.' or '..'",
80 ));
81 }
82 let ok: bool = key.chars().all(|c| {
83 c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'
84 });
85 if !ok {
86 return Err(io::Error::new(
87 io::ErrorKind::InvalidInput,
88 "Entry key must match [A-Za-z0-9_.-]+",
89 ));
90 }
91 Ok(())
92}
93
94#[cfg(test)]
95#[path = "../tests/unit_tests/store.rs"]
96pub mod tests;