Skip to main content

emperor_folder/
error.rs

1//! Errors a tool can answer with. Every message is written for the model: it says what was
2//! wrong and, where there is one, what to do instead.
3
4use std::io;
5
6/// Why a folder operation was refused or failed.
7#[derive(Debug, thiserror::Error)]
8pub enum FolderError {
9    /// An absolute path was given.
10    #[error("absolute paths are not allowed, use a path relative to the root folder: {0}")]
11    Absolute(String),
12    /// The path uses `..` or resolves (through a symlink) outside the root.
13    #[error("path leaves the root folder and is refused: {0}")]
14    Escape(String),
15    /// Nothing exists at the path.
16    #[error("not found: {0}")]
17    NotFound(String),
18    /// The target already exists.
19    #[error("{0}")]
20    Exists(String),
21    /// A write or move on a read-only server.
22    #[error(
23        "the server is read-only: write and move are disabled (the operator starts it with --writable to allow them)"
24    )]
25    ReadOnly,
26    /// The file or content is over the configured size limit.
27    #[error("{path} is {size} bytes, over the {limit}-byte limit{hint}")]
28    TooLarge {
29        /// Root-relative path.
30        path: String,
31        /// Size found.
32        size: u64,
33        /// Configured limit.
34        limit: u64,
35        /// What to do instead (may be empty).
36        hint: &'static str,
37    },
38    /// Bad arguments: wrong kind of entry, bad glob or regex, empty range.
39    #[error("{0}")]
40    Invalid(String),
41    /// Any other I/O failure.
42    #[error("{path}: {source}")]
43    Io {
44        /// Root-relative path.
45        path: String,
46        /// The underlying error.
47        source: io::Error,
48    },
49}
50
51impl FolderError {
52    /// Classify an I/O error from `cap_std` for the path the model asked about.
53    pub fn from_io(err: io::Error, path: &str) -> Self {
54        // cap-std reports any resolution that leaves the root (absolute symlink target, `..`
55        // through a symlink) as PermissionDenied "a path led outside of the filesystem".
56        if err.kind() == io::ErrorKind::PermissionDenied
57            && err.to_string().contains("outside of the filesystem")
58        {
59            return Self::Escape(path.to_string());
60        }
61        match err.kind() {
62            io::ErrorKind::NotFound => Self::NotFound(path.to_string()),
63            io::ErrorKind::AlreadyExists => Self::Exists(format!("{path} already exists")),
64            _ => Self::Io {
65                path: path.to_string(),
66                source: err,
67            },
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn cap_std_escape_maps_to_escape() {
78        let e = io::Error::new(
79            io::ErrorKind::PermissionDenied,
80            "a path led outside of the filesystem",
81        );
82        assert!(matches!(
83            FolderError::from_io(e, "link"),
84            FolderError::Escape(_)
85        ));
86    }
87
88    #[test]
89    fn not_found_and_exists_are_classified() {
90        let nf = io::Error::from(io::ErrorKind::NotFound);
91        assert!(matches!(
92            FolderError::from_io(nf, "x"),
93            FolderError::NotFound(_)
94        ));
95        let ex = io::Error::from(io::ErrorKind::AlreadyExists);
96        assert!(
97            FolderError::from_io(ex, "x")
98                .to_string()
99                .contains("already exists")
100        );
101    }
102}