1use std::{
14 fs, io,
15 path::{Path, PathBuf},
16 sync::Arc,
17};
18use toml_edit::{DocumentMut, Item, Table, value};
19use wcore::Memory;
20
21#[derive(Debug, Clone)]
25pub struct FsMemory {
26 base: Arc<PathBuf>,
27}
28
29impl FsMemory {
30 pub fn new(base: impl Into<PathBuf>) -> io::Result<Self> {
32 let base = base.into();
33 fs::create_dir_all(&base)?;
34 Ok(Self {
35 base: Arc::new(base),
36 })
37 }
38
39 fn file_path(&self, ns: &str) -> PathBuf {
41 self.base.join(format!("{ns}.toml"))
42 }
43
44 fn split_key(key: &str) -> Option<(&str, Vec<&str>)> {
48 let (ns, rest) = key.split_once('.')?;
49 let segs: Vec<&str> = rest.split('.').collect();
50 if ns.is_empty() || segs.iter().any(|s| s.is_empty()) {
51 return None;
52 }
53 Some((ns, segs))
54 }
55
56 fn load(path: &Path) -> DocumentMut {
58 fs::read_to_string(path)
59 .ok()
60 .and_then(|s| s.parse::<DocumentMut>().ok())
61 .unwrap_or_default()
62 }
63
64 fn save(path: &Path, doc: &DocumentMut) {
66 let tmp = path.with_extension("toml.tmp");
67 if fs::write(&tmp, doc.to_string()).is_ok() {
68 let _ = fs::rename(&tmp, path);
69 }
70 }
71
72 fn get_nested<'a>(item: &'a Item, segs: &[&str]) -> Option<&'a str> {
74 if segs.is_empty() {
75 return item.as_str();
76 }
77 Self::get_nested(&item[segs[0]], &segs[1..])
78 }
79
80 fn collect_leaves(table: &Table, prefix: &str, out: &mut Vec<(String, String)>) {
83 for (k, v) in table {
84 let full_key = if prefix.is_empty() {
85 k.to_owned()
86 } else {
87 format!("{prefix}.{k}")
88 };
89 match v {
90 Item::Value(toml_edit::Value::String(s)) => {
91 out.push((full_key, s.value().clone()));
92 }
93 Item::Table(t) => {
94 Self::collect_leaves(t, &full_key, out);
95 }
96 _ => {}
97 }
98 }
99 }
100
101 fn insert_nested(doc: &mut DocumentMut, segs: &[&str], val: &str) -> Option<String> {
105 let mut table: *mut Table = doc.as_table_mut();
107 for &seg in &segs[..segs.len() - 1] {
108 let t = unsafe { &mut *table };
110 if !t.contains_key(seg) {
111 t.insert(seg, Item::Table(Table::new()));
112 }
113 table = match unsafe { &mut *table }.get_mut(seg) {
114 Some(Item::Table(t)) => t as *mut Table,
115 _ => return None,
116 };
117 }
118 let leaf = segs[segs.len() - 1];
119 let t = unsafe { &mut *table };
120 let old = t.get(leaf).and_then(|i| i.as_str()).map(ToOwned::to_owned);
121 t.insert(leaf, value(val));
122 old
123 }
124
125 fn remove_nested(doc: &mut DocumentMut, segs: &[&str]) -> Option<String> {
128 if segs.len() == 1 {
129 return doc
130 .remove(segs[0])
131 .and_then(|i| i.into_value().ok())
132 .and_then(|v| {
133 if let toml_edit::Value::String(s) = v {
134 Some(s.into_value())
135 } else {
136 None
137 }
138 });
139 }
140 Self::remove_nested_recursive(doc.as_table_mut(), segs)
143 }
144
145 fn remove_nested_recursive(table: &mut Table, segs: &[&str]) -> Option<String> {
146 if segs.len() == 1 {
147 return table
148 .remove(segs[0])
149 .and_then(|i| i.into_value().ok())
150 .and_then(|v| {
151 if let toml_edit::Value::String(s) = v {
152 Some(s.into_value())
153 } else {
154 None
155 }
156 });
157 }
158 let child = table.get_mut(segs[0])?;
159 let child_table = child.as_table_mut()?;
160 let old = Self::remove_nested_recursive(child_table, &segs[1..]);
161 if old.is_some() && child_table.is_empty() {
162 table.remove(segs[0]);
163 }
164 old
165 }
166}
167
168impl Memory for FsMemory {
169 fn get(&self, key: &str) -> Option<String> {
170 let (ns, segs) = Self::split_key(key)?;
171 let doc = Self::load(&self.file_path(ns));
172 Self::get_nested(doc.as_item(), &segs).map(ToOwned::to_owned)
173 }
174
175 fn entries(&self) -> Vec<(String, String)> {
176 let Ok(read_dir) = fs::read_dir(&*self.base) else {
177 return Vec::new();
178 };
179 let mut out = Vec::new();
180 for entry in read_dir.flatten() {
181 let path = entry.path();
182 if path.extension().and_then(|e| e.to_str()) != Some("toml") {
183 continue;
184 }
185 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
186 continue;
187 };
188 let doc = Self::load(&path);
189 Self::collect_leaves(doc.as_table(), stem, &mut out);
190 }
191 out
192 }
193
194 fn set(&self, key: impl Into<String>, value: impl Into<String>) -> Option<String> {
195 let key = key.into();
196 let value = value.into();
197 let (ns, segs) = Self::split_key(&key)?;
198 let path = self.file_path(ns);
199 let mut doc = Self::load(&path);
200 let old = Self::insert_nested(&mut doc, &segs, &value);
201 Self::save(&path, &doc);
202 old
203 }
204
205 fn remove(&self, key: &str) -> Option<String> {
206 let (ns, segs) = Self::split_key(key)?;
207 let path = self.file_path(ns);
208 let mut doc = Self::load(&path);
209 let old = Self::remove_nested(&mut doc, &segs);
210 if old.is_some() {
211 if doc.as_table().is_empty() {
212 let _ = fs::remove_file(&path);
213 } else {
214 Self::save(&path, &doc);
215 }
216 }
217 old
218 }
219}