1use std::path::{Path, PathBuf};
4
5use crate::traits::{DirEntry, EntryKind, Filesystem, FsError, FsMetadata, WritableFilesystem};
6
7#[derive(Debug, Default, Clone, Copy)]
34pub struct StdFilesystem;
35
36impl StdFilesystem {
37 #[must_use]
39 pub const fn new() -> Self {
40 Self
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct StdCanonicalPath(PathBuf);
58
59impl StdCanonicalPath {
60 #[must_use]
62 pub fn as_path(&self) -> &Path {
63 &self.0
64 }
65
66 #[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#[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 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 false
142}
143
144#[cfg(windows)]
145fn is_not_a_directory(e: &std::io::Error) -> bool {
146 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 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 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 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 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 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 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 m.permissions().mode() & 0o7777
413}
414
415#[cfg(not(unix))]
416fn permissions_impl(m: &std::fs::Metadata) -> u32 {
417 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 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 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 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 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 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 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 assert_eq!(fs_.metadata(&c).unwrap().kind, EntryKind::File);
1102 assert_eq!(fs_.read(&c).unwrap(), b"payload");
1104 assert_eq!(
1106 fs_.canonicalize(&c).unwrap().as_path(),
1107 fs::canonicalize(&target).unwrap()
1108 );
1109 assert_eq!(fs_.read_link(&c).unwrap(), b);
1111 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 #[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 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 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 assert!(!final_entry.exists());
1448 }
1449 }
1450}