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 extend::ext;
15
use std::sync::LazyLock;
16

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

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

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

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

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

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

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

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

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

            
107
        // Didn't match any homedir.
108

            
109
820
        display_lossy(self.0).fmt(f)
110
2048
    }
111
}
112

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

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

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

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

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

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

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