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 }