Skip to main content

wowlab_fs/
error.rs

1//! Contextual filesystem errors.
2
3use std::{fmt, io};
4
5use crate::path::{Path, PathBuf};
6
7#[derive(Clone, Copy, Debug)]
8pub(crate) enum Operation {
9    Canonicalize,
10    CreateDirectory,
11    CreateFile,
12    #[cfg(not(target_family = "wasm"))]
13    CreateLink,
14    Inspect,
15    OpenFile,
16    ReadDirectory,
17    ReadFile,
18    RemoveDirectory,
19    RemoveFile,
20    Rename,
21    #[cfg(not(target_family = "wasm"))]
22    RetainFile,
23    #[cfg(not(target_family = "wasm"))]
24    SecureFile,
25    Sync,
26    WriteFile,
27}
28
29impl fmt::Display for Operation {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        let value = match self {
32            Self::Canonicalize => "canonicalize",
33            Self::CreateDirectory => "create directory",
34            Self::CreateFile => "create file",
35            #[cfg(not(target_family = "wasm"))]
36            Self::CreateLink => "create symbolic link",
37            Self::Inspect => "inspect",
38            Self::OpenFile => "open file",
39            Self::ReadDirectory => "read directory",
40            Self::ReadFile => "read file",
41            Self::RemoveDirectory => "remove directory",
42            Self::RemoveFile => "remove file",
43            Self::Rename => "rename",
44            #[cfg(not(target_family = "wasm"))]
45            Self::RetainFile => "retain file",
46            #[cfg(not(target_family = "wasm"))]
47            Self::SecureFile => "secure file",
48            Self::Sync => "sync",
49            Self::WriteFile => "write file",
50        };
51
52        f.write_str(value)
53    }
54}
55
56/// A filesystem failure with the affected path attached.
57#[derive(Debug, thiserror::Error)]
58#[error("failed to {operation} {path}{destination}: {source}")]
59pub struct Error {
60    operation: Operation,
61    path: PathBuf,
62    destination: Destination,
63    #[source]
64    source: io::Error,
65}
66
67impl Error {
68    pub(crate) fn new(operation: Operation, path: &Path, source: io::Error) -> Self {
69        Self {
70            operation,
71            path: path.to_path_buf(),
72            destination: Destination(None),
73            source,
74        }
75    }
76
77    pub(crate) fn between(
78        operation: Operation,
79        path: &Path,
80        other_path: &Path,
81        source: io::Error,
82    ) -> Self {
83        Self {
84            operation,
85            path: path.to_path_buf(),
86            destination: Destination(Some(other_path.to_path_buf())),
87            source,
88        }
89    }
90
91    /// Return the primary path involved in the failed operation.
92    #[must_use]
93    pub fn path(&self) -> &Path {
94        &self.path
95    }
96
97    /// Return the secondary path for a two-path operation.
98    #[must_use]
99    pub fn other_path(&self) -> Option<&Path> {
100        self.destination.0.as_deref()
101    }
102
103    /// Report whether the target did not exist.
104    #[must_use]
105    pub fn is_not_found(&self) -> bool {
106        self.source.kind() == io::ErrorKind::NotFound
107    }
108
109    /// Report whether the target already existed.
110    #[must_use]
111    pub fn is_already_exists(&self) -> bool {
112        self.source.kind() == io::ErrorKind::AlreadyExists
113    }
114
115    /// Report whether a directory could not be removed because it was not empty.
116    #[must_use]
117    pub fn is_directory_not_empty(&self) -> bool {
118        self.source.kind() == io::ErrorKind::DirectoryNotEmpty
119    }
120}
121
122#[derive(Debug)]
123struct Destination(Option<PathBuf>);
124
125impl fmt::Display for Destination {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        if let Some(path) = &self.0 {
128            write!(f, " to {path}")?;
129        }
130
131        Ok(())
132    }
133}
134
135/// Result returned by filesystem operations.
136pub type Result<T> = std::result::Result<T, Error>;
137
138#[cfg(test)]
139mod tests {
140    use std::{error::Error as _, io};
141
142    use googletest::prelude::*;
143
144    use super::{Error, Operation};
145    use crate::path::Path;
146
147    #[gtest]
148    fn single_path_error_preserves_context_source_and_predicate() -> Result<()> {
149        let error = Error::new(
150            Operation::ReadFile,
151            Path::new("config.toml"),
152            io::Error::new(io::ErrorKind::NotFound, "missing"),
153        );
154
155        verify_eq!(
156            error.to_string(),
157            "failed to read file config.toml: missing"
158        )?;
159        verify_eq!(error.path(), Path::new("config.toml"))?;
160        verify_true!(error.other_path().is_none())?;
161        verify_true!(error.is_not_found())?;
162
163        verify_true!(error.source().is_some())
164    }
165
166    #[gtest]
167    fn two_path_error_preserves_both_paths() -> Result<()> {
168        let error = Error::between(
169            Operation::Rename,
170            Path::new("before"),
171            Path::new("after"),
172            io::Error::new(io::ErrorKind::AlreadyExists, "occupied"),
173        );
174
175        verify_eq!(
176            error.to_string(),
177            "failed to rename before to after: occupied"
178        )?;
179        verify_eq!(error.path(), Path::new("before"))?;
180        verify_eq!(error.other_path(), Some(Path::new("after")))?;
181
182        verify_true!(error.is_already_exists())
183    }
184}