1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Error and Result definitions

use std::{error, fmt, io};

/// The error type of this crate
#[derive(Debug)]
pub struct VfsError {
    /// The path this error was encountered in
    path: String,
    /// The kind of error
    kind: VfsErrorKind,
    /// An optional human-readable string describing the context for this error
    ///
    /// If not provided, a generic context message is used
    context: String,
    /// The underlying error
    cause: Option<Box<VfsError>>,
}

/// The only way to create a VfsError is via a VfsErrorKind
///
/// This conversion implements certain normalizations
impl From<VfsErrorKind> for VfsError {
    fn from(kind: VfsErrorKind) -> Self {
        // Normalize the error here before we return it
        let kind = match kind {
            VfsErrorKind::IoError(io) => match io.kind() {
                io::ErrorKind::NotFound => VfsErrorKind::FileNotFound,
                // TODO: If MSRV changes to 1.53, enable this. Alternatively,
                //      if it's possible to #[cfg] just this line, try that
                // io::ErrorKind::Unsupported => VfsErrorKind::NotSupported,
                _ => VfsErrorKind::IoError(io),
            },
            // Remaining kinda are passed through as-is
            other => other,
        };

        Self {
            // TODO (Techno): See if this could be checked at compile-time to make sure the VFS abstraction
            //              never forgets to add a path. Might need a separate error type for FS impls vs VFS
            path: "PATH NOT FILLED BY VFS LAYER".into(),
            kind,
            context: "An error occured".into(),
            cause: None,
        }
    }
}

impl From<io::Error> for VfsError {
    fn from(err: io::Error) -> Self {
        Self::from(VfsErrorKind::IoError(err))
    }
}

impl VfsError {
    // Path filled by the VFS crate rather than the implementations
    pub(crate) fn with_path(mut self, path: impl Into<String>) -> Self {
        self.path = path.into();
        self
    }

    pub fn with_context<C, F>(mut self, context: F) -> Self
    where
        C: fmt::Display + Send + Sync + 'static,
        F: FnOnce() -> C,
    {
        self.context = context().to_string();
        self
    }

    pub fn with_cause(mut self, cause: VfsError) -> Self {
        self.cause = Some(Box::new(cause));
        self
    }

    pub fn kind(&self) -> &VfsErrorKind {
        &self.kind
    }

    pub fn path(&self) -> &String {
        &self.path
    }
}

impl fmt::Display for VfsError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} for '{}': {}", self.context, self.path, self.kind())
    }
}

impl error::Error for VfsError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        if let Some(cause) = &self.cause {
            Some(cause)
        } else {
            None
        }
    }
}

/// The kinds of errors that can occur
#[derive(Debug)]
pub enum VfsErrorKind {
    /// A generic I/O error
    ///
    /// Certain standard I/O errors are normalized to their VfsErrorKind counterparts
    IoError(io::Error),

    #[cfg(feature = "async-fs")]
    /// A generic async I/O error
    AsyncIoError(io::Error),

    /// The file or directory at the given path could not be found
    FileNotFound,

    /// The given path is invalid, e.g. because contains '.' or '..'
    InvalidPath,

    /// Generic error variant
    Other(String),

    /// There is already a directory at the given path
    DirectoryExists,

    /// There is already a file at the given path
    FileExists,

    /// Functionality not supported by this filesystem
    NotSupported,
}

impl fmt::Display for VfsErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            VfsErrorKind::IoError(cause) => {
                write!(f, "IO error: {}", cause)
            }
            #[cfg(feature = "async-fs")]
            VfsErrorKind::AsyncIoError(cause) => {
                write!(f, "Async IO error: {}", cause)
            }
            VfsErrorKind::FileNotFound => {
                write!(f, "The file or directory could not be found")
            }
            VfsErrorKind::InvalidPath => {
                write!(f, "The path is invalid")
            }
            VfsErrorKind::Other(message) => {
                write!(f, "FileSystem error: {}", message)
            }
            VfsErrorKind::NotSupported => {
                write!(f, "Functionality not supported by this filesystem")
            }
            VfsErrorKind::DirectoryExists => {
                write!(f, "Directory already exists")
            }
            VfsErrorKind::FileExists => {
                write!(f, "File already exists")
            }
        }
    }
}

/// The result type of this crate
pub type VfsResult<T> = std::result::Result<T, VfsError>;

#[cfg(test)]
mod tests {
    use crate::error::VfsErrorKind;
    use crate::{VfsError, VfsResult};

    fn produce_vfs_result() -> VfsResult<()> {
        Err(VfsError::from(VfsErrorKind::Other("Not a file".into())).with_path("foo"))
    }

    fn produce_anyhow_result() -> anyhow::Result<()> {
        Ok(produce_vfs_result()?)
    }

    #[test]
    fn anyhow_compatibility() {
        let result = produce_anyhow_result().unwrap_err();
        assert_eq!(
            result.to_string(),
            "An error occured for 'foo': FileSystem error: Not a file"
        )
    }
}