1use super::{KeySeq, PutOutcome, Store, StoreError};
21use serde_json::Value;
22use std::fs;
23use std::io;
24use std::path::{Path, PathBuf};
25
26const EXT: &str = "json";
28const LOCK: &str = ".lock";
30
31#[derive(Debug)]
34pub struct FileStore {
35 root: PathBuf,
36 _lock: LockFile,
38}
39
40impl FileStore {
41 pub fn open(root: &Path) -> Result<FileStore, StoreError> {
43 fs::create_dir_all(root)
44 .map_err(|e| StoreError::Io(format!("store dir {}: {e}", root.display())))?;
45 restrict_dir(root);
46 let lock = LockFile::acquire(&root.join(LOCK)).map_err(StoreError::Io)?;
47 Ok(FileStore {
48 root: root.to_path_buf(),
49 _lock: lock,
50 })
51 }
52
53 pub fn root(&self) -> &Path {
54 &self.root
55 }
56
57 fn path_of(&self, key: &str) -> Result<PathBuf, StoreError> {
64 let mut p = self.root.clone();
65 let segs: Vec<&str> = key.split('/').filter(|s| !s.is_empty()).collect();
66 if segs.is_empty() {
67 return Err(StoreError::Mapping("empty store key".into()));
68 }
69 for (i, seg) in segs.iter().enumerate() {
70 if seg.contains('\0') {
71 return Err(StoreError::Mapping("store key contains NUL".into()));
72 }
73 let enc = encode(seg);
74 if i + 1 == segs.len() {
75 p.push(format!("{enc}.{EXT}"));
76 } else {
77 p.push(enc);
78 }
79 }
80 Ok(p)
81 }
82
83 fn read(&self, path: &Path) -> Result<Option<Value>, StoreError> {
85 match fs::read(path) {
86 Ok(b) => serde_json::from_slice(&b)
87 .map(Some)
88 .map_err(|e| StoreError::Corrupt(format!("{}: {e}", path.display()))),
89 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
90 Err(e) => Err(StoreError::Io(format!("read {}: {e}", path.display()))),
91 }
92 }
93}
94
95impl Store for FileStore {
96 fn put(&self, key: &str, seq: u64, envelope: &Value) -> Result<PutOutcome, StoreError> {
97 let path = self.path_of(key)?;
98 if let Some(cur) = self.read(&path)?
101 && let Some(l) = cur.get("seq").and_then(Value::as_u64)
102 && seq <= l
103 {
104 return Ok(PutOutcome::Conflict {
105 latest_seq: Some(l),
106 });
107 }
108 if let Some(parent) = path.parent() {
109 fs::create_dir_all(parent)
110 .map_err(|e| StoreError::Io(format!("mkdir {}: {e}", parent.display())))?;
111 restrict_tree(&self.root, parent);
118 }
119 let body = serde_json::to_vec(envelope)
120 .map_err(|e| StoreError::Mapping(format!("envelope: {e}")))?;
121 write_atomic(&path, &body).map_err(StoreError::Io)?;
122 Ok(PutOutcome::Ok)
123 }
124
125 fn get(&self, key: &str, _seq: Option<u64>) -> Result<Option<Value>, StoreError> {
126 let v = self.read(&self.path_of(key)?)?;
128 Ok(v.filter(|v| !v.get("state").is_some_and(Value::is_null)))
130 }
131
132 fn list(&self, prefix: &str) -> Result<Vec<KeySeq>, StoreError> {
133 let mut dir = self.root.clone();
134 for seg in prefix.split('/').filter(|s| !s.is_empty()) {
135 dir.push(encode(seg));
136 }
137 let mut out = Vec::new();
138 walk(&dir, &self.root, &mut out)?;
139 out.sort_by(|a, b| a.key.cmp(&b.key));
140 Ok(out)
141 }
142
143 fn delete(&self, key: &str) -> Result<(), StoreError> {
144 match fs::remove_file(self.path_of(key)?) {
145 Ok(()) => Ok(()),
146 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
147 Err(e) => Err(StoreError::Io(format!("delete {key}: {e}"))),
148 }
149 }
150
151 fn kind(&self) -> &'static str {
152 "file"
153 }
154}
155
156fn walk(dir: &Path, root: &Path, out: &mut Vec<KeySeq>) -> Result<(), StoreError> {
159 let rd = match fs::read_dir(dir) {
160 Ok(rd) => rd,
161 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
162 Err(e) => return Err(StoreError::Io(format!("list {}: {e}", dir.display()))),
163 };
164 for ent in rd.flatten() {
165 let p = ent.path();
166 if p.is_dir() {
167 walk(&p, root, out)?;
168 } else if p.extension().is_some_and(|e| e == EXT) {
169 let Ok(rel) = p.strip_prefix(root) else {
170 continue;
171 };
172 let mut segs: Vec<String> = rel
173 .components()
174 .map(|c| decode(&c.as_os_str().to_string_lossy()))
175 .collect();
176 if let Some(last) = segs.last_mut()
177 && let Some(stem) = last.strip_suffix(&format!(".{EXT}"))
178 {
179 *last = stem.to_string();
180 }
181 let seq = fs::read(&p)
182 .ok()
183 .and_then(|b| serde_json::from_slice::<Value>(&b).ok())
184 .and_then(|v| v.get("seq").and_then(Value::as_u64));
185 out.push(KeySeq {
186 key: segs.join("/"),
187 seq,
188 });
189 }
190 }
191 Ok(())
192}
193
194fn write_atomic(path: &Path, body: &[u8]) -> Result<(), String> {
199 use std::io::Write;
200 let dir = path.parent().ok_or_else(|| "no parent dir".to_string())?;
201 let tmp = dir.join(format!(
202 ".{}.tmp.{}",
203 path.file_name().unwrap_or_default().to_string_lossy(),
204 std::process::id()
205 ));
206 {
207 let mut f = fs::File::create(&tmp).map_err(|e| format!("create {}: {e}", tmp.display()))?;
208 restrict_file(&f);
209 f.write_all(body)
210 .map_err(|e| format!("write {}: {e}", tmp.display()))?;
211 f.sync_all()
212 .map_err(|e| format!("fsync {}: {e}", tmp.display()))?;
213 }
214 fs::rename(&tmp, path).map_err(|e| {
215 let _ = fs::remove_file(&tmp);
216 format!("rename into {}: {e}", path.display())
217 })?;
218 if let Ok(d) = fs::File::open(dir) {
220 let _ = d.sync_all();
221 }
222 Ok(())
223}
224
225fn restrict_tree(root: &Path, leaf: &Path) {
227 let Ok(rel) = leaf.strip_prefix(root) else {
228 restrict_dir(leaf);
229 return;
230 };
231 let mut p = root.to_path_buf();
232 for comp in rel.components() {
233 p.push(comp);
234 restrict_dir(&p);
235 }
236}
237
238fn restrict_dir(p: &Path) {
240 #[cfg(unix)]
241 {
242 use std::os::unix::fs::PermissionsExt;
243 let _ = fs::set_permissions(p, fs::Permissions::from_mode(0o700));
244 }
245 #[cfg(not(unix))]
246 let _ = p;
247}
248
249fn restrict_file(f: &fs::File) {
251 #[cfg(unix)]
252 {
253 use std::os::unix::fs::PermissionsExt;
254 let _ = f.set_permissions(fs::Permissions::from_mode(0o600));
255 }
256 #[cfg(not(unix))]
257 let _ = f;
258}
259
260fn encode(seg: &str) -> String {
263 let mut out = String::with_capacity(seg.len());
264 for b in seg.bytes() {
265 let safe = b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_');
266 if safe {
267 out.push(b as char);
268 } else {
269 out.push_str(&format!("%{b:02X}"));
270 }
271 }
272 out
273}
274
275fn decode(seg: &str) -> String {
276 let b = seg.as_bytes();
277 let mut out = Vec::with_capacity(b.len());
278 let mut i = 0;
279 while i < b.len() {
280 if b[i] == b'%' && i + 2 < b.len() {
281 let hex = std::str::from_utf8(&b[i + 1..i + 3]).unwrap_or("");
282 if let Ok(v) = u8::from_str_radix(hex, 16) {
283 out.push(v);
284 i += 3;
285 continue;
286 }
287 }
288 out.push(b[i]);
289 i += 1;
290 }
291 String::from_utf8_lossy(&out).into_owned()
292}
293
294#[derive(Debug)]
296struct LockFile {
297 _file: fs::File,
298}
299
300impl LockFile {
301 fn acquire(path: &Path) -> Result<LockFile, String> {
302 let file = fs::OpenOptions::new()
303 .create(true)
304 .read(true)
305 .write(true)
306 .truncate(false)
307 .open(path)
308 .map_err(|e| format!("lock {}: {e}", path.display()))?;
309 restrict_file(&file);
310 #[cfg(unix)]
311 {
312 use std::io::{Read, Seek, Write};
313 use std::os::unix::io::AsRawFd;
314 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
317 if rc != 0 {
318 let mut holder = String::new();
319 let mut f = &file;
320 let _ = f.rewind();
321 let _ = f.read_to_string(&mut holder);
322 let who = holder.trim();
323 let who = if who.is_empty() {
324 "another process".to_string()
325 } else {
326 format!("pid {who}")
327 };
328 return Err(format!(
329 "{} is locked by {who} — another agentd is using this state \
330 directory; give this instance its own agent.name or store.file.path",
331 path.parent().unwrap_or(path).display()
332 ));
333 }
334 let mut f = &file;
335 let _ = f.rewind();
336 let _ = f.set_len(0);
337 let _ = write!(f, "{}", std::process::id());
338 let _ = f.flush();
339 }
340 Ok(LockFile { _file: file })
341 }
342}