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
#[extend::ext]
15
impl 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]
33
pub 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

            
145
impl Error {
146
    /// Create an error from an IoError encountered while inspecting permissions
147
    /// on an object.
148
24368
    pub(crate) fn inspecting(err: IoError, fname: impl Into<PathBuf>) -> Self {
149
24368
        match err.kind() {
150
24368
            IoErrorKind::NotFound => Error::NotFound(fname.into()),
151
            _ => Error::CouldNotInspect(fname.into(), Arc::new(err)),
152
        }
153
24368
    }
154

            
155
    /// Create an error from an IoError encountered while performing IO (open,
156
    /// read, write) on an object.
157
5115
    pub(crate) fn io(err: IoError, fname: impl Into<PathBuf>, action: &'static str) -> Self {
158
5115
        match err.kind() {
159
5043
            IoErrorKind::NotFound => Error::NotFound(fname.into()),
160
72
            _ => Error::Io {
161
72
                filename: fname.into(),
162
72
                action,
163
72
                err: Arc::new(err),
164
72
            },
165
        }
166
5115
    }
167

            
168
    /// Return the path, if any, associated with this error.
169
14
    pub fn path(&self) -> Option<&Path> {
170
        Some(
171
14
            match self {
172
                Error::NotFound(pb) => pb,
173
8
                Error::BadPermission(pb, ..) => pb,
174
                Error::BadOwner(pb, _) => pb,
175
4
                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
2
                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
12
            .as_path(),
192
        )
193
14
    }
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
6
    pub fn errors<'a>(&'a self) -> impl Iterator<Item = &'a Error> + 'a {
233
6
        let result: Box<dyn Iterator<Item = &Error> + 'a> = match self {
234
5
            Error::Multiple(v) => Box::new(v.iter().map(|e| e.as_ref())),
235
4
            _ => Box::new(vec![self].into_iter()),
236
        };
237

            
238
6
        result
239
6
    }
240
}
241

            
242
impl std::iter::FromIterator<Error> for Option<Error> {
243
6
    fn from_iter<T: IntoIterator<Item = Error>>(iter: T) -> Self {
244
6
        let mut iter = iter.into_iter();
245

            
246
6
        let first_err = iter.next()?;
247

            
248
6
        if let Some(second_err) = iter.next() {
249
2
            let mut errors = Vec::with_capacity(iter.size_hint().0 + 2);
250
2
            errors.push(Box::new(first_err));
251
2
            errors.push(Box::new(second_err));
252
2
            errors.extend(iter.map(Box::new));
253
2
            Some(Error::Multiple(errors))
254
        } else {
255
4
            Some(first_err)
256
        }
257
6
    }
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.
266
558
pub fn format_access_bits(bits: u32, c: char) -> String {
267
558
    let mut s = String::new();
268

            
269
1674
    for (shift, prefix) in [(6, 'u'), (3, 'g'), (0, 'o')] {
270
1674
        let b = (bits >> shift) & 7;
271
1674
        if b != 0 {
272
1386
            if !s.is_empty() {
273
830
                s.push(',');
274
830
            }
275
1386
            s.push(prefix);
276
1386
            s.push(c);
277
4158
            for (bit, ch) in [(4, 'r'), (2, 'w'), (1, 'x')] {
278
4158
                if b & bit != 0 {
279
3042
                    s.push(ch);
280
3042
                }
281
            }
282
288
        }
283
    }
284

            
285
558
    s
286
558
}
287

            
288
#[cfg(test)]
289
mod 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
}