1use crate::atomic::TEMP_MARKER;
18use crate::error::{FsError, Result};
19use std::fs::File;
20use std::io::{Read, Write};
21use std::path::{Path, PathBuf};
22use std::sync::atomic::{AtomicU64, Ordering};
23
24static COUNTER: AtomicU64 = AtomicU64::new(0);
25#[cfg(not(windows))]
26const MAX_NAME: usize = 200;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum EntryKind {
30 File,
31 Dir,
32 Symlink,
33 Other,
34}
35
36#[derive(Clone, Copy, Debug)]
37pub struct EntryStat {
38 pub kind: EntryKind,
39 pub size: u64,
40 pub dev: u64,
41 pub ino: u64,
42 pub mode: u32,
43 pub modified_unix_ms: u64,
44}
45
46pub fn validate_name(name: &str) -> Result<()> {
48 let bad = name.is_empty()
49 || name == "."
50 || name == ".."
51 || name.bytes().any(|b| b == b'/' || b == b'\\' || b == 0);
52 #[cfg(not(windows))]
53 let bad = bad || name.len() > MAX_NAME;
54 #[cfg(windows)]
55 let bad = bad || !sys::valid_component(name);
56 if bad {
57 return Err(FsError::BadName(name.to_string()));
58 }
59 Ok(())
60}
61
62#[derive(Debug)]
63pub struct StateDir {
64 dir: sys::Dir,
65 path: PathBuf,
66}
67
68impl StateDir {
69 pub fn open(path: &Path) -> Result<Self> {
71 Ok(Self {
72 dir: sys::Dir::pin(path, false)?,
73 path: path.to_path_buf(),
74 })
75 }
76
77 pub fn create(path: &Path) -> Result<Self> {
80 Ok(Self {
81 dir: sys::Dir::pin(path, true)?,
82 path: path.to_path_buf(),
83 })
84 }
85
86 pub fn create_in_resolved_parent(path: &Path) -> Result<Self> {
89 let name = path
90 .file_name()
91 .ok_or_else(|| FsError::BadName(path.display().to_string()))?;
92 let parent = match path.parent() {
93 Some(p) if !p.as_os_str().is_empty() => {
94 p.canonicalize().map_err(|e| FsError::io(p, e))?
95 }
96 _ => std::env::current_dir().map_err(|e| FsError::io(path, e))?,
97 };
98 Self::create(&parent.join(name))
99 }
100
101 pub fn path(&self) -> &Path {
102 &self.path
103 }
104
105 pub fn child(&self, name: &str) -> Result<StateDir> {
107 validate_name(name)?;
108 Ok(Self {
109 dir: self.dir.child(name)?,
110 path: self.path.join(name),
111 })
112 }
113
114 pub fn stat(&self, name: &str) -> Result<Option<EntryStat>> {
115 validate_name(name)?;
116 self.dir.stat(name)
117 }
118
119 pub fn exists(&self, name: &str) -> Result<bool> {
120 Ok(self.stat(name)?.is_some())
121 }
122
123 pub fn read(&self, name: &str, max_bytes: u64) -> Result<Vec<u8>> {
125 validate_name(name)?;
126 let path = self.path.join(name);
127 let mut file = self.dir.open_read(name)?;
128 let meta = file.metadata().map_err(|e| FsError::io(&path, e))?;
129 if !meta.is_file() {
130 return Err(FsError::NotAFile(path));
131 }
132 if meta.len() > max_bytes {
133 return Err(FsError::TooLarge {
134 path,
135 size: meta.len(),
136 limit: max_bytes,
137 });
138 }
139 let mut out = Vec::with_capacity(meta.len() as usize);
140 (&mut file)
141 .take(max_bytes + 1)
142 .read_to_end(&mut out)
143 .map_err(|e| FsError::io(&path, e))?;
144 if out.len() as u64 > max_bytes {
145 return Err(FsError::TooLarge {
146 path,
147 size: out.len() as u64,
148 limit: max_bytes,
149 });
150 }
151 Ok(out)
152 }
153
154 pub fn read_opt(&self, name: &str, max_bytes: u64) -> Result<Option<Vec<u8>>> {
156 match self.read(name, max_bytes) {
157 Ok(bytes) => Ok(Some(bytes)),
158 Err(e) if e.is_not_found() => Ok(None),
159 Err(e) => Err(e),
160 }
161 }
162
163 pub fn write(&self, name: &str, bytes: &[u8]) -> Result<()> {
165 self.publish(name, bytes, true)
166 }
167
168 pub fn write_new(&self, name: &str, bytes: &[u8]) -> Result<()> {
170 self.publish(name, bytes, false)
171 }
172
173 fn publish(&self, name: &str, bytes: &[u8], replace: bool) -> Result<()> {
174 validate_name(name)?;
175 let final_path = self.path.join(name);
176 let (temp, mut file) = self.create_temp(name)?;
177 let result = (|| -> Result<()> {
178 file.write_all(bytes)
179 .map_err(|e| FsError::io(&final_path, e))?;
180 file.sync_all().map_err(|e| FsError::io(&final_path, e))?;
181 drop(file);
182 if replace {
183 if let Some(existing) = self.dir.stat(name)? {
185 if existing.kind != EntryKind::File {
186 return Err(FsError::NotAFile(final_path.clone()));
187 }
188 }
189 self.dir.rename_replace(&temp, name)?;
190 } else {
191 self.dir.link_new(&temp, name).map_err(|e| match e {
192 FsError::AlreadyExists(_) => FsError::AlreadyExists(final_path.clone()),
193 other => other,
194 })?;
195 let _ = self.dir.unlink(&temp);
196 }
197 self.dir.sync();
198 Ok(())
199 })();
200 if result.is_err() {
201 let _ = self.dir.unlink(&temp);
202 }
203 result
204 }
205
206 fn create_temp(&self, name: &str) -> Result<(String, File)> {
207 for attempt in 0..32u32 {
208 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
209 #[cfg(windows)]
211 let prefix: String = name.chars().take(64).collect();
212 #[cfg(not(windows))]
213 let prefix = name;
214 let temp = format!(".{prefix}{TEMP_MARKER}{}-{n}-{attempt}", std::process::id());
215 match self.dir.create_new(&temp) {
216 Ok(file) => return Ok((temp, file)),
217 Err(FsError::AlreadyExists(_)) => continue,
218 Err(e) => return Err(e),
219 }
220 }
221 Err(FsError::AlreadyExists(self.path.join(name)))
222 }
223
224 pub fn remove(&self, name: &str) -> Result<bool> {
227 validate_name(name)?;
228 match self.dir.unlink(name) {
229 Ok(()) => Ok(true),
230 Err(e) if e.is_not_found() => Ok(false),
231 Err(e) => Err(e),
232 }
233 }
234
235 pub fn list(&self, limit: usize) -> Result<Vec<String>> {
237 self.dir.names(limit)
238 }
239
240 pub fn sweep_temps(&self, max_age: std::time::Duration) -> Result<usize> {
243 let mut removed = 0;
244 for name in self.dir.names(usize::MAX)? {
245 let Some(idx) = name.find(TEMP_MARKER) else {
246 continue;
247 };
248 let pid: Option<u32> = name[idx + TEMP_MARKER.len()..]
249 .split('-')
250 .next()
251 .and_then(|p| p.parse().ok());
252 let Some(pid) = pid else { continue };
253 let Some(stat) = self.dir.stat(&name)? else {
254 continue;
255 };
256 if stat.kind != EntryKind::File {
257 continue;
258 }
259 let age_ms = now_ms().saturating_sub(stat.modified_unix_ms);
260 let old = age_ms >= max_age.as_millis() as u64;
261 if ((pid != std::process::id() && !crate::process_alive(pid)) || old)
262 && self.dir.unlink(&name).is_ok()
263 {
264 removed += 1;
265 }
266 }
267 Ok(removed)
268 }
269}
270
271fn now_ms() -> u64 {
272 std::time::SystemTime::now()
273 .duration_since(std::time::UNIX_EPOCH)
274 .map(|d| d.as_millis() as u64)
275 .unwrap_or(0)
276}
277
278#[cfg(unix)]
279mod sys {
280 use super::{EntryKind, EntryStat};
281 use crate::error::{FsError, Result};
282 use std::ffi::{CStr, CString};
283 use std::fs::File;
284 use std::io;
285 use std::os::unix::ffi::OsStrExt;
286 use std::os::unix::fs::MetadataExt;
287 use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
288 use std::path::Path;
289
290 #[derive(Debug)]
291 pub struct Dir(File);
292
293 fn cname(name: &str) -> Result<CString> {
294 CString::new(name).map_err(|_| FsError::BadName(name.to_string()))
295 }
296
297 fn errno_is(e: &io::Error, code: i32) -> bool {
298 e.raw_os_error() == Some(code)
299 }
300
301 #[cfg(any(
302 target_os = "macos",
303 target_os = "ios",
304 target_os = "freebsd",
305 target_os = "openbsd",
306 target_os = "netbsd"
307 ))]
308 unsafe fn reset_errno() {
309 unsafe { *libc::__error() = 0 };
310 }
311 #[cfg(not(any(
312 target_os = "macos",
313 target_os = "ios",
314 target_os = "freebsd",
315 target_os = "openbsd",
316 target_os = "netbsd"
317 )))]
318 unsafe fn reset_errno() {
319 unsafe { *libc::__errno_location() = 0 };
320 }
321
322 fn kind_of(mode: u32) -> EntryKind {
323 match mode & libc::S_IFMT as u32 {
324 m if m == libc::S_IFREG as u32 => EntryKind::File,
325 m if m == libc::S_IFDIR as u32 => EntryKind::Dir,
326 m if m == libc::S_IFLNK as u32 => EntryKind::Symlink,
327 _ => EntryKind::Other,
328 }
329 }
330
331 fn fstat(file: &File) -> io::Result<libc::stat> {
332 let mut st: libc::stat = unsafe { std::mem::zeroed() };
334 if unsafe { libc::fstat(file.as_raw_fd(), &mut st) } != 0 {
335 return Err(io::Error::last_os_error());
336 }
337 Ok(st)
338 }
339
340 fn open_dir_at(base: RawFd, name: &CStr, shown: &Path) -> Result<File> {
341 let fd = unsafe {
343 libc::openat(
344 base,
345 name.as_ptr(),
346 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
347 )
348 };
349 if fd < 0 {
350 let e = io::Error::last_os_error();
351 if errno_is(&e, libc::ELOOP) {
352 return Err(FsError::Symlink(shown.to_path_buf()));
353 }
354 if errno_is(&e, libc::ENOTDIR) {
355 let mut st: libc::stat = unsafe { std::mem::zeroed() };
359 let rc = unsafe {
360 libc::fstatat(base, name.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW)
361 };
362 if rc == 0 && kind_of(st.st_mode as u32) == EntryKind::Symlink {
363 return Err(FsError::Symlink(shown.to_path_buf()));
364 }
365 return Err(FsError::NotADirectory(shown.to_path_buf()));
366 }
367 return Err(FsError::io(shown, e));
368 }
369 Ok(unsafe { File::from_raw_fd(fd) })
371 }
372
373 fn mkdir_at(base: RawFd, name: &CStr, shown: &Path) -> Result<()> {
374 if unsafe { libc::mkdirat(base, name.as_ptr(), 0o700) } != 0 {
376 let e = io::Error::last_os_error();
377 if !errno_is(&e, libc::EEXIST) {
378 return Err(FsError::io(shown, e));
379 }
380 }
381 Ok(())
382 }
383
384 fn verify(file: &File, shown: &Path) -> Result<libc::stat> {
385 let st = fstat(file).map_err(|e| FsError::io(shown, e))?;
386 if kind_of(st.st_mode as u32) != EntryKind::Dir {
387 return Err(FsError::NotADirectory(shown.to_path_buf()));
388 }
389 if st.st_uid != unsafe { libc::geteuid() } {
391 return Err(FsError::NotOwned(shown.to_path_buf()));
392 }
393 if st.st_mode as u32 & 0o022 != 0 {
394 return Err(FsError::TooPermissive(shown.to_path_buf()));
395 }
396 Ok(st)
397 }
398
399 impl Dir {
400 fn fd(&self) -> RawFd {
401 self.0.as_raw_fd()
402 }
403
404 pub fn pin(path: &Path, create: bool) -> Result<Dir> {
405 use std::path::Component;
406 let root = CString::new("/").unwrap();
407 let dot = CString::new(".").unwrap();
408 let mut current = open_dir_at(
409 libc::AT_FDCWD,
410 if path.is_absolute() { &root } else { &dot },
411 path,
412 )?;
413 let mut walked = std::path::PathBuf::new();
414 for component in path.components() {
415 let name = match component {
416 Component::RootDir => {
417 walked.push("/");
418 continue;
419 }
420 Component::CurDir => continue,
421 Component::ParentDir | Component::Prefix(_) => {
422 return Err(FsError::BadName(path.display().to_string()))
423 }
424 Component::Normal(n) => n,
425 };
426 walked.push(name);
427 let c = CString::new(name.as_bytes())
428 .map_err(|_| FsError::BadName(path.display().to_string()))?;
429 current = match open_dir_at(current.as_raw_fd(), &c, &walked) {
430 Ok(next) => next,
431 Err(FsError::Io { source, .. })
432 if create && errno_is(&source, libc::ENOENT) =>
433 {
434 mkdir_at(current.as_raw_fd(), &c, &walked)?;
435 open_dir_at(current.as_raw_fd(), &c, &walked)?
436 }
437 Err(e) => return Err(e),
438 };
439 }
440 let st = verify(¤t, path)?;
441 match std::fs::symlink_metadata(path) {
443 Ok(before)
444 if before.dev() == st.st_dev as u64 && before.ino() == st.st_ino as u64 => {}
445 Ok(_) => return Err(FsError::IdentityChanged(path.to_path_buf())),
446 Err(e) => return Err(FsError::io(path, e)),
447 }
448 Ok(Dir(current))
449 }
450
451 pub fn child(&self, name: &str) -> Result<Dir> {
452 let c = cname(name)?;
453 let shown = Path::new(name);
454 let file = match open_dir_at(self.fd(), &c, shown) {
455 Ok(f) => f,
456 Err(FsError::Io { source, .. }) if errno_is(&source, libc::ENOENT) => {
457 mkdir_at(self.fd(), &c, shown)?;
458 open_dir_at(self.fd(), &c, shown)?
459 }
460 Err(e) => return Err(e),
461 };
462 verify(&file, shown)?;
463 Ok(Dir(file))
464 }
465
466 pub fn stat(&self, name: &str) -> Result<Option<EntryStat>> {
467 let c = cname(name)?;
468 let mut st: libc::stat = unsafe { std::mem::zeroed() };
470 if unsafe { libc::fstatat(self.fd(), c.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW) }
471 == 0
472 {
473 return Ok(Some(EntryStat {
474 kind: kind_of(st.st_mode as u32),
475 size: st.st_size as u64,
476 dev: st.st_dev as u64,
477 ino: st.st_ino as u64,
478 mode: st.st_mode as u32 & 0o7777,
479 modified_unix_ms: (st.st_mtime as u64) * 1000
480 + (st.st_mtime_nsec as u64) / 1_000_000,
481 }));
482 }
483 let e = io::Error::last_os_error();
484 if errno_is(&e, libc::ENOENT) || errno_is(&e, libc::ENOTDIR) {
485 Ok(None)
486 } else {
487 Err(FsError::io(name, e))
488 }
489 }
490
491 pub fn open_read(&self, name: &str) -> Result<File> {
492 let c = cname(name)?;
493 let fd = unsafe {
495 libc::openat(
496 self.fd(),
497 c.as_ptr(),
498 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
499 )
500 };
501 if fd < 0 {
502 let e = io::Error::last_os_error();
503 if errno_is(&e, libc::ELOOP) {
504 return Err(FsError::Symlink(name.into()));
505 }
506 return Err(FsError::io(name, e));
507 }
508 Ok(unsafe { File::from_raw_fd(fd) })
510 }
511
512 pub fn create_new(&self, name: &str) -> Result<File> {
513 let c = cname(name)?;
514 let fd = unsafe {
516 libc::openat(
517 self.fd(),
518 c.as_ptr(),
519 libc::O_WRONLY
520 | libc::O_CREAT
521 | libc::O_EXCL
522 | libc::O_NOFOLLOW
523 | libc::O_CLOEXEC,
524 0o600 as libc::c_uint,
525 )
526 };
527 if fd < 0 {
528 let e = io::Error::last_os_error();
529 if errno_is(&e, libc::EEXIST) {
530 return Err(FsError::AlreadyExists(name.into()));
531 }
532 return Err(FsError::io(name, e));
533 }
534 Ok(unsafe { File::from_raw_fd(fd) })
536 }
537
538 pub fn rename_replace(&self, from: &str, to: &str) -> Result<()> {
539 let (cf, ct) = (cname(from)?, cname(to)?);
540 if unsafe { libc::renameat(self.fd(), cf.as_ptr(), self.fd(), ct.as_ptr()) } != 0 {
542 return Err(FsError::io(to, io::Error::last_os_error()));
543 }
544 Ok(())
545 }
546
547 pub fn link_new(&self, from: &str, to: &str) -> Result<()> {
549 let (cf, ct) = (cname(from)?, cname(to)?);
550 if unsafe { libc::linkat(self.fd(), cf.as_ptr(), self.fd(), ct.as_ptr(), 0) } != 0 {
552 let e = io::Error::last_os_error();
553 if errno_is(&e, libc::EEXIST) {
554 return Err(FsError::AlreadyExists(to.into()));
555 }
556 return Err(FsError::io(to, e));
557 }
558 Ok(())
559 }
560
561 pub fn unlink(&self, name: &str) -> Result<()> {
562 let c = cname(name)?;
563 if unsafe { libc::unlinkat(self.fd(), c.as_ptr(), 0) } != 0 {
565 return Err(FsError::io(name, io::Error::last_os_error()));
566 }
567 Ok(())
568 }
569
570 pub fn sync(&self) {
571 let _ = self.0.sync_all();
572 }
573
574 pub fn names(&self, limit: usize) -> Result<Vec<String>> {
575 let dup = self
576 .0
577 .try_clone()
578 .map_err(|e| FsError::io(".", e))?
579 .into_raw_fd();
580 let raw = unsafe { libc::fdopendir(dup) };
582 if raw.is_null() {
583 let e = io::Error::last_os_error();
584 unsafe { libc::close(dup) };
586 return Err(FsError::io(".", e));
587 }
588 struct DirGuard(*mut libc::DIR);
589 impl Drop for DirGuard {
590 fn drop(&mut self) {
591 unsafe { libc::closedir(self.0) };
593 }
594 }
595 let guard = DirGuard(raw);
596 unsafe { libc::rewinddir(guard.0) };
599 let mut out = Vec::new();
600 loop {
601 unsafe { reset_errno() };
603 let entry = unsafe { libc::readdir(guard.0) };
604 if entry.is_null() {
605 let e = io::Error::last_os_error();
606 return if e.raw_os_error() == Some(0) {
607 Ok(out)
608 } else {
609 Err(FsError::io(".", e))
610 };
611 }
612 let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }
614 .to_string_lossy()
615 .into_owned();
616 if name != "." && name != ".." {
617 if out.len() >= limit {
618 return Err(FsError::TooManyEntries(limit));
619 }
620 out.push(name);
621 }
622 }
623 }
624 }
625}
626
627#[cfg(windows)]
628#[path = "state/windows.rs"]
629mod sys;
630
631#[cfg(not(any(unix, windows)))]
632mod sys {
633 use super::{EntryKind, EntryStat};
634 use crate::error::{FsError, Result};
635 use std::fs::{self, File, OpenOptions};
636 use std::path::{Path, PathBuf};
637
638 #[derive(Debug)]
639 pub struct Dir(PathBuf);
640
641 fn is_link(meta: &fs::Metadata) -> bool {
642 if meta.file_type().is_symlink() {
643 return true;
644 }
645 #[cfg(windows)]
646 {
647 use std::os::windows::fs::MetadataExt;
648 if meta.file_attributes() & 0x400 != 0 {
649 return true; }
651 }
652 false
653 }
654
655 fn reject_links(path: &Path) -> Result<()> {
656 for ancestor in path.ancestors() {
657 match fs::symlink_metadata(ancestor) {
658 Ok(m) if is_link(&m) => return Err(FsError::Symlink(path.to_path_buf())),
659 Ok(_) | Err(_) => {}
660 }
661 }
662 Ok(())
663 }
664
665 impl Dir {
666 pub fn pin(path: &Path, create: bool) -> Result<Dir> {
667 reject_links(path)?;
668 if create {
669 fs::create_dir_all(path).map_err(|e| FsError::io(path, e))?;
670 }
671 reject_links(path)?;
672 if !fs::metadata(path)
673 .map_err(|e| FsError::io(path, e))?
674 .is_dir()
675 {
676 return Err(FsError::NotADirectory(path.to_path_buf()));
677 }
678 Ok(Dir(path.to_path_buf()))
679 }
680 pub fn child(&self, name: &str) -> Result<Dir> {
681 let p = self.0.join(name);
682 Dir::pin(&p, true)
683 }
684 pub fn stat(&self, name: &str) -> Result<Option<EntryStat>> {
685 let p = self.0.join(name);
686 match fs::symlink_metadata(&p) {
687 Ok(m) => Ok(Some(EntryStat {
688 kind: if is_link(&m) {
689 EntryKind::Symlink
690 } else if m.is_dir() {
691 EntryKind::Dir
692 } else if m.is_file() {
693 EntryKind::File
694 } else {
695 EntryKind::Other
696 },
697 size: m.len(),
698 dev: 0,
699 ino: 0,
700 mode: 0,
701 modified_unix_ms: m
702 .modified()
703 .ok()
704 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
705 .map(|d| d.as_millis() as u64)
706 .unwrap_or(0),
707 })),
708 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
709 Err(e) => Err(FsError::io(p, e)),
710 }
711 }
712 pub fn open_read(&self, name: &str) -> Result<File> {
713 let p = self.0.join(name);
714 if let Ok(m) = fs::symlink_metadata(&p) {
715 if is_link(&m) {
716 return Err(FsError::Symlink(p));
717 }
718 }
719 File::open(&p).map_err(|e| FsError::io(p, e))
720 }
721 pub fn create_new(&self, name: &str) -> Result<File> {
722 let p = self.0.join(name);
723 let f = OpenOptions::new()
724 .write(true)
725 .create_new(true)
726 .open(&p)
727 .map_err(|e| {
728 if e.kind() == std::io::ErrorKind::AlreadyExists {
729 FsError::AlreadyExists(p.clone())
730 } else {
731 FsError::io(&p, e)
732 }
733 })?;
734 crate::private::restrict_owner_only(&p)?;
735 Ok(f)
736 }
737 pub fn rename_replace(&self, from: &str, to: &str) -> Result<()> {
738 fs::rename(self.0.join(from), self.0.join(to))
739 .map_err(|e| FsError::io(self.0.join(to), e))
740 }
741 pub fn link_new(&self, from: &str, to: &str) -> Result<()> {
742 fs::hard_link(self.0.join(from), self.0.join(to)).map_err(|e| {
743 if e.kind() == std::io::ErrorKind::AlreadyExists {
744 FsError::AlreadyExists(self.0.join(to))
745 } else {
746 FsError::io(self.0.join(to), e)
747 }
748 })
749 }
750 pub fn unlink(&self, name: &str) -> Result<()> {
751 fs::remove_file(self.0.join(name)).map_err(|e| FsError::io(self.0.join(name), e))
752 }
753 pub fn sync(&self) {}
754 pub fn names(&self, limit: usize) -> Result<Vec<String>> {
755 let mut out = Vec::new();
756 for entry in fs::read_dir(&self.0).map_err(|e| FsError::io(&self.0, e))? {
757 let entry = entry.map_err(|e| FsError::io(&self.0, e))?;
758 if out.len() >= limit {
759 return Err(FsError::TooManyEntries(limit));
760 }
761 out.push(entry.file_name().to_string_lossy().into_owned());
762 }
763 Ok(out)
764 }
765 }
766}