fs_mistrust/
err.rs

1//! Declare an Error type for `fs-mistrust`.
2
3use std::path::Path;
4use std::{path::PathBuf, sync::Arc};
5
6use std::io::{Error as IoError, ErrorKind as IoErrorKind};
7
8#[cfg(feature = "anon_home")]
9use crate::anon_home::PathExt as _;
10
11/// Define a local-only version of anonymize_home so that we can define our errors
12/// unconditionally.
13#[cfg(not(feature = "anon_home"))]
14trait PathExt {
15    /// A do-nothing extension function.
16    fn anonymize_home(&self) -> impl std::fmt::Display + '_;
17}
18#[cfg(not(feature = "anon_home"))]
19impl PathExt for Path {
20    #[allow(clippy::disallowed_methods)] // lossiness is expected
21    fn anonymize_home(&self) -> impl std::fmt::Display + '_ {
22        self.display()
23    }
24}
25
26/// An error returned while checking a path for privacy.
27///
28/// Note that this often means a necessary file *doesn't exist at all*.
29///
30/// When printing a `fs_mistrust::Error`, do not describe it as a "permissions error".
31/// Describe it with less specific wording, perhaps "Problem accessing Thing".
32///
33/// The `Display` impl will give the details.
34#[derive(Clone, Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum Error {
37    /// A target  (or one of its ancestors) was not found.
38    #[error("File or directory {} not found", _0.anonymize_home())]
39    NotFound(PathBuf),
40
41    /// A target  (or one of its ancestors) had incorrect permissions.
42    ///
43    /// Only generated on unix-like systems.
44    ///
45    /// The first integer contains the current permission bits, and the second
46    /// contains the permission bits which were incorrectly set.
47    #[error("Incorrect permissions: {} is {}; must be {}",
48            _0.anonymize_home(),
49            format_access_bits(* .1, '='),
50            format_access_bits(* .2, '-'))]
51    BadPermission(PathBuf, u32, u32),
52
53    /// A target  (or one of its ancestors) had an untrusted owner.
54    ///
55    /// Only generated on unix-like systems.
56    ///
57    /// The provided integer contains the user_id o
58    #[error("Bad owner (UID {1}) on file or directory {anon}", anon = _0.anonymize_home())]
59    BadOwner(PathBuf, u32),
60
61    /// A target (or one of its ancestors) had the wrong type.
62    ///
63    /// Ordinarily, the target may be anything at all, though you can override
64    /// this with [`require_file`](crate::Verifier::require_file) and
65    /// [`require_directory`](crate::Verifier::require_directory).
66    #[error("Wrong type of file at {}", _0.anonymize_home())]
67    BadType(PathBuf),
68
69    /// We were unable to inspect the target or one of its ancestors.
70    ///
71    /// (Ironically, we might lack permissions to see if something's permissions
72    /// are correct.)
73    ///
74    /// (The `std::io::Error` that caused this problem is wrapped in an `Arc` so
75    /// that our own [`Error`] type can implement `Clone`.)
76    #[error("Unable to access {}", _0.anonymize_home())]
77    CouldNotInspect(PathBuf, #[source] Arc<IoError>),
78
79    /// Multiple errors occurred while inspecting the target.
80    ///
81    /// This variant will only be returned if the caller specifically asked for
82    /// it by calling [`all_errors`](crate::Verifier::all_errors).
83    ///
84    /// We will never construct an instance of this variant with an empty `Vec`.
85    #[error("Multiple errors found")]
86    Multiple(Vec<Box<Error>>),
87
88    /// We've realized that we can't finish resolving our path without taking
89    /// more than the maximum number of steps.  The likeliest explanation is a
90    /// symlink loop.
91    #[error("Too many steps taken or planned: Possible symlink loop?")]
92    StepsExceeded,
93
94    /// We can't find our current working directory, or we found it but it looks
95    /// impossible.
96    #[error("Problem finding current directory")]
97    CurrentDirectory(#[source] Arc<IoError>),
98
99    /// We tried to create a directory, and encountered a failure in doing so.
100    #[error("Problem creating directory")]
101    CreatingDir(#[source] Arc<IoError>),
102
103    /// We found a problem while checking the contents of the directory.
104    #[error("Problem in directory contents")]
105    Content(#[source] Box<Error>),
106
107    /// We were unable to inspect the contents of the directory
108    ///
109    /// This error is only present when the `walkdir` feature is enabled.
110    #[cfg(feature = "walkdir")]
111    #[error("Unable to list directory contents")]
112    Listing(#[source] Arc<walkdir::Error>),
113
114    /// Tried to use an invalid path with a [`CheckedDir`](crate::CheckedDir),
115    #[error("Provided path was not valid for use with CheckedDir")]
116    InvalidSubdirectory,
117
118    /// We encountered an error while attempting an IO operation on a file.
119    #[error("IO error on {} while attempting to {action}", filename.anonymize_home())]
120    Io {
121        /// The file that we were trying to modify or inspect
122        filename: PathBuf,
123        /// The action that failed.
124        action: &'static str,
125        /// The error that we got when trying to perform the operation.
126        #[source]
127        err: Arc<IoError>,
128    },
129
130    /// A field was missing when we tried to construct a
131    /// [`Mistrust`](crate::Mistrust).
132    #[error("Missing field when constructing Mistrust")]
133    MissingField(#[from] derive_builder::UninitializedFieldError),
134
135    /// A  group that we were configured to trust could not be found.
136    #[error("Configured with nonexistent group: {0}")]
137    NoSuchGroup(String),
138
139    /// A user that we were configured to trust could not be found.
140    #[error("Configured with nonexistent user: {0}")]
141    NoSuchUser(String),
142
143    /// Error accessing passwd/group databases or obtaining our uids/gids
144    #[error("Error accessing passwd/group databases or obtaining our uids/gids")]
145    PasswdGroupIoError(#[source] Arc<IoError>),
146}
147
148impl Error {
149    /// Create an error from an IoError encountered while inspecting permissions
150    /// on an object.
151    pub(crate) fn inspecting(err: IoError, fname: impl Into<PathBuf>) -> Self {
152        match err.kind() {
153            IoErrorKind::NotFound => Error::NotFound(fname.into()),
154            _ => Error::CouldNotInspect(fname.into(), Arc::new(err)),
155        }
156    }
157
158    /// Create an error from an IoError encountered while performing IO (open,
159    /// read, write) on an object.
160    pub(crate) fn io(err: IoError, fname: impl Into<PathBuf>, action: &'static str) -> Self {
161        match err.kind() {
162            IoErrorKind::NotFound => Error::NotFound(fname.into()),
163            _ => Error::Io {
164                filename: fname.into(),
165                action,
166                err: Arc::new(err),
167            },
168        }
169    }
170
171    /// Return the path, if any, associated with this error.
172    pub fn path(&self) -> Option<&Path> {
173        Some(
174            match self {
175                Error::NotFound(pb) => pb,
176                Error::BadPermission(pb, ..) => pb,
177                Error::BadOwner(pb, _) => pb,
178                Error::BadType(pb) => pb,
179                Error::CouldNotInspect(pb, _) => pb,
180                Error::Io { filename: pb, .. } => pb,
181                Error::Multiple(_) => return None,
182                Error::StepsExceeded => return None,
183                Error::CurrentDirectory(_) => return None,
184                Error::CreatingDir(_) => return None,
185                Error::InvalidSubdirectory => return None,
186                Error::Content(e) => return e.path(),
187                #[cfg(feature = "walkdir")]
188                Error::Listing(e) => return e.path(),
189                Error::MissingField(_) => return None,
190                Error::NoSuchGroup(_) => return None,
191                Error::NoSuchUser(_) => return None,
192                Error::PasswdGroupIoError(_) => return None,
193            }
194            .as_path(),
195        )
196    }
197
198    /// Return true iff this error indicates a problem with filesystem
199    /// permissions.
200    ///
201    /// (Other errors typically indicate an IO problem, possibly one preventing
202    /// us from looking at permissions in the first place)
203    pub fn is_bad_permission(&self) -> bool {
204        match self {
205            Error::BadPermission(..) | Error::BadOwner(_, _) | Error::BadType(_) => true,
206
207            Error::NotFound(_)
208            | Error::CouldNotInspect(_, _)
209            | Error::StepsExceeded
210            | Error::CurrentDirectory(_)
211            | Error::CreatingDir(_)
212            | Error::InvalidSubdirectory
213            | Error::Io { .. }
214            | Error::MissingField(_)
215            | Error::NoSuchGroup(_)
216            | Error::NoSuchUser(_)
217            | Error::PasswdGroupIoError(_) => false,
218
219            #[cfg(feature = "walkdir")]
220            Error::Listing(_) => false,
221
222            Error::Multiple(errs) => errs.iter().any(|e| e.is_bad_permission()),
223            Error::Content(err) => err.is_bad_permission(),
224        }
225    }
226
227    /// Return an iterator over all of the errors contained in this Error.
228    ///
229    /// If this is a singleton, the iterator returns only a single element.
230    /// Otherwise, it returns all the elements inside the `Error::Multiple`
231    /// variant.
232    ///
233    /// Does not recurse, since we do not create nested instances of
234    /// `Error::Multiple`.
235    pub fn errors<'a>(&'a self) -> impl Iterator<Item = &'a Error> + 'a {
236        let result: Box<dyn Iterator<Item = &Error> + 'a> = match self {
237            Error::Multiple(v) => Box::new(v.iter().map(|e| e.as_ref())),
238            _ => Box::new(vec![self].into_iter()),
239        };
240
241        result
242    }
243}
244
245impl std::iter::FromIterator<Error> for Option<Error> {
246    fn from_iter<T: IntoIterator<Item = Error>>(iter: T) -> Self {
247        let mut iter = iter.into_iter();
248
249        let first_err = iter.next()?;
250
251        if let Some(second_err) = iter.next() {
252            let mut errors = Vec::with_capacity(iter.size_hint().0 + 2);
253            errors.push(Box::new(first_err));
254            errors.push(Box::new(second_err));
255            errors.extend(iter.map(Box::new));
256            Some(Error::Multiple(errors))
257        } else {
258            Some(first_err)
259        }
260    }
261}
262
263/// Convert the low 9 bits of `bits` into a unix-style string describing its
264/// access permission. Insert `c` between the ugo and perm.
265///
266/// For example, 0o022, '+' becomes 'g+w,o+w'.
267///
268/// Used for generating error messages.
269pub fn format_access_bits(bits: u32, c: char) -> String {
270    let mut s = String::new();
271
272    for (shift, prefix) in [(6, 'u'), (3, 'g'), (0, 'o')] {
273        let b = (bits >> shift) & 7;
274        if b != 0 {
275            if !s.is_empty() {
276                s.push(',');
277            }
278            s.push(prefix);
279            s.push(c);
280            for (bit, ch) in [(4, 'r'), (2, 'w'), (1, 'x')] {
281                if b & bit != 0 {
282                    s.push(ch);
283                }
284            }
285        }
286    }
287
288    s
289}
290
291#[cfg(test)]
292mod test {
293    // @@ begin test lint list maintained by maint/add_warning @@
294    #![allow(clippy::bool_assert_comparison)]
295    #![allow(clippy::clone_on_copy)]
296    #![allow(clippy::dbg_macro)]
297    #![allow(clippy::mixed_attributes_style)]
298    #![allow(clippy::print_stderr)]
299    #![allow(clippy::print_stdout)]
300    #![allow(clippy::single_char_pattern)]
301    #![allow(clippy::unwrap_used)]
302    #![allow(clippy::unchecked_duration_subtraction)]
303    #![allow(clippy::useless_vec)]
304    #![allow(clippy::needless_pass_by_value)]
305    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
306    use super::*;
307
308    #[test]
309    fn bits() {
310        assert_eq!(format_access_bits(0o777, '='), "u=rwx,g=rwx,o=rwx");
311        assert_eq!(format_access_bits(0o022, '='), "g=w,o=w");
312        assert_eq!(format_access_bits(0o022, '-'), "g-w,o-w");
313        assert_eq!(format_access_bits(0o020, '-'), "g-w");
314        assert_eq!(format_access_bits(0, ' '), "");
315    }
316
317    #[test]
318    fn bad_perms() {
319        assert_eq!(
320            Error::BadPermission(PathBuf::from("/path"), 0o777, 0o022).to_string(),
321            "Incorrect permissions: /path is u=rwx,g=rwx,o=rwx; must be g-w,o-w"
322        );
323    }
324}