Skip to main content

rightkit_fs/
state.rs

1//! A descriptor-pinned state directory.
2//!
3//! On unix every operation goes through one verified directory descriptor: the directory is
4//! opened component by component with `O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC`, `fstat`-checked
5//! (a directory, owned by the effective user, not writable by group/others) and compared with an
6//! `lstat` taken before the walk. Reads, writes, renames, unlinks and listings are `*at`
7//! operations relative to that descriptor, so renaming or swapping the path afterwards (or
8//! planting a symlink at a child name) cannot redirect anything. Donor: Cockpit
9//! `core/src/store.rs` (pinned descriptor, tested against swaps).
10//!
11//! Windows walks components without following reparse points, then retains a directory handle
12//! without delete sharing. Child opens use `NtCreateFile` relative to that handle; publication
13//! and removal use handle-based rename/disposition. New entries have an owner-only DACL at
14//! creation. Reparse-point children are refused, including removal. Other targets retain a
15//! path-based fallback.
16
17use 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
46/// Entry names are single path components: no separators, no NUL, not `.`/`..`.
47pub 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    /// Pin an existing directory.
70    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    /// Create the directory (and missing ancestors, owner-only, through descriptor-relative
78    /// `mkdirat`) if needed, then pin it.
79    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    /// Like [`create`](Self::create) but resolves symlinks in the *parent* first (for example
87    /// macOS `/var` -> `/private/var`). The final component must still be a real directory.
88    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    /// Create-or-open a direct child directory (owner-only), pinned relative to this one.
106    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    /// Read a regular file (never through a symlink), refusing anything over `max_bytes`.
124    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    /// Like [`read`](Self::read) but `Ok(None)` when the entry does not exist.
155    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    /// Atomically create or replace `name` (owner-only, `fsync`ed file and directory).
164    pub fn write(&self, name: &str, bytes: &[u8]) -> Result<()> {
165        self.publish(name, bytes, true)
166    }
167
168    /// Atomically create `name`, failing with [`FsError::AlreadyExists`] rather than replacing.
169    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                // Never replace a symlinked or non-file destination through the rename.
184                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            // Bound the Windows prefix independently of the public 255 UTF-16-unit name.
210            #[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    /// Remove a file. Unix removes a symlink itself; Windows refuses reparse points.
225    /// Missing is `Ok(false)`.
226    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    /// Names of direct children, capped at `limit` entries (error past the cap).
236    pub fn list(&self, limit: usize) -> Result<Vec<String>> {
237        self.dir.names(limit)
238    }
239
240    /// Remove temp files left by writers that were killed mid-write (pid gone, or older than
241    /// `max_age`). Returns how many were removed.
242    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        // SAFETY: zeroed stat is a valid out-parameter and fd is open.
333        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        // SAFETY: base is an open directory fd (or AT_FDCWD) and name is NUL-terminated.
342        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                // A symlink to a directory reports ENOTDIR with O_DIRECTORY|O_NOFOLLOW on some
356                // kernels; distinguish it from a plain non-directory.
357                // SAFETY: as above, with a valid out-parameter.
358                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        // SAFETY: fd was just returned by openat and is owned here.
370        Ok(unsafe { File::from_raw_fd(fd) })
371    }
372
373    fn mkdir_at(base: RawFd, name: &CStr, shown: &Path) -> Result<()> {
374        // SAFETY: valid fd and C string.
375        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        // SAFETY: geteuid has no preconditions.
390        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(&current, path)?;
441            // Defence in depth: the pinned inode must be the one the path names right now.
442            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            // SAFETY: valid fd, C string, and zeroed out-parameter.
469            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            // SAFETY: valid fd and C string.
494            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            // SAFETY: fd just returned by openat.
509            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            // SAFETY: valid fd and C string; O_EXCL|O_NOFOLLOW means a planted symlink is never followed.
515            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            // SAFETY: fd just returned by openat.
535            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            // SAFETY: valid fds and C strings.
541            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        /// No-replace publication: a hard link fails with EEXIST if `to` exists.
548        pub fn link_new(&self, from: &str, to: &str) -> Result<()> {
549            let (cf, ct) = (cname(from)?, cname(to)?);
550            // SAFETY: valid fds and C strings.
551            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            // SAFETY: valid fd and C string.
564            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            // SAFETY: dup is a fresh owned fd; on success fdopendir takes ownership of it.
581            let raw = unsafe { libc::fdopendir(dup) };
582            if raw.is_null() {
583                let e = io::Error::last_os_error();
584                // SAFETY: fdopendir failed, so dup is still ours to close.
585                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                    // SAFETY: pointer came from a successful fdopendir.
592                    unsafe { libc::closedir(self.0) };
593                }
594            }
595            let guard = DirGuard(raw);
596            // The dup shares the file offset with the pinned fd; start from the beginning.
597            // SAFETY: valid DIR*.
598            unsafe { libc::rewinddir(guard.0) };
599            let mut out = Vec::new();
600            loop {
601                // SAFETY: readdir signals end vs error through errno, which we clear first.
602                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                // SAFETY: d_name is a NUL-terminated array inside a live dirent.
613                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; // FILE_ATTRIBUTE_REPARSE_POINT
650            }
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}