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.
78use std::{
9 collections::HashSet,
10 fmt::Display,
11 path::{Path, PathBuf},
12};
1314use once_cell::sync::Lazy;
1516/// Cached value of our observed home directory.
17static HOMEDIRS: Lazy<Vec<PathBuf>> = Lazy::new(default_homedirs);
1819/// Return a list of home directories in official and canonical forms.
20fn default_homedirs() -> Vec<PathBuf> {
21if let Some(basic_home) = dirs::home_dir() {
22// Build as a HashSet, to de-duplicate.
23let mut homedirs = HashSet::new();
2425// We like our home directory.
26homedirs.insert(basic_home.clone());
27// We like the canonical version of our home directory.
28if let Ok(canonical) = std::fs::canonicalize(&basic_home) {
29 homedirs.insert(canonical);
30 }
31// We like the version of our home directory generated by `ResolvePath`.
32if let Ok(rp) = crate::walk::ResolvePath::new(basic_home) {
33let (mut p, rest) = rp.into_result();
34 p.extend(rest);
35 homedirs.insert(p);
36 }
3738 homedirs.into_iter().collect()
39 } else {
40vec![]
41 }
42}
4344/// The string that we use to represent our home directory in a compacted path.
45const HOME_SUBSTITUTION: &str = {
46if cfg!(target_family = "windows") {
47"%UserProfile%"
48} else {
49"${HOME}"
50}
51};
5253/// An extension trait for [`Path`].
54pub 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 /// ```
72fn anonymize_home(&self) -> AnonHomePath<'_>;
73}
7475impl PathExt for Path {
76fn anonymize_home(&self) -> AnonHomePath<'_> {
77 AnonHomePath(self)
78 }
79}
8081/// A wrapper for `Path` which, when displayed, replaces the home directory with
82/// a symbolic reference.
83#[derive(Debug, Clone)]
84pub struct AnonHomePath<'a>(&'a Path);
8586impl<'a> std::fmt::Display for AnonHomePath<'a> {
87fn 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)]
90fn display_lossy(p: &Path) -> impl Display + '_ {
91 p.display()
92 }
9394// 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.
97for home in HOMEDIRS.iter() {
98if let Ok(suffix) = self.0.strip_prefix(home) {
99return write!(
100 f,
101"{}{}{}",
102 HOME_SUBSTITUTION,
103 std::path::MAIN_SEPARATOR,
104 display_lossy(suffix),
105 );
106 }
107 }
108109// Didn't match any homedir.
110111display_lossy(self.0).fmt(f)
112 }
113}
114115#[cfg(test)]
116mod 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 @@ -->
130use super::*;
131132#[test]
133fn no_change() {
134// This is not your home directory
135let path = PathBuf::from("/completely/untoucha8le");
136assert_eq!(path.anonymize_home().to_string(), path.to_string_lossy());
137 }
138139fn check_with_home(homedir: &Path) {
140let arti_conf = homedir.join("here").join("is").join("a").join("path");
141142#[cfg(target_family = "windows")]
143assert_eq!(
144 arti_conf.anonymize_home().to_string(),
145"%UserProfile%\\here\\is\\a\\path"
146);
147148#[cfg(not(target_family = "windows"))]
149assert_eq!(
150 arti_conf.anonymize_home().to_string(),
151"${HOME}/here/is/a/path"
152);
153 }
154155#[test]
156fn in_home() {
157if let Some(home) = dirs::home_dir() {
158 check_with_home(&home);
159 }
160 }
161162#[test]
163fn in_canonical_home() {
164if 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}