/ crates / fs-mistrust / src / err.rs
err.rs
  1  //! Declare an Error type for `fs-mistrust`.
  2  
  3  use std::path::Path;
  4  use std::{path::PathBuf, sync::Arc};
  5  
  6  use std::io::{Error as IoError, ErrorKind as IoErrorKind};
  7  
  8  #[cfg(feature = "anon_home")]
  9  use 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  trait PathExt {
 15      /// A do-nothing extension function.
 16      fn anonymize_home(&self) -> impl std::fmt::Display + '_;
 17  }
 18  #[cfg(not(feature = "anon_home"))]
 19  impl 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]
 36  pub 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  
148  impl 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  
245  impl 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.
269  pub 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)]
292  mod 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  }