Skip to main content

sys_traits/
ctx.rs

1//! Error context wrapper for sys_traits operations.
2//!
3//! This module provides [`SysWithPathsInErrors`], a wrapper that adds operation and path
4//! context to errors returned by sys_traits methods.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use sys_traits::PathsInErrorsExt;
10//! # #[cfg(feature = "real")]
11//! use sys_traits::impls::RealSys;
12//!
13//! # #[cfg(feature = "real")]
14//! # fn example() -> std::io::Result<()> {
15//! let sys = RealSys;
16//!
17//! // Without context:
18//! // sys.fs_read("/path/to/file")?;
19//! // Error: No such file or directory (os error 2)
20//!
21//! // With context:
22//! sys.with_paths_in_errors().fs_read("/path/to/file")?;
23//! // Error: failed to read '/path/to/file': No such file or directory (os error 2)
24//! # Ok(())
25//! # }
26//! ```
27
28use std::borrow::Cow;
29use std::error::Error;
30use std::fmt;
31use std::io;
32use std::path::Path;
33use std::path::PathBuf;
34use std::time::SystemTime;
35
36use crate::BaseFsCanonicalize;
37use crate::BaseFsChown;
38use crate::BaseFsCloneFile;
39use crate::BaseFsCopy;
40use crate::BaseFsCreateDir;
41use crate::BaseFsCreateJunction;
42use crate::BaseFsHardLink;
43use crate::BaseFsMetadata;
44use crate::BaseFsOpen;
45use crate::BaseFsRead;
46use crate::BaseFsReadDir;
47use crate::BaseFsReadLink;
48use crate::BaseFsRemoveDir;
49use crate::BaseFsRemoveDirAll;
50use crate::BaseFsRemoveFile;
51use crate::BaseFsRename;
52use crate::BaseFsSetFileTimes;
53use crate::BaseFsSetPermissions;
54use crate::BaseFsSetSymlinkFileTimes;
55use crate::BaseFsSymlinkChown;
56use crate::BaseFsSymlinkDir;
57use crate::BaseFsSymlinkFile;
58use crate::BaseFsWrite;
59use crate::CreateDirOptions;
60use crate::FileType;
61use crate::FsFile;
62use crate::FsFileAsRaw;
63use crate::FsFileIsTerminal;
64use crate::FsFileLock;
65use crate::FsFileLockMode;
66use crate::FsFileMetadata;
67use crate::FsFileSetLen;
68use crate::FsFileSetPermissions;
69use crate::FsFileSetTimes;
70use crate::FsFileSyncAll;
71use crate::FsFileSyncData;
72use crate::FsFileTimes;
73use crate::FsMetadata;
74use crate::FsMetadataValue;
75use crate::FsRead;
76use crate::OpenOptions;
77
78use crate::boxed::BoxedFsFile;
79use crate::boxed::BoxedFsMetadataValue;
80use crate::boxed::FsOpenBoxed;
81
82/// An error that includes context about the operation that failed.
83#[derive(Debug)]
84pub struct OperationError {
85  operation: &'static str,
86  kind: OperationErrorKind,
87  /// The underlying I/O error.
88  pub err: io::Error,
89}
90
91impl OperationError {
92  /// Returns the operation name (e.g., "read", "write", "copy").
93  pub fn operation(&self) -> &'static str {
94    self.operation
95  }
96
97  /// Returns the error context kind.
98  pub fn kind(&self) -> &OperationErrorKind {
99    &self.kind
100  }
101}
102
103impl fmt::Display for OperationError {
104  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105    write!(f, "failed to {}", self.operation)?;
106    match &self.kind {
107      OperationErrorKind::WithPath(path) => write!(f, " '{}'", path)?,
108      OperationErrorKind::WithTwoPaths(from, to) => {
109        write!(f, " '{}' to '{}'", from, to)?
110      }
111    }
112    write!(f, ": {}", self.err)
113  }
114}
115
116impl Error for OperationError {}
117
118/// The kind of context associated with an operation error.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum OperationErrorKind {
121  /// Single path context.
122  WithPath(String),
123  /// Two path context (e.g., copy, rename).
124  WithTwoPaths(String, String),
125}
126
127/// A wrapper that adds error context to sys_traits operations.
128///
129/// Use [`PathsInErrorsExt::with_paths_in_errors`] to create an instance.
130#[derive(Debug)]
131pub struct SysWithPathsInErrors<'a, T: ?Sized>(pub &'a T);
132
133// These implementations of Clone and Copy are needed in order to get this
134// working when `T` does not implement `Clone` or `Copy`
135impl<T: ?Sized> Copy for SysWithPathsInErrors<'_, T> {}
136
137impl<T: ?Sized> Clone for SysWithPathsInErrors<'_, T> {
138  fn clone(&self) -> Self {
139    *self
140  }
141}
142
143impl<'a, T: ?Sized> SysWithPathsInErrors<'a, T> {
144  /// Creates a new `SysWithPathsInErrors` wrapper.
145  pub fn new(inner: &'a T) -> Self {
146    Self(inner)
147  }
148
149  /// Returns a reference to the inner value.
150  #[allow(clippy::should_implement_trait)]
151  pub fn as_ref(&self) -> &T {
152    // WARNING: Do not implement deref or anything like that on this struct
153    // because we do not want to accidentally have this being able to be passed
154    // into functions for a trait. That would lead to the error being wrapped
155    // multiple times.
156    self.0
157  }
158}
159
160/// Extension trait that provides the [`with_paths_in_errors`](PathsInErrorsExt::with_paths_in_errors) method.
161///
162/// Import this trait to use `.with_paths_in_errors()` on any type.
163pub trait PathsInErrorsExt {
164  /// Wraps `self` in a [`SysWithPathsInErrors`] that includes paths in error messages.
165  fn with_paths_in_errors(&self) -> SysWithPathsInErrors<'_, Self> {
166    SysWithPathsInErrors(self)
167  }
168}
169
170impl<T: ?Sized> PathsInErrorsExt for T {}
171
172/// A file wrapper that includes the path in error messages.
173///
174/// Returned by [`SysWithPathsInErrors::fs_open`].
175#[derive(Debug)]
176pub struct FsFileWithPathsInErrors<F> {
177  file: F,
178  path: PathBuf,
179}
180
181impl<F> FsFileWithPathsInErrors<F> {
182  /// Creates a new file wrapper with path context.
183  pub fn new(file: F, path: PathBuf) -> Self {
184    Self { file, path }
185  }
186
187  /// Returns a reference to the path.
188  pub fn path(&self) -> &Path {
189    &self.path
190  }
191
192  /// Returns a reference to the inner file.
193  pub fn inner(&self) -> &F {
194    &self.file
195  }
196
197  /// Returns a mutable reference to the inner file.
198  pub fn inner_mut(&mut self) -> &mut F {
199    &mut self.file
200  }
201
202  /// Consumes the wrapper and returns the inner file.
203  pub fn into_inner(self) -> F {
204    self.file
205  }
206
207  fn wrap_err(&self, operation: &'static str, err: io::Error) -> io::Error {
208    err_with_path(operation, &self.path, err)
209  }
210}
211
212impl<F: io::Read> io::Read for FsFileWithPathsInErrors<F> {
213  fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
214    self.file.read(buf).map_err(|e| self.wrap_err("read", e))
215  }
216}
217
218impl<F: io::Write> io::Write for FsFileWithPathsInErrors<F> {
219  fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
220    self.file.write(buf).map_err(|e| self.wrap_err("write", e))
221  }
222
223  fn flush(&mut self) -> io::Result<()> {
224    self.file.flush().map_err(|e| self.wrap_err("flush", e))
225  }
226}
227
228impl<F: io::Seek> io::Seek for FsFileWithPathsInErrors<F> {
229  fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
230    self.file.seek(pos).map_err(|e| self.wrap_err("seek", e))
231  }
232}
233
234impl<F: FsFileIsTerminal> FsFileIsTerminal for FsFileWithPathsInErrors<F> {
235  fn fs_file_is_terminal(&self) -> bool {
236    self.file.fs_file_is_terminal()
237  }
238}
239
240impl<F: FsFileLock> FsFileLock for FsFileWithPathsInErrors<F> {
241  fn fs_file_lock(&mut self, mode: FsFileLockMode) -> io::Result<()> {
242    self
243      .file
244      .fs_file_lock(mode)
245      .map_err(|e| self.wrap_err("lock", e))
246  }
247
248  fn fs_file_try_lock(&mut self, mode: FsFileLockMode) -> io::Result<()> {
249    self
250      .file
251      .fs_file_try_lock(mode)
252      .map_err(|e| self.wrap_err("try lock", e))
253  }
254
255  fn fs_file_unlock(&mut self) -> io::Result<()> {
256    self
257      .file
258      .fs_file_unlock()
259      .map_err(|e| self.wrap_err("unlock", e))
260  }
261}
262
263impl<F: FsFileMetadata> FsFileMetadata for FsFileWithPathsInErrors<F> {
264  fn fs_file_metadata(&self) -> io::Result<BoxedFsMetadataValue> {
265    self
266      .file
267      .fs_file_metadata()
268      .map_err(|e| self.wrap_err("stat", e))
269  }
270}
271
272impl<F: FsFileSetPermissions> FsFileSetPermissions
273  for FsFileWithPathsInErrors<F>
274{
275  fn fs_file_set_permissions(&mut self, mode: u32) -> io::Result<()> {
276    self
277      .file
278      .fs_file_set_permissions(mode)
279      .map_err(|e| self.wrap_err("set permissions", e))
280  }
281}
282
283impl<F: FsFileSetTimes> FsFileSetTimes for FsFileWithPathsInErrors<F> {
284  fn fs_file_set_times(&mut self, times: FsFileTimes) -> io::Result<()> {
285    self
286      .file
287      .fs_file_set_times(times)
288      .map_err(|e| self.wrap_err("set file times", e))
289  }
290}
291
292impl<F: FsFileSetLen> FsFileSetLen for FsFileWithPathsInErrors<F> {
293  fn fs_file_set_len(&mut self, size: u64) -> io::Result<()> {
294    self
295      .file
296      .fs_file_set_len(size)
297      .map_err(|e| self.wrap_err("truncate", e))
298  }
299}
300
301impl<F: FsFileSyncAll> FsFileSyncAll for FsFileWithPathsInErrors<F> {
302  fn fs_file_sync_all(&mut self) -> io::Result<()> {
303    self
304      .file
305      .fs_file_sync_all()
306      .map_err(|e| self.wrap_err("sync", e))
307  }
308}
309
310impl<F: FsFileSyncData> FsFileSyncData for FsFileWithPathsInErrors<F> {
311  fn fs_file_sync_data(&mut self) -> io::Result<()> {
312    self
313      .file
314      .fs_file_sync_data()
315      .map_err(|e| self.wrap_err("sync data", e))
316  }
317}
318
319impl<F: FsFileAsRaw> FsFileAsRaw for FsFileWithPathsInErrors<F> {
320  #[cfg(windows)]
321  fn fs_file_as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
322    self.file.fs_file_as_raw_handle()
323  }
324
325  #[cfg(unix)]
326  fn fs_file_as_raw_fd(&self) -> Option<std::os::fd::RawFd> {
327    self.file.fs_file_as_raw_fd()
328  }
329}
330
331impl<F: FsFile> FsFile for FsFileWithPathsInErrors<F> {}
332
333// helper to create single-path errors wrapped in io::Error
334fn err_with_path(
335  operation: &'static str,
336  path: &Path,
337  err: io::Error,
338) -> io::Error {
339  io::Error::new(
340    err.kind(),
341    OperationError {
342      operation,
343      kind: OperationErrorKind::WithPath(path.to_string_lossy().into_owned()),
344      err,
345    },
346  )
347}
348
349// helper to create two-path errors wrapped in io::Error
350fn err_with_two_paths(
351  operation: &'static str,
352  from: &Path,
353  to: &Path,
354  err: io::Error,
355) -> io::Error {
356  io::Error::new(
357    err.kind(),
358    OperationError {
359      operation,
360      kind: OperationErrorKind::WithTwoPaths(
361        from.to_string_lossy().into_owned(),
362        to.to_string_lossy().into_owned(),
363      ),
364      err,
365    },
366  )
367}
368
369// == FsCanonicalize ==
370
371impl<T: BaseFsCanonicalize> SysWithPathsInErrors<'_, T> {
372  pub fn fs_canonicalize(&self, path: impl AsRef<Path>) -> io::Result<PathBuf> {
373    let path = path.as_ref();
374    self
375      .0
376      .base_fs_canonicalize(path)
377      .map_err(|e| err_with_path("canonicalize", path, e))
378  }
379}
380
381// == FsChown ==
382
383impl<T: BaseFsChown> SysWithPathsInErrors<'_, T> {
384  pub fn fs_chown(
385    &self,
386    path: impl AsRef<Path>,
387    uid: Option<u32>,
388    gid: Option<u32>,
389  ) -> io::Result<()> {
390    let path = path.as_ref();
391    self
392      .0
393      .base_fs_chown(path, uid, gid)
394      .map_err(|e| err_with_path("chown", path, e))
395  }
396}
397
398// == FsSymlinkChown ==
399
400impl<T: BaseFsSymlinkChown> SysWithPathsInErrors<'_, T> {
401  pub fn fs_symlink_chown(
402    &self,
403    path: impl AsRef<Path>,
404    uid: Option<u32>,
405    gid: Option<u32>,
406  ) -> io::Result<()> {
407    let path = path.as_ref();
408    self
409      .0
410      .base_fs_symlink_chown(path, uid, gid)
411      .map_err(|e| err_with_path("chown symlink", path, e))
412  }
413}
414
415// == FsCloneFile ==
416
417impl<T: BaseFsCloneFile> SysWithPathsInErrors<'_, T> {
418  pub fn fs_clone_file(
419    &self,
420    from: impl AsRef<Path>,
421    to: impl AsRef<Path>,
422  ) -> io::Result<()> {
423    let from = from.as_ref();
424    let to = to.as_ref();
425    self
426      .0
427      .base_fs_clone_file(from, to)
428      .map_err(|e| err_with_two_paths("clone", from, to, e))
429  }
430}
431
432// == FsCopy ==
433
434impl<T: BaseFsCopy> SysWithPathsInErrors<'_, T> {
435  pub fn fs_copy(
436    &self,
437    from: impl AsRef<Path>,
438    to: impl AsRef<Path>,
439  ) -> io::Result<u64> {
440    let from = from.as_ref();
441    let to = to.as_ref();
442    self
443      .0
444      .base_fs_copy(from, to)
445      .map_err(|e| err_with_two_paths("copy", from, to, e))
446  }
447}
448
449// == FsCreateDir ==
450
451impl<T: BaseFsCreateDir> SysWithPathsInErrors<'_, T> {
452  pub fn fs_create_dir(
453    &self,
454    path: impl AsRef<Path>,
455    options: &CreateDirOptions,
456  ) -> io::Result<()> {
457    let path = path.as_ref();
458    self
459      .0
460      .base_fs_create_dir(path, options)
461      .map_err(|e| err_with_path("create directory", path, e))
462  }
463
464  pub fn fs_create_dir_all(&self, path: impl AsRef<Path>) -> io::Result<()> {
465    let path = path.as_ref();
466    self
467      .0
468      .base_fs_create_dir(
469        path,
470        &CreateDirOptions {
471          recursive: true,
472          mode: None,
473        },
474      )
475      .map_err(|e| err_with_path("create directory", path, e))
476  }
477}
478
479// == FsHardLink ==
480
481impl<T: BaseFsHardLink> SysWithPathsInErrors<'_, T> {
482  pub fn fs_hard_link(
483    &self,
484    src: impl AsRef<Path>,
485    dst: impl AsRef<Path>,
486  ) -> io::Result<()> {
487    let src = src.as_ref();
488    let dst = dst.as_ref();
489    self
490      .0
491      .base_fs_hard_link(src, dst)
492      .map_err(|e| err_with_two_paths("hard link", src, dst, e))
493  }
494}
495
496// == FsCreateJunction ==
497
498impl<T: BaseFsCreateJunction> SysWithPathsInErrors<'_, T> {
499  pub fn fs_create_junction(
500    &self,
501    original: impl AsRef<Path>,
502    junction: impl AsRef<Path>,
503  ) -> io::Result<()> {
504    let original = original.as_ref();
505    let junction = junction.as_ref();
506    self
507      .0
508      .base_fs_create_junction(original, junction)
509      .map_err(|e| err_with_two_paths("create junction", original, junction, e))
510  }
511}
512
513// == FsMetadata ==
514
515impl<T: BaseFsMetadata> SysWithPathsInErrors<'_, T> {
516  pub fn fs_metadata(&self, path: impl AsRef<Path>) -> io::Result<T::Metadata> {
517    let path = path.as_ref();
518    self
519      .0
520      .base_fs_metadata(path)
521      .map_err(|e| err_with_path("stat", path, e))
522  }
523
524  pub fn fs_symlink_metadata(
525    &self,
526    path: impl AsRef<Path>,
527  ) -> io::Result<T::Metadata> {
528    let path = path.as_ref();
529    self
530      .0
531      .base_fs_symlink_metadata(path)
532      .map_err(|e| err_with_path("lstat", path, e))
533  }
534
535  pub fn fs_is_file(&self, path: impl AsRef<Path>) -> io::Result<bool> {
536    Ok(self.fs_metadata(path)?.file_type() == FileType::File)
537  }
538
539  pub fn fs_is_dir(&self, path: impl AsRef<Path>) -> io::Result<bool> {
540    Ok(self.fs_metadata(path)?.file_type() == FileType::Dir)
541  }
542
543  pub fn fs_is_symlink(&self, path: impl AsRef<Path>) -> io::Result<bool> {
544    Ok(self.fs_symlink_metadata(path)?.file_type() == FileType::Symlink)
545  }
546
547  pub fn fs_exists(&self, path: impl AsRef<Path>) -> io::Result<bool> {
548    let path = path.as_ref();
549    match self.0.base_fs_exists(path) {
550      Ok(exists) => Ok(exists),
551      Err(e) => Err(err_with_path("stat", path, e)),
552    }
553  }
554
555  pub fn fs_exists_no_err(&self, path: impl AsRef<Path>) -> bool {
556    self.0.base_fs_exists_no_err(path.as_ref())
557  }
558
559  pub fn fs_is_file_no_err(&self, path: impl AsRef<Path>) -> bool {
560    self.0.fs_is_file_no_err(path)
561  }
562
563  pub fn fs_is_dir_no_err(&self, path: impl AsRef<Path>) -> bool {
564    self.0.fs_is_dir_no_err(path)
565  }
566
567  pub fn fs_is_symlink_no_err(&self, path: impl AsRef<Path>) -> bool {
568    self.0.fs_is_symlink_no_err(path)
569  }
570}
571
572// == FsOpen ==
573
574impl<T: BaseFsOpen> SysWithPathsInErrors<'_, T> {
575  pub fn fs_open(
576    &self,
577    path: impl AsRef<Path>,
578    options: &OpenOptions,
579  ) -> io::Result<FsFileWithPathsInErrors<T::File>> {
580    let path = path.as_ref();
581    let file = self
582      .0
583      .base_fs_open(path, options)
584      .map_err(|e| err_with_path("open", path, e))?;
585    Ok(FsFileWithPathsInErrors::new(file, path.to_path_buf()))
586  }
587}
588
589// == FsOpenBoxed ==
590
591impl<T: FsOpenBoxed + ?Sized> SysWithPathsInErrors<'_, T> {
592  pub fn fs_open_boxed(
593    &self,
594    path: impl AsRef<Path>,
595    options: &OpenOptions,
596  ) -> io::Result<FsFileWithPathsInErrors<BoxedFsFile>> {
597    let path = path.as_ref();
598    let file = self
599      .0
600      .fs_open_boxed(path, options)
601      .map_err(|e| err_with_path("open", path, e))?;
602    Ok(FsFileWithPathsInErrors::new(file, path.to_path_buf()))
603  }
604}
605
606// == FsRead ==
607
608impl<T: BaseFsRead> SysWithPathsInErrors<'_, T> {
609  pub fn fs_read(
610    &self,
611    path: impl AsRef<Path>,
612  ) -> io::Result<Cow<'static, [u8]>> {
613    let path = path.as_ref();
614    self
615      .0
616      .base_fs_read(path)
617      .map_err(|e| err_with_path("read", path, e))
618  }
619
620  pub fn fs_read_to_string(
621    &self,
622    path: impl AsRef<Path>,
623  ) -> io::Result<Cow<'static, str>> {
624    let path = path.as_ref();
625    self
626      .0
627      .fs_read_to_string(path)
628      .map_err(|e| err_with_path("read", path, e))
629  }
630
631  pub fn fs_read_to_string_lossy(
632    &self,
633    path: impl AsRef<Path>,
634  ) -> io::Result<Cow<'static, str>> {
635    let path = path.as_ref();
636    self
637      .0
638      .fs_read_to_string_lossy(path)
639      .map_err(|e| err_with_path("read", path, e))
640  }
641}
642
643// == FsReadDir ==
644
645impl<T: BaseFsReadDir> SysWithPathsInErrors<'_, T> {
646  pub fn fs_read_dir(
647    &self,
648    path: impl AsRef<Path>,
649  ) -> io::Result<Box<dyn Iterator<Item = io::Result<T::ReadDirEntry>>>> {
650    let path = path.as_ref();
651    self
652      .0
653      .base_fs_read_dir(path)
654      .map_err(|e| err_with_path("read directory", path, e))
655  }
656}
657
658// == FsReadLink ==
659
660impl<T: BaseFsReadLink> SysWithPathsInErrors<'_, T> {
661  pub fn fs_read_link(&self, path: impl AsRef<Path>) -> io::Result<PathBuf> {
662    let path = path.as_ref();
663    self
664      .0
665      .base_fs_read_link(path)
666      .map_err(|e| err_with_path("read link", path, e))
667  }
668}
669
670// == FsRemoveDir ==
671
672impl<T: BaseFsRemoveDir> SysWithPathsInErrors<'_, T> {
673  pub fn fs_remove_dir(&self, path: impl AsRef<Path>) -> io::Result<()> {
674    let path = path.as_ref();
675    self
676      .0
677      .base_fs_remove_dir(path)
678      .map_err(|e| err_with_path("remove directory", path, e))
679  }
680}
681
682// == FsRemoveDirAll ==
683
684impl<T: BaseFsRemoveDirAll> SysWithPathsInErrors<'_, T> {
685  pub fn fs_remove_dir_all(&self, path: impl AsRef<Path>) -> io::Result<()> {
686    let path = path.as_ref();
687    self
688      .0
689      .base_fs_remove_dir_all(path)
690      .map_err(|e| err_with_path("remove directory", path, e))
691  }
692}
693
694// == FsRemoveFile ==
695
696impl<T: BaseFsRemoveFile> SysWithPathsInErrors<'_, T> {
697  pub fn fs_remove_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
698    let path = path.as_ref();
699    self
700      .0
701      .base_fs_remove_file(path)
702      .map_err(|e| err_with_path("remove", path, e))
703  }
704}
705
706// == FsRename ==
707
708impl<T: BaseFsRename> SysWithPathsInErrors<'_, T> {
709  pub fn fs_rename(
710    &self,
711    from: impl AsRef<Path>,
712    to: impl AsRef<Path>,
713  ) -> io::Result<()> {
714    let from = from.as_ref();
715    let to = to.as_ref();
716    self
717      .0
718      .base_fs_rename(from, to)
719      .map_err(|e| err_with_two_paths("rename", from, to, e))
720  }
721}
722
723// == FsSetFileTimes ==
724
725impl<T: BaseFsSetFileTimes> SysWithPathsInErrors<'_, T> {
726  pub fn fs_set_file_times(
727    &self,
728    path: impl AsRef<Path>,
729    atime: SystemTime,
730    mtime: SystemTime,
731  ) -> io::Result<()> {
732    let path = path.as_ref();
733    self
734      .0
735      .base_fs_set_file_times(path, atime, mtime)
736      .map_err(|e| err_with_path("set file times", path, e))
737  }
738}
739
740// == FsSetSymlinkFileTimes ==
741
742impl<T: BaseFsSetSymlinkFileTimes> SysWithPathsInErrors<'_, T> {
743  pub fn fs_set_symlink_file_times(
744    &self,
745    path: impl AsRef<Path>,
746    atime: SystemTime,
747    mtime: SystemTime,
748  ) -> io::Result<()> {
749    let path = path.as_ref();
750    self
751      .0
752      .base_fs_set_symlink_file_times(path, atime, mtime)
753      .map_err(|e| err_with_path("set symlink file times", path, e))
754  }
755}
756
757// == FsSetPermissions ==
758
759impl<T: BaseFsSetPermissions> SysWithPathsInErrors<'_, T> {
760  pub fn fs_set_permissions(
761    &self,
762    path: impl AsRef<Path>,
763    mode: u32,
764  ) -> io::Result<()> {
765    let path = path.as_ref();
766    self
767      .0
768      .base_fs_set_permissions(path, mode)
769      .map_err(|e| err_with_path("set permissions", path, e))
770  }
771}
772
773// == FsSymlinkDir ==
774
775impl<T: BaseFsSymlinkDir> SysWithPathsInErrors<'_, T> {
776  pub fn fs_symlink_dir(
777    &self,
778    original: impl AsRef<Path>,
779    link: impl AsRef<Path>,
780  ) -> io::Result<()> {
781    let original = original.as_ref();
782    let link = link.as_ref();
783    self
784      .0
785      .base_fs_symlink_dir(original, link)
786      .map_err(|e| err_with_two_paths("symlink directory", original, link, e))
787  }
788}
789
790// == FsSymlinkFile ==
791
792impl<T: BaseFsSymlinkFile> SysWithPathsInErrors<'_, T> {
793  pub fn fs_symlink_file(
794    &self,
795    original: impl AsRef<Path>,
796    link: impl AsRef<Path>,
797  ) -> io::Result<()> {
798    let original = original.as_ref();
799    let link = link.as_ref();
800    self
801      .0
802      .base_fs_symlink_file(original, link)
803      .map_err(|e| err_with_two_paths("symlink", original, link, e))
804  }
805}
806
807// == FsWrite ==
808
809impl<T: BaseFsWrite> SysWithPathsInErrors<'_, T> {
810  pub fn fs_write(
811    &self,
812    path: impl AsRef<Path>,
813    data: impl AsRef<[u8]>,
814  ) -> io::Result<()> {
815    let path = path.as_ref();
816    self
817      .0
818      .base_fs_write(path, data.as_ref())
819      .map_err(|e| err_with_path("write", path, e))
820  }
821}
822
823#[cfg(all(test, feature = "memory"))]
824mod tests {
825  use super::*;
826  use crate::impls::InMemorySys;
827  use crate::FsCreateDir;
828  use crate::FsMetadata;
829  use crate::FsRead;
830  use crate::FsWrite;
831  use std::io::Read;
832  use std::io::Write;
833
834  #[test]
835  fn test_error_display_single_path() {
836    let sys = InMemorySys::default();
837    let err = sys
838      .with_paths_in_errors()
839      .fs_read("/nonexistent")
840      .unwrap_err();
841    let inner = err.get_ref().unwrap();
842    let op_err = inner.downcast_ref::<OperationError>().unwrap();
843    assert_eq!(op_err.operation(), "read");
844    assert_eq!(
845      op_err.kind(),
846      &OperationErrorKind::WithPath("/nonexistent".to_string())
847    );
848    assert_eq!(
849      err.to_string(),
850      format!("failed to read '/nonexistent': {}", op_err.err)
851    );
852  }
853
854  #[test]
855  fn test_error_display_two_paths() {
856    let sys = InMemorySys::default();
857    let err = sys
858      .with_paths_in_errors()
859      .fs_copy("/src", "/dst")
860      .unwrap_err();
861    let inner = err.get_ref().unwrap();
862    let op_err = inner.downcast_ref::<OperationError>().unwrap();
863    assert_eq!(op_err.operation(), "copy");
864    assert_eq!(
865      op_err.kind(),
866      &OperationErrorKind::WithTwoPaths("/src".to_string(), "/dst".to_string())
867    );
868    assert_eq!(
869      err.to_string(),
870      format!("failed to copy '/src' to '/dst': {}", op_err.err)
871    );
872  }
873
874  #[test]
875  fn test_error_preserves_kind() {
876    let sys = InMemorySys::default();
877    let err = sys
878      .with_paths_in_errors()
879      .fs_read("/nonexistent")
880      .unwrap_err();
881    assert_eq!(err.kind(), io::ErrorKind::NotFound);
882  }
883
884  #[test]
885  fn test_error_downcast_to_operation_error() {
886    let sys = InMemorySys::default();
887    let err = sys
888      .with_paths_in_errors()
889      .fs_read("/nonexistent")
890      .unwrap_err();
891    let inner = err.get_ref().unwrap();
892    let op_err = inner.downcast_ref::<OperationError>().unwrap();
893    assert_eq!(op_err.operation(), "read");
894    assert_eq!(
895      op_err.kind(),
896      &OperationErrorKind::WithPath("/nonexistent".to_string())
897    );
898  }
899
900  #[test]
901  fn test_fs_read_success() {
902    let sys = InMemorySys::new_with_cwd("/");
903    sys.fs_write("/test.txt", b"hello").unwrap();
904    let data = sys.with_paths_in_errors().fs_read("/test.txt").unwrap();
905    assert_eq!(&*data, b"hello");
906  }
907
908  #[test]
909  fn test_fs_read_to_string_success() {
910    let sys = InMemorySys::new_with_cwd("/");
911    sys.fs_write("/test.txt", b"hello").unwrap();
912    let data = sys
913      .with_paths_in_errors()
914      .fs_read_to_string("/test.txt")
915      .unwrap();
916    assert_eq!(&*data, "hello");
917  }
918
919  #[test]
920  fn test_fs_read_to_string_lossy_success() {
921    let sys = InMemorySys::new_with_cwd("/");
922    sys.fs_write("/test.txt", b"hello").unwrap();
923    let data = sys
924      .with_paths_in_errors()
925      .fs_read_to_string_lossy("/test.txt")
926      .unwrap();
927    assert_eq!(&*data, "hello");
928  }
929
930  #[test]
931  fn test_fs_write_success() {
932    let sys = InMemorySys::new_with_cwd("/");
933    sys
934      .with_paths_in_errors()
935      .fs_write("/test.txt", b"hello")
936      .unwrap();
937    let data = sys.fs_read("/test.txt").unwrap();
938    assert_eq!(&*data, b"hello");
939  }
940
941  #[test]
942  fn test_fs_write_error() {
943    let sys = InMemorySys::default();
944    // writing to a path in a non-existent directory should fail
945    let err = sys
946      .with_paths_in_errors()
947      .fs_write("/nonexistent/test.txt", b"hello")
948      .unwrap_err();
949    let inner = err.get_ref().unwrap();
950    let op_err = inner.downcast_ref::<OperationError>().unwrap();
951    assert_eq!(op_err.operation(), "write");
952    assert_eq!(
953      op_err.kind(),
954      &OperationErrorKind::WithPath("/nonexistent/test.txt".to_string())
955    );
956  }
957
958  #[test]
959  fn test_fs_create_dir() {
960    let sys = InMemorySys::default();
961    sys
962      .with_paths_in_errors()
963      .fs_create_dir("/newdir", &CreateDirOptions::default())
964      .unwrap();
965    assert!(sys.fs_is_dir("/newdir").unwrap());
966  }
967
968  #[test]
969  fn test_fs_create_dir_all() {
970    let sys = InMemorySys::default();
971    sys
972      .with_paths_in_errors()
973      .fs_create_dir_all("/a/b/c")
974      .unwrap();
975    assert!(sys.fs_is_dir("/a/b/c").unwrap());
976  }
977
978  #[test]
979  fn test_fs_remove_file() {
980    let sys = InMemorySys::new_with_cwd("/");
981    sys.fs_write("/test.txt", b"hello").unwrap();
982    sys
983      .with_paths_in_errors()
984      .fs_remove_file("/test.txt")
985      .unwrap();
986    assert!(!sys.fs_exists("/test.txt").unwrap());
987  }
988
989  #[test]
990  fn test_fs_remove_file_error() {
991    let sys = InMemorySys::default();
992    let err = sys
993      .with_paths_in_errors()
994      .fs_remove_file("/nonexistent")
995      .unwrap_err();
996    let inner = err.get_ref().unwrap();
997    let op_err = inner.downcast_ref::<OperationError>().unwrap();
998    assert_eq!(op_err.operation(), "remove");
999    assert_eq!(
1000      op_err.kind(),
1001      &OperationErrorKind::WithPath("/nonexistent".to_string())
1002    );
1003  }
1004
1005  #[test]
1006  fn test_fs_remove_dir() {
1007    let sys = InMemorySys::default();
1008    sys
1009      .fs_create_dir("/testdir", &CreateDirOptions::default())
1010      .unwrap();
1011    sys
1012      .with_paths_in_errors()
1013      .fs_remove_dir("/testdir")
1014      .unwrap();
1015    assert!(!sys.fs_exists("/testdir").unwrap());
1016  }
1017
1018  #[test]
1019  fn test_fs_rename() {
1020    let sys = InMemorySys::new_with_cwd("/");
1021    sys.fs_write("/old.txt", b"hello").unwrap();
1022    sys
1023      .with_paths_in_errors()
1024      .fs_rename("/old.txt", "/new.txt")
1025      .unwrap();
1026    assert!(!sys.fs_exists("/old.txt").unwrap());
1027    assert!(sys.fs_exists("/new.txt").unwrap());
1028  }
1029
1030  #[test]
1031  fn test_fs_rename_error() {
1032    let sys = InMemorySys::default();
1033    let err = sys
1034      .with_paths_in_errors()
1035      .fs_rename("/nonexistent", "/new.txt")
1036      .unwrap_err();
1037    let inner = err.get_ref().unwrap();
1038    let op_err = inner.downcast_ref::<OperationError>().unwrap();
1039    assert_eq!(op_err.operation(), "rename");
1040    assert_eq!(
1041      op_err.kind(),
1042      &OperationErrorKind::WithTwoPaths(
1043        "/nonexistent".to_string(),
1044        "/new.txt".to_string()
1045      )
1046    );
1047  }
1048
1049  #[test]
1050  fn test_fs_copy() {
1051    let sys = InMemorySys::new_with_cwd("/");
1052    sys.fs_write("/src.txt", b"hello").unwrap();
1053    let bytes = sys
1054      .with_paths_in_errors()
1055      .fs_copy("/src.txt", "/dst.txt")
1056      .unwrap();
1057    assert_eq!(bytes, 5);
1058    assert_eq!(&*sys.fs_read("/dst.txt").unwrap(), b"hello");
1059  }
1060
1061  #[test]
1062  fn test_fs_metadata() {
1063    let sys = InMemorySys::new_with_cwd("/");
1064    sys.fs_write("/test.txt", b"hello").unwrap();
1065    let meta = sys.with_paths_in_errors().fs_metadata("/test.txt").unwrap();
1066    assert_eq!(meta.file_type(), FileType::File);
1067    assert_eq!(meta.len(), 5);
1068  }
1069
1070  #[test]
1071  fn test_fs_metadata_error() {
1072    let sys = InMemorySys::default();
1073    let err = sys
1074      .with_paths_in_errors()
1075      .fs_metadata("/nonexistent")
1076      .unwrap_err();
1077    let inner = err.get_ref().unwrap();
1078    let op_err = inner.downcast_ref::<OperationError>().unwrap();
1079    assert_eq!(op_err.operation(), "stat");
1080    assert_eq!(
1081      op_err.kind(),
1082      &OperationErrorKind::WithPath("/nonexistent".to_string())
1083    );
1084  }
1085
1086  #[test]
1087  fn test_fs_is_file() {
1088    let sys = InMemorySys::new_with_cwd("/");
1089    sys.fs_write("/test.txt", b"hello").unwrap();
1090    assert!(sys.with_paths_in_errors().fs_is_file("/test.txt").unwrap());
1091    sys
1092      .fs_create_dir("/testdir", &CreateDirOptions::default())
1093      .unwrap();
1094    assert!(!sys.with_paths_in_errors().fs_is_file("/testdir").unwrap());
1095  }
1096
1097  #[test]
1098  fn test_fs_is_dir() {
1099    let sys = InMemorySys::new_with_cwd("/");
1100    sys
1101      .fs_create_dir("/testdir", &CreateDirOptions::default())
1102      .unwrap();
1103    assert!(sys.with_paths_in_errors().fs_is_dir("/testdir").unwrap());
1104    sys.fs_write("/test.txt", b"hello").unwrap();
1105    assert!(!sys.with_paths_in_errors().fs_is_dir("/test.txt").unwrap());
1106  }
1107
1108  #[test]
1109  fn test_fs_read_dir() {
1110    let sys = InMemorySys::new_with_cwd("/");
1111    sys
1112      .fs_create_dir("/testdir", &CreateDirOptions::default())
1113      .unwrap();
1114    sys.fs_write("/testdir/a.txt", b"a").unwrap();
1115    sys.fs_write("/testdir/b.txt", b"b").unwrap();
1116    let entries: Vec<_> = sys
1117      .with_paths_in_errors()
1118      .fs_read_dir("/testdir")
1119      .unwrap()
1120      .collect::<Result<_, _>>()
1121      .unwrap();
1122    assert_eq!(entries.len(), 2);
1123  }
1124
1125  #[test]
1126  fn test_fs_read_dir_error() {
1127    let sys = InMemorySys::default();
1128    let result = sys.with_paths_in_errors().fs_read_dir("/nonexistent");
1129    let err = match result {
1130      Ok(_) => panic!("expected error"),
1131      Err(e) => e,
1132    };
1133    let inner = err.get_ref().unwrap();
1134    let op_err = inner.downcast_ref::<OperationError>().unwrap();
1135    assert_eq!(op_err.operation(), "read directory");
1136    assert_eq!(
1137      op_err.kind(),
1138      &OperationErrorKind::WithPath("/nonexistent".to_string())
1139    );
1140  }
1141
1142  #[test]
1143  fn test_fs_hard_link() {
1144    let sys = InMemorySys::new_with_cwd("/");
1145    sys.fs_write("/original.txt", b"hello").unwrap();
1146    sys
1147      .with_paths_in_errors()
1148      .fs_hard_link("/original.txt", "/link.txt")
1149      .unwrap();
1150    assert_eq!(&*sys.fs_read("/link.txt").unwrap(), b"hello");
1151  }
1152
1153  #[test]
1154  fn test_fs_hard_link_error() {
1155    let sys = InMemorySys::default();
1156    let err = sys
1157      .with_paths_in_errors()
1158      .fs_hard_link("/nonexistent", "/link.txt")
1159      .unwrap_err();
1160    let inner = err.get_ref().unwrap();
1161    let op_err = inner.downcast_ref::<OperationError>().unwrap();
1162    assert_eq!(op_err.operation(), "hard link");
1163    assert_eq!(
1164      op_err.kind(),
1165      &OperationErrorKind::WithTwoPaths(
1166        "/nonexistent".to_string(),
1167        "/link.txt".to_string()
1168      )
1169    );
1170  }
1171
1172  #[test]
1173  fn test_fs_open_error() {
1174    let sys = InMemorySys::default();
1175    let err = sys
1176      .with_paths_in_errors()
1177      .fs_open("/nonexistent", &OpenOptions::default())
1178      .unwrap_err();
1179    let inner = err.get_ref().unwrap();
1180    let op_err = inner.downcast_ref::<OperationError>().unwrap();
1181    assert_eq!(op_err.operation(), "open");
1182    assert_eq!(
1183      op_err.kind(),
1184      &OperationErrorKind::WithPath("/nonexistent".to_string())
1185    );
1186  }
1187
1188  #[test]
1189  fn test_fs_open_success() {
1190    let sys = InMemorySys::new_with_cwd("/");
1191    sys.fs_write("/test.txt", b"hello").unwrap();
1192    let mut file = sys
1193      .with_paths_in_errors()
1194      .fs_open(
1195        "/test.txt",
1196        &OpenOptions {
1197          read: true,
1198          ..Default::default()
1199        },
1200      )
1201      .unwrap();
1202    let mut buf = [0u8; 5];
1203    file.read_exact(&mut buf).unwrap();
1204    assert_eq!(&buf, b"hello");
1205  }
1206
1207  #[test]
1208  fn test_fs_file_read_write_success() {
1209    let sys = InMemorySys::new_with_cwd("/");
1210    // create and write via wrapped file
1211    let mut file = sys
1212      .with_paths_in_errors()
1213      .fs_open(
1214        "/test.txt",
1215        &OpenOptions {
1216          write: true,
1217          create: true,
1218          ..Default::default()
1219        },
1220      )
1221      .unwrap();
1222    file.write_all(b"hello").unwrap();
1223    drop(file);
1224
1225    // read via wrapped file
1226    let mut file = sys
1227      .with_paths_in_errors()
1228      .fs_open(
1229        "/test.txt",
1230        &OpenOptions {
1231          read: true,
1232          ..Default::default()
1233        },
1234      )
1235      .unwrap();
1236    let mut buf = Vec::new();
1237    file.read_to_end(&mut buf).unwrap();
1238    assert_eq!(&buf, b"hello");
1239  }
1240
1241  #[test]
1242  fn test_fs_file_path_accessor() {
1243    let sys = InMemorySys::new_with_cwd("/");
1244    sys.fs_write("/test.txt", b"hello").unwrap();
1245    let file = sys
1246      .with_paths_in_errors()
1247      .fs_open(
1248        "/test.txt",
1249        &OpenOptions {
1250          read: true,
1251          ..Default::default()
1252        },
1253      )
1254      .unwrap();
1255    assert_eq!(file.path(), Path::new("/test.txt"));
1256  }
1257
1258  #[test]
1259  fn test_fs_exists() {
1260    let sys = InMemorySys::new_with_cwd("/");
1261    sys.fs_write("/test.txt", b"hello").unwrap();
1262    assert!(sys.with_paths_in_errors().fs_exists("/test.txt").unwrap());
1263    assert!(!sys
1264      .with_paths_in_errors()
1265      .fs_exists("/nonexistent")
1266      .unwrap());
1267  }
1268
1269  #[test]
1270  fn test_fs_exists_no_err() {
1271    let sys = InMemorySys::new_with_cwd("/");
1272    sys.fs_write("/test.txt", b"hello").unwrap();
1273    assert!(sys.with_paths_in_errors().fs_exists_no_err("/test.txt"));
1274    assert!(!sys.with_paths_in_errors().fs_exists_no_err("/nonexistent"));
1275  }
1276
1277  #[test]
1278  fn test_fs_is_file_no_err() {
1279    let sys = InMemorySys::new_with_cwd("/");
1280    sys
1281      .fs_create_dir("/dir", &CreateDirOptions::default())
1282      .unwrap();
1283    sys.fs_write("/dir/file.txt", b"hello").unwrap();
1284    assert!(sys
1285      .with_paths_in_errors()
1286      .fs_is_file_no_err("/dir/file.txt"));
1287    assert!(!sys.with_paths_in_errors().fs_is_file_no_err("/dir"));
1288    assert!(!sys.with_paths_in_errors().fs_is_file_no_err("/nonexistent"));
1289  }
1290
1291  #[test]
1292  fn test_fs_is_dir_no_err() {
1293    let sys = InMemorySys::new_with_cwd("/");
1294    sys
1295      .fs_create_dir("/dir", &CreateDirOptions::default())
1296      .unwrap();
1297    sys.fs_write("/dir/file.txt", b"hello").unwrap();
1298    assert!(sys.with_paths_in_errors().fs_is_dir_no_err("/dir"));
1299    assert!(!sys.with_paths_in_errors().fs_is_dir_no_err("/dir/file.txt"));
1300    assert!(!sys.with_paths_in_errors().fs_is_dir_no_err("/nonexistent"));
1301  }
1302}