Skip to main content

haz_vfs/
std_impl.rs

1//! Real-filesystem implementation of [`Filesystem`].
2
3use std::path::{Path, PathBuf};
4
5use crate::traits::{DirEntry, EntryKind, Filesystem, FsError, FsMetadata, WritableFilesystem};
6
7/// [`Filesystem`] implementation backed by [`std::fs`].
8///
9/// Every method delegates directly to the standard library. Symlink
10/// semantics, depth caps, and platform-specific path normalisation
11/// are whatever `std::fs` provides on the host platform.
12///
13/// OS errors are translated to the trait-level vocabulary by
14/// inspecting `raw_os_error`:
15///
16/// - `ELOOP` on Unix and `ERROR_CANT_RESOLVE_FILENAME` (1921) on
17///   Windows map to [`FsError::SymlinkLoop`].
18/// - `EISDIR` on Unix maps to [`FsError::NotAFile`] when surfaced
19///   from `read`; on Windows `ERROR_ACCESS_DENIED` is overloaded
20///   with genuine permission failures and is NOT remapped here.
21/// - `ENOTDIR` on Unix and `ERROR_DIRECTORY` (267) on Windows map
22///   to [`FsError::NotADirectory`] when surfaced from `read_dir`.
23///
24/// Every other I/O error falls through to [`FsError::Io`].
25///
26/// `read` performs an additional [`Filesystem::metadata`] pre-check
27/// before delegating to [`std::fs::read`]: the OS does not surface
28/// "not a regular file" through a single error code (FIFO opens
29/// block, char-device opens succeed, sockets emit `ENXIO`), so a
30/// kind check by stat is the only way to honour the
31/// [`FsError::NotAFile`] contract for those kinds. The `EISDIR`
32/// translation remains in place as a TOCTOU backstop.
33#[derive(Debug, Default, Clone, Copy)]
34pub struct StdFilesystem;
35
36impl StdFilesystem {
37    /// Construct a new [`StdFilesystem`].
38    #[must_use]
39    pub const fn new() -> Self {
40        Self
41    }
42}
43
44/// Canonical host-filesystem path produced by
45/// [`StdFilesystem::canonicalize`].
46///
47/// Wraps a [`PathBuf`] returned by [`std::fs::canonicalize`]; the
48/// inner field is private and the type is constructible only inside
49/// this module, so a `StdCanonicalPath` can only be obtained as the
50/// successful result of a real-filesystem canonicalisation call.
51///
52/// This is the host-canonical analogue of, but disjoint from,
53/// `MemCanonicalPath` (in `haz-vfs-testing`): the
54/// type system prevents mixing canonical paths produced by
55/// different [`Filesystem`] implementations.
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct StdCanonicalPath(PathBuf);
58
59impl StdCanonicalPath {
60    /// Borrow the wrapped path as a [`Path`].
61    #[must_use]
62    pub fn as_path(&self) -> &Path {
63        &self.0
64    }
65
66    /// Unwrap into the inner [`PathBuf`], discarding the canonical-
67    /// path invariant. Useful for handing the value to APIs that
68    /// require an owned [`PathBuf`] and do not need the invariant.
69    #[must_use]
70    pub fn into_path_buf(self) -> PathBuf {
71        self.0
72    }
73}
74
75impl AsRef<Path> for StdCanonicalPath {
76    fn as_ref(&self) -> &Path {
77        &self.0
78    }
79}
80
81impl std::fmt::Display for StdCanonicalPath {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        std::fmt::Display::fmt(&self.0.display(), f)
84    }
85}
86
87fn require_absolute(path: &Path) -> Result<(), FsError> {
88    if path.is_absolute() {
89        Ok(())
90    } else {
91        Err(FsError::NotAbsolute {
92            path: path.to_path_buf(),
93        })
94    }
95}
96
97fn map_io(path: &Path, e: std::io::Error) -> FsError {
98    if is_symlink_loop(&e) {
99        return FsError::SymlinkLoop {
100            path: path.to_path_buf(),
101        };
102    }
103    FsError::from_io(path.to_path_buf(), e)
104}
105
106// TODO: collapse the cfg branches below into single
107// `e.kind() == std::io::ErrorKind::{FilesystemLoop,IsADirectory,
108// NotADirectory}` checks once `feature(io_error_more)`
109// (rust-lang/rust#86442) stabilises. At that point `libc` is no
110// longer needed by this module.
111#[cfg(unix)]
112fn is_symlink_loop(e: &std::io::Error) -> bool {
113    e.raw_os_error() == Some(libc::ELOOP)
114}
115
116#[cfg(unix)]
117fn is_is_a_directory(e: &std::io::Error) -> bool {
118    e.raw_os_error() == Some(libc::EISDIR)
119}
120
121#[cfg(unix)]
122fn is_not_a_directory(e: &std::io::Error) -> bool {
123    e.raw_os_error() == Some(libc::ENOTDIR)
124}
125
126#[cfg(windows)]
127fn is_symlink_loop(e: &std::io::Error) -> bool {
128    /// `ERROR_CANT_RESOLVE_FILENAME`: Windows surfaces reparse-point
129    /// loops and depth-cap exhaustion with this Win32 code.
130    const ERROR_CANT_RESOLVE_FILENAME: i32 = 1921;
131    e.raw_os_error() == Some(ERROR_CANT_RESOLVE_FILENAME)
132}
133
134#[cfg(windows)]
135fn is_is_a_directory(_e: &std::io::Error) -> bool {
136    // Windows surfaces "tried to open a directory as a file" as
137    // ERROR_ACCESS_DENIED (5), which is overloaded with genuine
138    // permission denials and cannot be distinguished from them
139    // here. Callers needing the distinction on Windows should
140    // pre-check via [`Filesystem::metadata`].
141    false
142}
143
144#[cfg(windows)]
145fn is_not_a_directory(e: &std::io::Error) -> bool {
146    /// `ERROR_DIRECTORY`: "The directory name is invalid." Returned
147    /// by `FindFirstFile`/`FindNextFile` and friends when the path
148    /// is a regular file rather than a directory.
149    const ERROR_DIRECTORY: i32 = 267;
150    e.raw_os_error() == Some(ERROR_DIRECTORY)
151}
152
153#[cfg(not(any(unix, windows)))]
154fn is_symlink_loop(_e: &std::io::Error) -> bool {
155    false
156}
157
158#[cfg(not(any(unix, windows)))]
159fn is_is_a_directory(_e: &std::io::Error) -> bool {
160    false
161}
162
163#[cfg(not(any(unix, windows)))]
164fn is_not_a_directory(_e: &std::io::Error) -> bool {
165    false
166}
167
168fn metadata_from_std(path: &Path, m: &std::fs::Metadata) -> Result<FsMetadata, FsError> {
169    Ok(FsMetadata {
170        kind: kind_from_file_type(path, m.file_type())?,
171        size: m.len(),
172    })
173}
174
175#[cfg(unix)]
176fn kind_from_file_type(path: &Path, ft: std::fs::FileType) -> Result<EntryKind, FsError> {
177    use std::os::unix::fs::FileTypeExt;
178
179    if ft.is_dir() {
180        Ok(EntryKind::Dir)
181    } else if ft.is_file() {
182        Ok(EntryKind::File)
183    } else if ft.is_symlink() {
184        Ok(EntryKind::Symlink)
185    } else if ft.is_block_device() {
186        Ok(EntryKind::BlockDevice)
187    } else if ft.is_char_device() {
188        Ok(EntryKind::CharDevice)
189    } else if ft.is_fifo() {
190        Ok(EntryKind::Fifo)
191    } else if ft.is_socket() {
192        Ok(EntryKind::Socket)
193    } else {
194        // POSIX `S_IFMT` only defines the seven kinds tested above;
195        // any other observation here would be a kernel or libstd
196        // surprise, not an OS-level legitimate state.
197        Err(FsError::UnknownEntryKind {
198            path: path.to_path_buf(),
199        })
200    }
201}
202
203#[cfg(not(unix))]
204fn kind_from_file_type(path: &Path, ft: std::fs::FileType) -> Result<EntryKind, FsError> {
205    if ft.is_dir() {
206        Ok(EntryKind::Dir)
207    } else if ft.is_file() {
208        Ok(EntryKind::File)
209    } else if ft.is_symlink() {
210        Ok(EntryKind::Symlink)
211    } else {
212        // On Windows, non-symlink reparse points (dedup, OneDrive
213        // placeholders, etc.) report false for all three of
214        // `is_dir`/`is_file`/`is_symlink`. We do not have a stable
215        // cross-platform way to classify them further.
216        Err(FsError::UnknownEntryKind {
217            path: path.to_path_buf(),
218        })
219    }
220}
221
222impl Filesystem for StdFilesystem {
223    type CanonicalPath = StdCanonicalPath;
224
225    fn metadata(&self, path: &Path) -> Result<FsMetadata, FsError> {
226        require_absolute(path)?;
227        let m = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
228        metadata_from_std(path, &m)
229    }
230
231    fn symlink_metadata(&self, path: &Path) -> Result<FsMetadata, FsError> {
232        require_absolute(path)?;
233        let m = std::fs::symlink_metadata(path).map_err(|e| map_io(path, e))?;
234        metadata_from_std(path, &m)
235    }
236
237    fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>, FsError> {
238        require_absolute(path)?;
239        let iter = std::fs::read_dir(path).map_err(|e| {
240            if is_not_a_directory(&e) {
241                FsError::NotADirectory {
242                    path: path.to_path_buf(),
243                }
244            } else {
245                map_io(path, e)
246            }
247        })?;
248        let mut out = Vec::new();
249        for entry in iter {
250            let entry = entry.map_err(|e| map_io(path, e))?;
251            let entry_path = entry.path();
252            let ft = entry.file_type().map_err(|e| map_io(&entry_path, e))?;
253            let kind = kind_from_file_type(&entry_path, ft)?;
254            let m = entry.metadata().map_err(|e| map_io(&entry_path, e))?;
255            out.push(DirEntry {
256                path: entry_path,
257                metadata: FsMetadata {
258                    kind,
259                    size: m.len(),
260                },
261            });
262        }
263        Ok(out)
264    }
265
266    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
267        // Kind pre-check via [`Filesystem::metadata`] (which follows
268        // symlinks, matching this method's symlink semantics): the
269        // OS does not surface "not a regular file" via a single
270        // error code, so the only reliable way to honour the
271        // [`FsError::NotAFile`] contract for FIFOs, sockets, and
272        // character/block devices is a stat before the open.
273        let m = self.metadata(path)?;
274        if m.kind != EntryKind::File {
275            return Err(FsError::NotAFile {
276                path: path.to_path_buf(),
277            });
278        }
279        std::fs::read(path).map_err(|e| {
280            // TOCTOU backstop: if the entry's kind changed between
281            // the metadata call above and this open, an `EISDIR`
282            // can still surface; map it to NotAFile rather than
283            // letting it leak through as Io.
284            if is_is_a_directory(&e) {
285                FsError::NotAFile {
286                    path: path.to_path_buf(),
287                }
288            } else {
289                map_io(path, e)
290            }
291        })
292    }
293
294    fn permissions(&self, path: &Path) -> Result<u32, FsError> {
295        // Kind pre-check via `metadata` (which follows symlinks):
296        // the trait contract is regular-file-only; non-files are
297        // reported as `NotAFile`. Doing the stat through `metadata`
298        // also covers symlink-loop and unknown-kind branches in
299        // one place.
300        let m = self.metadata(path)?;
301        if m.kind != EntryKind::File {
302            return Err(FsError::NotAFile {
303                path: path.to_path_buf(),
304            });
305        }
306        let m_std = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
307        Ok(permissions_impl(&m_std))
308    }
309
310    fn canonicalize(&self, path: &Path) -> Result<Self::CanonicalPath, FsError> {
311        require_absolute(path)?;
312        std::fs::canonicalize(path)
313            .map(StdCanonicalPath)
314            .map_err(|e| map_io(path, e))
315    }
316
317    fn read_link(&self, path: &Path) -> Result<PathBuf, FsError> {
318        require_absolute(path)?;
319        match std::fs::read_link(path) {
320            Ok(p) => Ok(p),
321            Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => Err(FsError::NotASymlink {
322                path: path.to_path_buf(),
323            }),
324            Err(e) => Err(map_io(path, e)),
325        }
326    }
327}
328
329impl WritableFilesystem for StdFilesystem {
330    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
331        require_absolute(path)?;
332        std::fs::create_dir_all(path).map_err(|e| {
333            if is_not_a_directory(&e) {
334                FsError::NotADirectory {
335                    path: path.to_path_buf(),
336                }
337            } else {
338                map_io(path, e)
339            }
340        })
341    }
342
343    fn write_file(&self, path: &Path, contents: &[u8]) -> Result<(), FsError> {
344        require_absolute(path)?;
345        std::fs::write(path, contents).map_err(|e| {
346            if is_not_a_directory(&e) {
347                FsError::NotADirectory {
348                    path: path.to_path_buf(),
349                }
350            } else {
351                map_io(path, e)
352            }
353        })
354    }
355
356    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
357        require_absolute(from)?;
358        require_absolute(to)?;
359        std::fs::rename(from, to).map_err(|e| map_io(from, e))
360    }
361
362    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
363        require_absolute(path)?;
364        std::fs::remove_dir_all(path).map_err(|e| map_io(path, e))
365    }
366
367    fn set_permissions(&self, path: &Path, mode: u32) -> Result<(), FsError> {
368        require_absolute(path)?;
369        set_permissions_impl(path, mode)
370    }
371
372    fn fsync_file(&self, path: &Path) -> Result<(), FsError> {
373        require_absolute(path)?;
374        let f = std::fs::File::open(path).map_err(|e| map_io(path, e))?;
375        f.sync_all().map_err(|e| map_io(path, e))
376    }
377
378    fn fsync_dir(&self, path: &Path) -> Result<(), FsError> {
379        require_absolute(path)?;
380        fsync_dir_impl(path)
381    }
382}
383
384#[cfg(unix)]
385fn set_permissions_impl(path: &Path, mode: u32) -> Result<(), FsError> {
386    use std::os::unix::fs::PermissionsExt;
387    let perms = std::fs::Permissions::from_mode(mode);
388    std::fs::set_permissions(path, perms).map_err(|e| map_io(path, e))
389}
390
391#[cfg(not(unix))]
392fn set_permissions_impl(path: &Path, mode: u32) -> Result<(), FsError> {
393    // Windows has no general mapping from Unix mode bits to its ACL
394    // model. Best-effort: honour the owner-write bit by clearing or
395    // setting the read-only flag (matching how cargo and git
396    // round-trip executable bits on Windows). Higher-order Unix
397    // bits are silently dropped, as the trait documents.
398    let m = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
399    let mut perms = m.permissions();
400    let owner_writable = (mode & 0o200) != 0;
401    perms.set_readonly(!owner_writable);
402    std::fs::set_permissions(path, perms).map_err(|e| map_io(path, e))
403}
404
405#[cfg(unix)]
406fn permissions_impl(m: &std::fs::Metadata) -> u32 {
407    use std::os::unix::fs::PermissionsExt;
408    // Mask to the low 12 bits: the POSIX permission set plus the
409    // sticky / setuid / setgid bits. The high bits of `st_mode` are
410    // kind bits and are not what callers want to round-trip through
411    // `set_permissions`.
412    m.permissions().mode() & 0o7777
413}
414
415#[cfg(not(unix))]
416fn permissions_impl(m: &std::fs::Metadata) -> u32 {
417    // Inverse of the Windows arm of `set_permissions_impl`: synthesise
418    // a Unix-like mode from the read-only flag alone. Read-only entries
419    // surface as `0o444`; writable entries surface as `0o644`, matching
420    // the default file mode `MemFilesystem` uses and round-tripping
421    // through the owner-write bit that `set_permissions_impl` honours.
422    if m.permissions().readonly() {
423        0o444
424    } else {
425        0o644
426    }
427}
428
429#[cfg(unix)]
430fn fsync_dir_impl(path: &Path) -> Result<(), FsError> {
431    // Opening a directory for fsync is a Unix idiom; `File::open`
432    // succeeds on a directory on Linux and BSDs, and `sync_all` on
433    // the resulting descriptor calls `fsync(2)`.
434    let f = std::fs::File::open(path).map_err(|e| map_io(path, e))?;
435    f.sync_all().map_err(|e| map_io(path, e))
436}
437
438#[cfg(not(unix))]
439fn fsync_dir_impl(path: &Path) -> Result<(), FsError> {
440    // Windows does not provide a directory-level fsync. Verify the
441    // path exists and is a directory so callers get a consistent
442    // error story across platforms; otherwise no-op.
443    let m = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
444    if !m.is_dir() {
445        return Err(FsError::NotADirectory {
446            path: path.to_path_buf(),
447        });
448    }
449    Ok(())
450}
451
452#[cfg(test)]
453mod tests {
454    use std::fs;
455    use std::os::unix::fs::symlink;
456
457    use tempfile::TempDir;
458
459    use crate::std_impl::StdFilesystem;
460    use crate::traits::{EntryKind, Filesystem, FsError};
461
462    fn td() -> TempDir {
463        tempfile::tempdir().expect("create tempdir")
464    }
465
466    #[test]
467    fn rejects_relative_paths() {
468        let fs_ = StdFilesystem::new();
469        let err = fs_.metadata(std::path::Path::new("relative")).unwrap_err();
470        assert!(matches!(err, FsError::NotAbsolute { .. }));
471    }
472
473    #[test]
474    fn metadata_follows_symlinks() {
475        let dir = td();
476        let target = dir.path().join("target.txt");
477        fs::write(&target, "hi").unwrap();
478        let link = dir.path().join("link.txt");
479        symlink(&target, &link).unwrap();
480
481        let fs_ = StdFilesystem::new();
482        let m = fs_.metadata(&link).unwrap();
483        assert_eq!(m.kind, EntryKind::File);
484    }
485
486    #[test]
487    fn symlink_metadata_does_not_follow() {
488        let dir = td();
489        let target = dir.path().join("target.txt");
490        fs::write(&target, "hi").unwrap();
491        let link = dir.path().join("link.txt");
492        symlink(&target, &link).unwrap();
493
494        let fs_ = StdFilesystem::new();
495        let m = fs_.symlink_metadata(&link).unwrap();
496        assert_eq!(m.kind, EntryKind::Symlink);
497    }
498
499    #[test]
500    fn read_follows_symlinks() {
501        let dir = td();
502        let target = dir.path().join("data.txt");
503        fs::write(&target, b"hello").unwrap();
504        let link = dir.path().join("link.txt");
505        symlink(&target, &link).unwrap();
506
507        let fs_ = StdFilesystem::new();
508        let contents = fs_.read(&link).unwrap();
509        assert_eq!(contents, b"hello");
510    }
511
512    #[test]
513    fn read_dir_classifies_entries() {
514        let dir = td();
515        fs::write(dir.path().join("a.txt"), "").unwrap();
516        fs::create_dir(dir.path().join("b")).unwrap();
517        symlink(dir.path().join("a.txt"), dir.path().join("c")).unwrap();
518
519        let fs_ = StdFilesystem::new();
520        let mut entries = fs_.read_dir(dir.path()).unwrap();
521        entries.sort_by(|x, y| x.path.cmp(&y.path));
522
523        assert_eq!(entries.len(), 3);
524        let by_name: std::collections::BTreeMap<_, _> = entries
525            .iter()
526            .map(|e| (e.path.file_name().unwrap().to_owned(), e.metadata.kind))
527            .collect();
528        assert_eq!(by_name[std::ffi::OsStr::new("a.txt")], EntryKind::File);
529        assert_eq!(by_name[std::ffi::OsStr::new("b")], EntryKind::Dir);
530        assert_eq!(by_name[std::ffi::OsStr::new("c")], EntryKind::Symlink);
531    }
532
533    #[test]
534    fn canonicalize_detects_symlink_loop() {
535        let dir = td();
536        let a = dir.path().join("a");
537        let b = dir.path().join("b");
538        symlink(&b, &a).unwrap();
539        symlink(&a, &b).unwrap();
540
541        let fs_ = StdFilesystem::new();
542        let err = fs_.canonicalize(&a).unwrap_err();
543        assert!(
544            matches!(err, FsError::SymlinkLoop { .. }),
545            "expected SymlinkLoop, got {err:?}"
546        );
547    }
548
549    #[test]
550    fn canonicalize_resolves_symlinks() {
551        let dir = td();
552        let target = dir.path().join("real");
553        fs::create_dir(&target).unwrap();
554        let link = dir.path().join("link");
555        symlink(&target, &link).unwrap();
556
557        let fs_ = StdFilesystem::new();
558        let canon = fs_.canonicalize(&link).unwrap();
559        assert_eq!(canon, fs_.canonicalize(&target).unwrap());
560    }
561
562    #[test]
563    fn read_link_returns_target() {
564        let dir = td();
565        let target = dir.path().join("data");
566        fs::write(&target, "").unwrap();
567        let link = dir.path().join("link");
568        symlink(&target, &link).unwrap();
569
570        let fs_ = StdFilesystem::new();
571        let read = fs_.read_link(&link).unwrap();
572        assert_eq!(read, target);
573    }
574
575    #[test]
576    fn read_link_on_regular_file_errors() {
577        let dir = td();
578        let f = dir.path().join("file");
579        fs::write(&f, "").unwrap();
580
581        let fs_ = StdFilesystem::new();
582        let err = fs_.read_link(&f).unwrap_err();
583        assert!(matches!(err, FsError::NotASymlink { .. }));
584    }
585
586    #[test]
587    fn classifies_fifo() {
588        let dir = td();
589        let fifo = dir.path().join("pipe");
590        let status = std::process::Command::new("mkfifo")
591            .arg(&fifo)
592            .status()
593            .expect("spawn mkfifo");
594        assert!(status.success(), "mkfifo exited with {status}");
595
596        let fs_ = StdFilesystem::new();
597        let m = fs_.symlink_metadata(&fifo).unwrap();
598        assert_eq!(m.kind, EntryKind::Fifo);
599    }
600
601    #[test]
602    fn metadata_not_found() {
603        let dir = td();
604        let fs_ = StdFilesystem::new();
605        let err = fs_.metadata(&dir.path().join("ghost")).unwrap_err();
606        assert!(matches!(err, FsError::NotFound { .. }));
607    }
608
609    #[test]
610    fn each_method_rejects_relative_paths() {
611        let fs_ = StdFilesystem::new();
612        let p = std::path::Path::new("relative");
613        assert!(matches!(
614            fs_.metadata(p).unwrap_err(),
615            FsError::NotAbsolute { .. }
616        ));
617        assert!(matches!(
618            fs_.symlink_metadata(p).unwrap_err(),
619            FsError::NotAbsolute { .. }
620        ));
621        assert!(matches!(
622            fs_.read(p).unwrap_err(),
623            FsError::NotAbsolute { .. }
624        ));
625        assert!(matches!(
626            fs_.read_dir(p).unwrap_err(),
627            FsError::NotAbsolute { .. }
628        ));
629        assert!(matches!(
630            fs_.canonicalize(p).unwrap_err(),
631            FsError::NotAbsolute { .. }
632        ));
633        assert!(matches!(
634            fs_.read_link(p).unwrap_err(),
635            FsError::NotAbsolute { .. }
636        ));
637    }
638
639    #[test]
640    fn each_method_propagates_not_found() {
641        let dir = td();
642        let fs_ = StdFilesystem::new();
643        let p = dir.path().join("ghost");
644        assert!(matches!(
645            fs_.metadata(&p).unwrap_err(),
646            FsError::NotFound { .. }
647        ));
648        assert!(matches!(
649            fs_.symlink_metadata(&p).unwrap_err(),
650            FsError::NotFound { .. }
651        ));
652        assert!(matches!(
653            fs_.read(&p).unwrap_err(),
654            FsError::NotFound { .. }
655        ));
656        assert!(matches!(
657            fs_.read_dir(&p).unwrap_err(),
658            FsError::NotFound { .. }
659        ));
660        assert!(matches!(
661            fs_.canonicalize(&p).unwrap_err(),
662            FsError::NotFound { .. }
663        ));
664        assert!(matches!(
665            fs_.read_link(&p).unwrap_err(),
666            FsError::NotFound { .. }
667        ));
668    }
669
670    #[test]
671    #[cfg(unix)]
672    fn read_on_directory_errors_not_a_file() {
673        let dir = td();
674        let fs_ = StdFilesystem::new();
675        let err = fs_.read(dir.path()).unwrap_err();
676        assert!(
677            matches!(err, FsError::NotAFile { .. }),
678            "expected NotAFile, got {err:?}"
679        );
680    }
681
682    #[test]
683    fn read_dir_on_file_errors_not_a_directory() {
684        let dir = td();
685        let f = dir.path().join("file");
686        fs::write(&f, "").unwrap();
687        let fs_ = StdFilesystem::new();
688        let err = fs_.read_dir(&f).unwrap_err();
689        assert!(
690            matches!(err, FsError::NotADirectory { .. }),
691            "expected NotADirectory, got {err:?}"
692        );
693    }
694
695    #[test]
696    #[cfg(unix)]
697    fn permissions_round_trips_with_set_permissions() {
698        use std::os::unix::fs::PermissionsExt;
699
700        let dir = td();
701        let f = dir.path().join("file");
702        fs::write(&f, b"").unwrap();
703        let fs_ = StdFilesystem::new();
704
705        for mode in [0o600u32, 0o644, 0o755, 0o700] {
706            fs::set_permissions(&f, std::fs::Permissions::from_mode(mode)).unwrap();
707            let got = fs_.permissions(&f).unwrap();
708            assert_eq!(got, mode, "round-trip {mode:o}");
709        }
710    }
711
712    #[test]
713    #[cfg(unix)]
714    fn permissions_follows_symlinks() {
715        use std::os::unix::fs::PermissionsExt;
716
717        let dir = td();
718        let target = dir.path().join("target");
719        fs::write(&target, b"").unwrap();
720        fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
721
722        let link = dir.path().join("link");
723        symlink(&target, &link).unwrap();
724
725        let fs_ = StdFilesystem::new();
726        let mode = fs_.permissions(&link).unwrap();
727        assert_eq!(mode, 0o600);
728    }
729
730    #[test]
731    fn permissions_on_directory_errors_not_a_file() {
732        let dir = td();
733        let fs_ = StdFilesystem::new();
734        let err = fs_.permissions(dir.path()).unwrap_err();
735        assert!(
736            matches!(err, FsError::NotAFile { .. }),
737            "expected NotAFile, got {err:?}",
738        );
739    }
740
741    #[test]
742    fn permissions_on_missing_errors_not_found() {
743        let dir = td();
744        let fs_ = StdFilesystem::new();
745        let err = fs_.permissions(&dir.path().join("ghost")).unwrap_err();
746        assert!(matches!(err, FsError::NotFound { .. }));
747    }
748
749    #[test]
750    fn permissions_rejects_relative_path() {
751        let fs_ = StdFilesystem::new();
752        let err = fs_
753            .permissions(std::path::Path::new("relative"))
754            .unwrap_err();
755        assert!(matches!(err, FsError::NotAbsolute { .. }));
756    }
757
758    #[test]
759    fn read_link_on_directory_errors_not_a_symlink() {
760        let dir = td();
761        let fs_ = StdFilesystem::new();
762        let err = fs_.read_link(dir.path()).unwrap_err();
763        assert!(matches!(err, FsError::NotASymlink { .. }));
764    }
765
766    #[test]
767    fn read_link_returns_relative_target_verbatim() {
768        let dir = td();
769        // Stored target is intentionally relative AND contains a
770        // `..` component; `read_link` MUST return it as-stored
771        // without canonicalising.
772        let target_rel = std::path::PathBuf::from("../foo/bar");
773        let link = dir.path().join("link");
774        symlink(&target_rel, &link).unwrap();
775
776        let fs_ = StdFilesystem::new();
777        let got = fs_.read_link(&link).unwrap();
778        assert_eq!(got, target_rel);
779    }
780
781    #[test]
782    fn broken_symlink_metadata_errors_not_found() {
783        let dir = td();
784        let link = dir.path().join("link");
785        symlink(dir.path().join("missing"), &link).unwrap();
786
787        let fs_ = StdFilesystem::new();
788        let err = fs_.metadata(&link).unwrap_err();
789        assert!(matches!(err, FsError::NotFound { .. }));
790    }
791
792    #[test]
793    fn broken_symlink_symlink_metadata_returns_symlink() {
794        let dir = td();
795        let link = dir.path().join("link");
796        symlink(dir.path().join("missing"), &link).unwrap();
797
798        let fs_ = StdFilesystem::new();
799        let m = fs_.symlink_metadata(&link).unwrap();
800        assert_eq!(m.kind, EntryKind::Symlink);
801    }
802
803    #[test]
804    fn broken_symlink_read_link_returns_target() {
805        let dir = td();
806        let target = dir.path().join("missing");
807        let link = dir.path().join("link");
808        symlink(&target, &link).unwrap();
809
810        let fs_ = StdFilesystem::new();
811        let got = fs_.read_link(&link).unwrap();
812        assert_eq!(got, target);
813    }
814
815    #[test]
816    fn canonicalize_self_loop() {
817        let dir = td();
818        let a = dir.path().join("self_loop");
819        symlink(&a, &a).unwrap();
820
821        let fs_ = StdFilesystem::new();
822        let err = fs_.canonicalize(&a).unwrap_err();
823        assert!(matches!(err, FsError::SymlinkLoop { .. }));
824    }
825
826    #[test]
827    fn canonicalize_resolves_dotdot_in_symlink_target() {
828        let dir = td();
829        let real = dir.path().join("real");
830        fs::create_dir(&real).unwrap();
831        let target = real.join("file");
832        fs::write(&target, "x").unwrap();
833        let link = dir.path().join("link");
834        // Relative target with `..`: real/../real/file from dir,
835        // resolves to dir/real/file.
836        symlink("real/../real/file", &link).unwrap();
837
838        let fs_ = StdFilesystem::new();
839        let canon = fs_.canonicalize(&link).unwrap();
840        assert_eq!(canon.as_path(), fs::canonicalize(&target).unwrap());
841    }
842
843    #[test]
844    fn canonicalize_on_regular_file_returns_resolved_path() {
845        let dir = td();
846        let f = dir.path().join("file");
847        fs::write(&f, "").unwrap();
848
849        let fs_ = StdFilesystem::new();
850        let canon = fs_.canonicalize(&f).unwrap();
851        assert_eq!(canon.as_path(), fs::canonicalize(&f).unwrap());
852    }
853
854    #[cfg(unix)]
855    fn make_fifo(dir: &TempDir, name: &str) -> std::path::PathBuf {
856        let p = dir.path().join(name);
857        let status = std::process::Command::new("mkfifo")
858            .arg(&p)
859            .status()
860            .expect("spawn mkfifo");
861        assert!(status.success(), "mkfifo exited with {status}");
862        p
863    }
864
865    #[test]
866    #[cfg(unix)]
867    fn classifies_socket() {
868        let dir = td();
869        let sock_path = dir.path().join("sock");
870        let _listener =
871            std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
872
873        let fs_ = StdFilesystem::new();
874        let m = fs_.symlink_metadata(&sock_path).unwrap();
875        assert_eq!(m.kind, EntryKind::Socket);
876    }
877
878    #[test]
879    #[cfg(unix)]
880    fn classifies_char_device_via_dev_null() {
881        let dev_null = std::path::Path::new("/dev/null");
882        if !dev_null.exists() {
883            // Sandboxed environment without /dev/null; treat as skip.
884            return;
885        }
886        let fs_ = StdFilesystem::new();
887        let m = fs_.symlink_metadata(dev_null).unwrap();
888        assert_eq!(m.kind, EntryKind::CharDevice);
889    }
890
891    #[test]
892    #[cfg(unix)]
893    fn read_on_fifo_errors_not_a_file() {
894        let dir = td();
895        let fifo = make_fifo(&dir, "pipe");
896
897        let fs_ = StdFilesystem::new();
898        // Must NOT block on the FIFO; the kind pre-check returns
899        // before any open.
900        let err = fs_.read(&fifo).unwrap_err();
901        assert!(
902            matches!(err, FsError::NotAFile { .. }),
903            "expected NotAFile, got {err:?}"
904        );
905    }
906
907    #[test]
908    #[cfg(unix)]
909    fn read_on_socket_errors_not_a_file() {
910        let dir = td();
911        let sock_path = dir.path().join("sock");
912        let _listener =
913            std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
914
915        let fs_ = StdFilesystem::new();
916        let err = fs_.read(&sock_path).unwrap_err();
917        assert!(
918            matches!(err, FsError::NotAFile { .. }),
919            "expected NotAFile, got {err:?}"
920        );
921    }
922
923    #[test]
924    #[cfg(unix)]
925    fn read_on_char_device_errors_not_a_file() {
926        let dev_null = std::path::Path::new("/dev/null");
927        if !dev_null.exists() {
928            return;
929        }
930        let fs_ = StdFilesystem::new();
931        let err = fs_.read(dev_null).unwrap_err();
932        assert!(
933            matches!(err, FsError::NotAFile { .. }),
934            "expected NotAFile, got {err:?}"
935        );
936    }
937
938    #[test]
939    #[cfg(unix)]
940    fn read_dir_on_fifo_errors_not_a_directory() {
941        let dir = td();
942        let fifo = make_fifo(&dir, "pipe");
943
944        let fs_ = StdFilesystem::new();
945        let err = fs_.read_dir(&fifo).unwrap_err();
946        assert!(
947            matches!(err, FsError::NotADirectory { .. }),
948            "expected NotADirectory, got {err:?}"
949        );
950    }
951
952    #[test]
953    #[cfg(unix)]
954    fn read_link_on_fifo_errors_not_a_symlink() {
955        let dir = td();
956        let fifo = make_fifo(&dir, "pipe");
957
958        let fs_ = StdFilesystem::new();
959        let err = fs_.read_link(&fifo).unwrap_err();
960        assert!(
961            matches!(err, FsError::NotASymlink { .. }),
962            "expected NotASymlink, got {err:?}"
963        );
964    }
965
966    #[test]
967    #[cfg(unix)]
968    fn canonicalize_on_socket_returns_path() {
969        let dir = td();
970        let sock_path = dir.path().join("sock");
971        let _listener =
972            std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
973
974        let fs_ = StdFilesystem::new();
975        let canon = fs_.canonicalize(&sock_path).unwrap();
976        assert_eq!(canon.as_path(), fs::canonicalize(&sock_path).unwrap());
977    }
978
979    #[test]
980    fn read_on_broken_symlink_errors_not_found() {
981        let dir = td();
982        let link = dir.path().join("link");
983        symlink(dir.path().join("missing"), &link).unwrap();
984
985        let fs_ = StdFilesystem::new();
986        let err = fs_.read(&link).unwrap_err();
987        assert!(matches!(err, FsError::NotFound { .. }));
988    }
989
990    #[test]
991    fn canonicalize_on_broken_symlink_errors_not_found() {
992        let dir = td();
993        let link = dir.path().join("link");
994        symlink(dir.path().join("missing"), &link).unwrap();
995
996        let fs_ = StdFilesystem::new();
997        let err = fs_.canonicalize(&link).unwrap_err();
998        assert!(matches!(err, FsError::NotFound { .. }));
999    }
1000
1001    #[test]
1002    fn read_on_symlink_to_directory_errors_not_a_file() {
1003        let dir = td();
1004        let real = dir.path().join("real");
1005        fs::create_dir(&real).unwrap();
1006        let link = dir.path().join("link_to_d");
1007        symlink(&real, &link).unwrap();
1008
1009        let fs_ = StdFilesystem::new();
1010        let err = fs_.read(&link).unwrap_err();
1011        assert!(matches!(err, FsError::NotAFile { .. }));
1012    }
1013
1014    #[test]
1015    #[cfg(unix)]
1016    fn read_dir_on_socket_errors_not_a_directory() {
1017        let dir = td();
1018        let sock_path = dir.path().join("sock");
1019        let _listener =
1020            std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
1021
1022        let fs_ = StdFilesystem::new();
1023        let err = fs_.read_dir(&sock_path).unwrap_err();
1024        assert!(matches!(err, FsError::NotADirectory { .. }));
1025    }
1026
1027    #[test]
1028    #[cfg(unix)]
1029    fn read_dir_on_char_device_errors_not_a_directory() {
1030        let dev_null = std::path::Path::new("/dev/null");
1031        if !dev_null.exists() {
1032            return;
1033        }
1034        let fs_ = StdFilesystem::new();
1035        let err = fs_.read_dir(dev_null).unwrap_err();
1036        assert!(matches!(err, FsError::NotADirectory { .. }));
1037    }
1038
1039    #[test]
1040    #[cfg(unix)]
1041    fn read_link_on_socket_errors_not_a_symlink() {
1042        let dir = td();
1043        let sock_path = dir.path().join("sock");
1044        let _listener =
1045            std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
1046
1047        let fs_ = StdFilesystem::new();
1048        let err = fs_.read_link(&sock_path).unwrap_err();
1049        assert!(matches!(err, FsError::NotASymlink { .. }));
1050    }
1051
1052    #[test]
1053    #[cfg(unix)]
1054    fn read_link_on_char_device_errors_not_a_symlink() {
1055        let dev_null = std::path::Path::new("/dev/null");
1056        if !dev_null.exists() {
1057            return;
1058        }
1059        let fs_ = StdFilesystem::new();
1060        let err = fs_.read_link(dev_null).unwrap_err();
1061        assert!(matches!(err, FsError::NotASymlink { .. }));
1062    }
1063
1064    #[test]
1065    #[cfg(unix)]
1066    fn canonicalize_on_fifo_returns_path() {
1067        let dir = td();
1068        let fifo = make_fifo(&dir, "pipe");
1069
1070        let fs_ = StdFilesystem::new();
1071        let canon = fs_.canonicalize(&fifo).unwrap();
1072        assert_eq!(canon.as_path(), fs::canonicalize(&fifo).unwrap());
1073    }
1074
1075    #[test]
1076    #[cfg(unix)]
1077    fn canonicalize_on_char_device_returns_path() {
1078        let dev_null = std::path::Path::new("/dev/null");
1079        if !dev_null.exists() {
1080            return;
1081        }
1082        let fs_ = StdFilesystem::new();
1083        let canon = fs_.canonicalize(dev_null).unwrap();
1084        assert_eq!(canon.as_path(), fs::canonicalize(dev_null).unwrap());
1085    }
1086
1087    #[test]
1088    fn multi_hop_symlink_chain_resolves() {
1089        let dir = td();
1090        let target = dir.path().join("target");
1091        fs::write(&target, b"payload").unwrap();
1092        let a = dir.path().join("a");
1093        let b = dir.path().join("b");
1094        let c = dir.path().join("c");
1095        symlink(&target, &a).unwrap();
1096        symlink(&a, &b).unwrap();
1097        symlink(&b, &c).unwrap();
1098
1099        let fs_ = StdFilesystem::new();
1100        // metadata follows the whole chain.
1101        assert_eq!(fs_.metadata(&c).unwrap().kind, EntryKind::File);
1102        // read returns the terminal file's bytes.
1103        assert_eq!(fs_.read(&c).unwrap(), b"payload");
1104        // canonicalize resolves to the terminal path.
1105        assert_eq!(
1106            fs_.canonicalize(&c).unwrap().as_path(),
1107            fs::canonicalize(&target).unwrap()
1108        );
1109        // read_link returns the IMMEDIATE target verbatim.
1110        assert_eq!(fs_.read_link(&c).unwrap(), b);
1111        // symlink_metadata on the head of the chain reports Symlink.
1112        assert_eq!(fs_.symlink_metadata(&c).unwrap().kind, EntryKind::Symlink);
1113    }
1114
1115    #[test]
1116    fn metadata_through_intermediate_symlink() {
1117        let dir = td();
1118        let real = dir.path().join("real");
1119        fs::create_dir(&real).unwrap();
1120        fs::write(real.join("data"), b"x").unwrap();
1121        let link = dir.path().join("link");
1122        symlink(&real, &link).unwrap();
1123
1124        let fs_ = StdFilesystem::new();
1125        let m = fs_.metadata(&link.join("data")).unwrap();
1126        assert_eq!(m.kind, EntryKind::File);
1127    }
1128
1129    #[test]
1130    fn read_through_intermediate_symlink() {
1131        let dir = td();
1132        let real = dir.path().join("real");
1133        fs::create_dir(&real).unwrap();
1134        fs::write(real.join("data"), b"hello").unwrap();
1135        let link = dir.path().join("link");
1136        symlink(&real, &link).unwrap();
1137
1138        let fs_ = StdFilesystem::new();
1139        assert_eq!(fs_.read(&link.join("data")).unwrap(), b"hello");
1140    }
1141
1142    #[test]
1143    fn read_dir_through_intermediate_symlink() {
1144        let dir = td();
1145        let real = dir.path().join("real");
1146        fs::create_dir(&real).unwrap();
1147        fs::write(real.join("a"), b"").unwrap();
1148        fs::write(real.join("b"), b"").unwrap();
1149        let link = dir.path().join("link");
1150        symlink(&real, &link).unwrap();
1151
1152        let fs_ = StdFilesystem::new();
1153        let entries = fs_.read_dir(&link).unwrap();
1154        assert_eq!(entries.len(), 2);
1155    }
1156
1157    #[test]
1158    fn read_on_empty_file_returns_empty_bytes() {
1159        let dir = td();
1160        let f = dir.path().join("empty");
1161        fs::write(&f, b"").unwrap();
1162
1163        let fs_ = StdFilesystem::new();
1164        assert_eq!(fs_.read(&f).unwrap(), b"");
1165    }
1166
1167    // ----- FsMetadata.size -----
1168
1169    #[test]
1170    fn metadata_size_matches_file_byte_length() {
1171        let dir = td();
1172        let f = dir.path().join("data");
1173        fs::write(&f, vec![0u8; 1024]).unwrap();
1174        let fs_ = StdFilesystem::new();
1175        assert_eq!(fs_.metadata(&f).unwrap().size, 1024);
1176    }
1177
1178    #[test]
1179    fn metadata_size_zero_for_empty_file() {
1180        let dir = td();
1181        let f = dir.path().join("empty");
1182        fs::write(&f, b"").unwrap();
1183        let fs_ = StdFilesystem::new();
1184        assert_eq!(fs_.metadata(&f).unwrap().size, 0);
1185    }
1186
1187    #[test]
1188    fn read_dir_entries_carry_file_sizes() {
1189        let dir = td();
1190        fs::write(dir.path().join("a"), vec![0u8; 7]).unwrap();
1191        fs::write(dir.path().join("b"), vec![0u8; 100]).unwrap();
1192        let fs_ = StdFilesystem::new();
1193        let entries = fs_.read_dir(dir.path()).unwrap();
1194        let by_name: std::collections::BTreeMap<_, _> = entries
1195            .into_iter()
1196            .map(|e| (e.path.file_name().unwrap().to_owned(), e.metadata.size))
1197            .collect();
1198        assert_eq!(by_name[std::ffi::OsStr::new("a")], 7);
1199        assert_eq!(by_name[std::ffi::OsStr::new("b")], 100);
1200    }
1201
1202    // ----- WritableFilesystem -----
1203
1204    mod writable {
1205        use std::fs;
1206
1207        use tempfile::TempDir;
1208
1209        use crate::std_impl::StdFilesystem;
1210        use crate::traits::{EntryKind, Filesystem, FsError, WritableFilesystem};
1211
1212        fn td() -> TempDir {
1213            tempfile::tempdir().expect("create tempdir")
1214        }
1215
1216        #[test]
1217        fn create_dir_all_creates_chain() {
1218            let dir = td();
1219            let target = dir.path().join("a/b/c");
1220            let fs_ = StdFilesystem::new();
1221            fs_.create_dir_all(&target).unwrap();
1222            assert_eq!(fs_.metadata(&target).unwrap().kind, EntryKind::Dir);
1223        }
1224
1225        #[test]
1226        fn create_dir_all_is_idempotent() {
1227            let dir = td();
1228            let target = dir.path().join("a/b");
1229            let fs_ = StdFilesystem::new();
1230            fs_.create_dir_all(&target).unwrap();
1231            fs_.create_dir_all(&target).unwrap();
1232            assert_eq!(fs_.metadata(&target).unwrap().kind, EntryKind::Dir);
1233        }
1234
1235        #[test]
1236        fn create_dir_all_rejects_relative() {
1237            let fs_ = StdFilesystem::new();
1238            let err = fs_
1239                .create_dir_all(std::path::Path::new("relative"))
1240                .unwrap_err();
1241            assert!(matches!(err, FsError::NotAbsolute { .. }));
1242        }
1243
1244        #[test]
1245        fn write_file_creates_new_file() {
1246            let dir = td();
1247            let f = dir.path().join("new.txt");
1248            let fs_ = StdFilesystem::new();
1249            fs_.write_file(&f, b"hello").unwrap();
1250            assert_eq!(fs::read(&f).unwrap(), b"hello");
1251        }
1252
1253        #[test]
1254        fn write_file_overwrites_existing() {
1255            let dir = td();
1256            let f = dir.path().join("existing.txt");
1257            fs::write(&f, b"old").unwrap();
1258            let fs_ = StdFilesystem::new();
1259            fs_.write_file(&f, b"new").unwrap();
1260            assert_eq!(fs::read(&f).unwrap(), b"new");
1261        }
1262
1263        #[test]
1264        fn write_file_rejects_relative() {
1265            let fs_ = StdFilesystem::new();
1266            let err = fs_
1267                .write_file(std::path::Path::new("relative"), b"")
1268                .unwrap_err();
1269            assert!(matches!(err, FsError::NotAbsolute { .. }));
1270        }
1271
1272        #[test]
1273        fn write_file_rejects_missing_parent() {
1274            let dir = td();
1275            let f = dir.path().join("missing_parent/file");
1276            let fs_ = StdFilesystem::new();
1277            let err = fs_.write_file(&f, b"").unwrap_err();
1278            assert!(matches!(err, FsError::NotFound { .. }));
1279        }
1280
1281        #[test]
1282        fn rename_moves_file() {
1283            let dir = td();
1284            let from = dir.path().join("from");
1285            let to = dir.path().join("to");
1286            fs::write(&from, b"payload").unwrap();
1287            let fs_ = StdFilesystem::new();
1288            fs_.rename(&from, &to).unwrap();
1289            assert_eq!(fs::read(&to).unwrap(), b"payload");
1290            assert!(!from.exists());
1291        }
1292
1293        #[test]
1294        fn rename_moves_directory() {
1295            let dir = td();
1296            let src = dir.path().join("src");
1297            let dst = dir.path().join("dst");
1298            fs::create_dir(&src).unwrap();
1299            fs::write(src.join("a"), b"a").unwrap();
1300            let fs_ = StdFilesystem::new();
1301            fs_.rename(&src, &dst).unwrap();
1302            assert_eq!(fs::read(dst.join("a")).unwrap(), b"a");
1303            assert!(!src.exists());
1304        }
1305
1306        #[test]
1307        fn rename_missing_source_errors_not_found() {
1308            let dir = td();
1309            let fs_ = StdFilesystem::new();
1310            let err = fs_
1311                .rename(&dir.path().join("ghost"), &dir.path().join("to"))
1312                .unwrap_err();
1313            assert!(matches!(err, FsError::NotFound { .. }));
1314        }
1315
1316        #[test]
1317        fn rename_rejects_relative_paths() {
1318            let dir = td();
1319            let fs_ = StdFilesystem::new();
1320            let err = fs_
1321                .rename(std::path::Path::new("relative"), &dir.path().join("to"))
1322                .unwrap_err();
1323            assert!(matches!(err, FsError::NotAbsolute { .. }));
1324            let err = fs_
1325                .rename(&dir.path().join("from"), std::path::Path::new("relative"))
1326                .unwrap_err();
1327            assert!(matches!(err, FsError::NotAbsolute { .. }));
1328        }
1329
1330        #[test]
1331        fn remove_dir_all_drops_subtree() {
1332            let dir = td();
1333            let sub = dir.path().join("sub");
1334            fs::create_dir(&sub).unwrap();
1335            fs::write(sub.join("a"), b"").unwrap();
1336            fs::create_dir(sub.join("nested")).unwrap();
1337            fs::write(sub.join("nested/b"), b"").unwrap();
1338            let fs_ = StdFilesystem::new();
1339            fs_.remove_dir_all(&sub).unwrap();
1340            assert!(!sub.exists());
1341        }
1342
1343        #[test]
1344        fn remove_dir_all_on_missing_errors_not_found() {
1345            let dir = td();
1346            let fs_ = StdFilesystem::new();
1347            let err = fs_.remove_dir_all(&dir.path().join("ghost")).unwrap_err();
1348            assert!(matches!(err, FsError::NotFound { .. }));
1349        }
1350
1351        #[test]
1352        #[cfg(unix)]
1353        fn set_permissions_changes_mode() {
1354            use std::os::unix::fs::PermissionsExt;
1355            let dir = td();
1356            let f = dir.path().join("file");
1357            fs::write(&f, b"").unwrap();
1358            let fs_ = StdFilesystem::new();
1359            fs_.set_permissions(&f, 0o755).unwrap();
1360            let mode = fs::metadata(&f).unwrap().permissions().mode() & 0o777;
1361            assert_eq!(mode, 0o755);
1362        }
1363
1364        #[test]
1365        fn set_permissions_on_missing_errors_not_found() {
1366            let dir = td();
1367            let fs_ = StdFilesystem::new();
1368            let err = fs_
1369                .set_permissions(&dir.path().join("ghost"), 0o644)
1370                .unwrap_err();
1371            assert!(matches!(err, FsError::NotFound { .. }));
1372        }
1373
1374        #[test]
1375        fn fsync_file_succeeds_on_real_file() {
1376            let dir = td();
1377            let f = dir.path().join("f");
1378            fs::write(&f, b"").unwrap();
1379            let fs_ = StdFilesystem::new();
1380            fs_.fsync_file(&f).unwrap();
1381        }
1382
1383        #[test]
1384        fn fsync_file_on_missing_errors_not_found() {
1385            let dir = td();
1386            let fs_ = StdFilesystem::new();
1387            let err = fs_.fsync_file(&dir.path().join("ghost")).unwrap_err();
1388            assert!(matches!(err, FsError::NotFound { .. }));
1389        }
1390
1391        #[test]
1392        #[cfg(unix)]
1393        fn fsync_dir_succeeds_on_real_directory() {
1394            let dir = td();
1395            let fs_ = StdFilesystem::new();
1396            fs_.fsync_dir(dir.path()).unwrap();
1397        }
1398
1399        #[test]
1400        fn fsync_dir_on_missing_errors_not_found() {
1401            let dir = td();
1402            let fs_ = StdFilesystem::new();
1403            let err = fs_.fsync_dir(&dir.path().join("ghost")).unwrap_err();
1404            assert!(matches!(err, FsError::NotFound { .. }));
1405        }
1406
1407        #[test]
1408        fn two_phase_store_pattern_round_trips() {
1409            // Mirror of the in-memory test, against the real
1410            // filesystem: write into a `.tmp-` dir, fsync, rename
1411            // into place, fsync the parent.
1412            let dir = td();
1413            let shard = dir.path().join("shard");
1414            let tmp = shard.join(".tmp-abc");
1415            let final_entry = shard.join("abc");
1416
1417            let fs_ = StdFilesystem::new();
1418            fs_.create_dir_all(&tmp.join("outputs")).unwrap();
1419            fs_.write_file(&tmp.join("stdout"), b"out").unwrap();
1420            fs_.write_file(&tmp.join("stderr"), b"err").unwrap();
1421            fs_.write_file(&tmp.join("outputs/deadbeef"), b"blob")
1422                .unwrap();
1423            fs_.fsync_file(&tmp.join("stdout")).unwrap();
1424            fs_.fsync_file(&tmp.join("stderr")).unwrap();
1425            fs_.fsync_file(&tmp.join("outputs/deadbeef")).unwrap();
1426            fs_.write_file(&tmp.join("manifest.json"), b"{}").unwrap();
1427            fs_.fsync_file(&tmp.join("manifest.json")).unwrap();
1428            fs_.rename(&tmp, &final_entry).unwrap();
1429            fs_.fsync_dir(&shard).unwrap();
1430
1431            assert_eq!(fs::read(final_entry.join("stdout")).unwrap(), b"out");
1432            assert_eq!(fs::read(final_entry.join("manifest.json")).unwrap(), b"{}");
1433            assert!(!tmp.exists());
1434        }
1435
1436        #[test]
1437        fn crash_before_rename_leaves_entry_invisible() {
1438            let dir = td();
1439            let shard = dir.path().join("shard");
1440            let tmp = shard.join(".tmp-abc");
1441            let final_entry = shard.join("abc");
1442
1443            let fs_ = StdFilesystem::new();
1444            fs_.create_dir_all(&tmp).unwrap();
1445            fs_.write_file(&tmp.join("manifest.json"), b"{}").unwrap();
1446            // No rename; the entry must never become visible.
1447            assert!(!final_entry.exists());
1448        }
1449    }
1450}