1
//! Replace the home-directory in a filename with `${HOME}` or `%UserProfile%`
2
//!
3
//! In some privacy-sensitive applications, we want to lower the amount of
4
//! personally identifying information in our logs. In such environments, it's
5
//! good to avoid logging the actual value of the home directory, since those
6
//! frequently identify the user.
7

            
8
use std::{
9
    collections::HashSet,
10
    fmt::Display,
11
    path::{Path, PathBuf},
12
};
13

            
14
use once_cell::sync::Lazy;
15

            
16
/// Cached value of our observed home directory.
17
static HOMEDIRS: Lazy<Vec<PathBuf>> = Lazy::new(default_homedirs);
18

            
19
/// Return a list of home directories in official and canonical forms.
20
239
fn default_homedirs() -> Vec<PathBuf> {
21
239
    if let Some(basic_home) = dirs::home_dir() {
22
        // Build as a HashSet, to de-duplicate.
23
239
        let mut homedirs = HashSet::new();
24
239

            
25
239
        // We like our home directory.
26
239
        homedirs.insert(basic_home.clone());
27
        // We like the canonical version of our home directory.
28
239
        if let Ok(canonical) = std::fs::canonicalize(&basic_home) {
29
239
            homedirs.insert(canonical);
30
239
        }
31
        // We like the version of our home directory generated by `ResolvePath`.
32
239
        if let Ok(rp) = crate::walk::ResolvePath::new(basic_home) {
33
239
            let (mut p, rest) = rp.into_result();
34
239
            p.extend(rest);
35
239
            homedirs.insert(p);
36
239
        }
37

            
38
239
        homedirs.into_iter().collect()
39
    } else {
40
        vec![]
41
    }
42
239
}
43

            
44
/// The string that we use to represent our home directory in a compacted path.
45
const HOME_SUBSTITUTION: &str = {
46
    if cfg!(target_family = "windows") {
47
        "%UserProfile%"
48
    } else {
49
        "${HOME}"
50
    }
51
};
52

            
53
/// An extension trait for [`Path`].
54
pub trait PathExt {
55
    /// If this is a path within our home directory, try to replace the home
56
    /// directory component with a symbolic reference to our home directory.
57
    ///
58
    /// This function can be useful for outputting paths while reducing the risk
59
    /// of exposing usernames in the log.
60
    ///
61
    /// # Examples
62
    ///
63
    /// ```no_run
64
    /// use std::path::{Path,PathBuf};
65
    /// use fs_mistrust::anon_home::PathExt as _;
66
    ///
67
    /// let path = PathBuf::from("/home/arachnidsGrip/.config/arti.toml");
68
    /// assert_eq!(path.anonymize_home().to_string(),
69
    ///            "${HOME}/.config/arti.toml");
70
    /// panic!();
71
    /// ```
72
    fn anonymize_home(&self) -> AnonHomePath<'_>;
73
}
74

            
75
impl PathExt for Path {
76
2299
    fn anonymize_home(&self) -> AnonHomePath<'_> {
77
2299
        AnonHomePath(self)
78
2299
    }
79
}
80

            
81
/// A wrapper for `Path` which, when displayed, replaces the home directory with
82
/// a symbolic reference.
83
#[derive(Debug, Clone)]
84
pub struct AnonHomePath<'a>(&'a Path);
85

            
86
impl<'a> std::fmt::Display for AnonHomePath<'a> {
87
2299
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88
        /// `Path::display`, but with a better name and an allow
89
        #[allow(clippy::disallowed_methods)]
90
2299
        fn display_lossy(p: &Path) -> impl Display + '_ {
91
2299
            p.display()
92
2299
        }
93

            
94
        // We compare against both the home directory and the canonical home
95
        // directory, since sometimes we'll want to canonicalize a path before
96
        // passing it to this function and still have it work.
97
2299
        for home in HOMEDIRS.iter() {
98
2299
            if let Ok(suffix) = self.0.strip_prefix(home) {
99
1426
                return write!(
100
1426
                    f,
101
1426
                    "{}{}{}",
102
1426
                    HOME_SUBSTITUTION,
103
1426
                    std::path::MAIN_SEPARATOR,
104
1426
                    display_lossy(suffix),
105
1426
                );
106
873
            }
107
        }
108

            
109
        // Didn't match any homedir.
110

            
111
873
        display_lossy(self.0).fmt(f)
112
2299
    }
113
}
114

            
115
#[cfg(test)]
116
mod test {
117
    // @@ begin test lint list maintained by maint/add_warning @@
118
    #![allow(clippy::bool_assert_comparison)]
119
    #![allow(clippy::clone_on_copy)]
120
    #![allow(clippy::dbg_macro)]
121
    #![allow(clippy::mixed_attributes_style)]
122
    #![allow(clippy::print_stderr)]
123
    #![allow(clippy::print_stdout)]
124
    #![allow(clippy::single_char_pattern)]
125
    #![allow(clippy::unwrap_used)]
126
    #![allow(clippy::unchecked_duration_subtraction)]
127
    #![allow(clippy::useless_vec)]
128
    #![allow(clippy::needless_pass_by_value)]
129
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
130
    use super::*;
131

            
132
    #[test]
133
    fn no_change() {
134
        // This is not your home directory
135
        let path = PathBuf::from("/completely/untoucha8le");
136
        assert_eq!(path.anonymize_home().to_string(), path.to_string_lossy());
137
    }
138

            
139
    fn check_with_home(homedir: &Path) {
140
        let arti_conf = homedir.join("here").join("is").join("a").join("path");
141

            
142
        #[cfg(target_family = "windows")]
143
        assert_eq!(
144
            arti_conf.anonymize_home().to_string(),
145
            "%UserProfile%\\here\\is\\a\\path"
146
        );
147

            
148
        #[cfg(not(target_family = "windows"))]
149
        assert_eq!(
150
            arti_conf.anonymize_home().to_string(),
151
            "${HOME}/here/is/a/path"
152
        );
153
    }
154

            
155
    #[test]
156
    fn in_home() {
157
        if let Some(home) = dirs::home_dir() {
158
            check_with_home(&home);
159
        }
160
    }
161

            
162
    #[test]
163
    fn in_canonical_home() {
164
        if let Some(canonical_home) = dirs::home_dir()
165
            .map(std::fs::canonicalize)
166
            .transpose()
167
            .ok()
168
            .flatten()
169
        {
170
            check_with_home(&canonical_home);
171
        }
172
    }
173
}