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
21510
    pub(crate) fn inspecting(err: IoError, fname: impl Into<PathBuf>) -> Self {
152
21510
        match err.kind() {
153
21510
            IoErrorKind::NotFound => Error::NotFound(fname.into()),
154
            _ => Error::CouldNotInspect(fname.into(), Arc::new(err)),
155
        }
156
21510
    }
157

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

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

            
241
6
        result
242
6
    }
243
}
244

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

            
249
6
        let first_err = iter.next()?;
250

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

            
272
1938
    for (shift, prefix) in [(6, 'u'), (3, 'g'), (0, 'o')] {
273
1938
        let b = (bits >> shift) & 7;
274
1938
        if b != 0 {
275
1606
            if !s.is_empty() {
276
962
                s.push(',');
277
962
            }
278
1606
            s.push(prefix);
279
1606
            s.push(c);
280
4818
            for (bit, ch) in [(4, 'r'), (2, 'w'), (1, 'x')] {
281
4818
                if b & bit != 0 {
282
3526
                    s.push(ch);
283
3526
                }
284
            }
285
332
        }
286
    }
287

            
288
646
    s
289
646
}
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
}