Skip to main content

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