Skip to main content

fs_mistrust/
anon_home.rs

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
8use std::{
9    collections::HashSet,
10    fmt::Display,
11    path::{Path, PathBuf},
12};
13
14use extend::ext;
15use std::sync::LazyLock;
16
17/// Cached value of our observed home directory.
18static HOMEDIRS: LazyLock<Vec<PathBuf>> = LazyLock::new(default_homedirs);
19
20/// Return a list of home directories in official and canonical forms.
21fn default_homedirs() -> Vec<PathBuf> {
22    if let Some(basic_home) = dirs::home_dir() {
23        // Build as a HashSet, to de-duplicate.
24        let mut homedirs = HashSet::new();
25
26        // We like our home directory.
27        homedirs.insert(basic_home.clone());
28        // We like the canonical version of our home directory.
29        if let Ok(canonical) = std::fs::canonicalize(&basic_home) {
30            homedirs.insert(canonical);
31        }
32        // We like the version of our home directory generated by `ResolvePath`.
33        if let Ok(rp) = crate::walk::ResolvePath::new(basic_home) {
34            let (mut p, rest) = rp.into_result();
35            p.extend(rest);
36            homedirs.insert(p);
37        }
38
39        homedirs.into_iter().collect()
40    } else {
41        vec![]
42    }
43}
44
45/// The string that we use to represent our home directory in a compacted path.
46const 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]
56pub 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    fn anonymize_home(&self) -> AnonHomePath<'_> {
75        AnonHomePath(self)
76    }
77}
78
79/// A wrapper for `Path` which, when displayed, replaces the home directory with
80/// a symbolic reference.
81#[derive(Debug, Clone)]
82pub struct AnonHomePath<'a>(&'a Path);
83
84impl<'a> std::fmt::Display for AnonHomePath<'a> {
85    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        fn display_lossy(p: &Path) -> impl Display + '_ {
89            p.display()
90        }
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        for home in HOMEDIRS.iter() {
96            if let Ok(suffix) = self.0.strip_prefix(home) {
97                return write!(
98                    f,
99                    "{}{}{}",
100                    HOME_SUBSTITUTION,
101                    std::path::MAIN_SEPARATOR,
102                    display_lossy(suffix),
103                );
104            }
105        }
106
107        // Didn't match any homedir.
108
109        display_lossy(self.0).fmt(f)
110    }
111}
112
113#[cfg(test)]
114mod 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}