1use std::{
9 collections::HashSet,
10 fmt::Display,
11 path::{Path, PathBuf},
12};
13
14use std::sync::LazyLock;
15
16static HOMEDIRS: LazyLock<Vec<PathBuf>> = LazyLock::new(default_homedirs);
18
19fn default_homedirs() -> Vec<PathBuf> {
21 if let Some(basic_home) = dirs::home_dir() {
22 let mut homedirs = HashSet::new();
24
25 homedirs.insert(basic_home.clone());
27 if let Ok(canonical) = std::fs::canonicalize(&basic_home) {
29 homedirs.insert(canonical);
30 }
31 if let Ok(rp) = crate::walk::ResolvePath::new(basic_home) {
33 let (mut p, rest) = rp.into_result();
34 p.extend(rest);
35 homedirs.insert(p);
36 }
37
38 homedirs.into_iter().collect()
39 } else {
40 vec![]
41 }
42}
43
44const HOME_SUBSTITUTION: &str = {
46 if cfg!(target_family = "windows") {
47 "%UserProfile%"
48 } else {
49 "${HOME}"
50 }
51};
52
53pub trait PathExt {
55 fn anonymize_home(&self) -> AnonHomePath<'_>;
73}
74
75impl PathExt for Path {
76 fn anonymize_home(&self) -> AnonHomePath<'_> {
77 AnonHomePath(self)
78 }
79}
80
81#[derive(Debug, Clone)]
84pub struct AnonHomePath<'a>(&'a Path);
85
86impl<'a> std::fmt::Display for AnonHomePath<'a> {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 #[allow(clippy::disallowed_methods)]
90 fn display_lossy(p: &Path) -> impl Display + '_ {
91 p.display()
92 }
93
94 for home in HOMEDIRS.iter() {
98 if let Ok(suffix) = self.0.strip_prefix(home) {
99 return write!(
100 f,
101 "{}{}{}",
102 HOME_SUBSTITUTION,
103 std::path::MAIN_SEPARATOR,
104 display_lossy(suffix),
105 );
106 }
107 }
108
109 display_lossy(self.0).fmt(f)
112 }
113}
114
115#[cfg(test)]
116mod test {
117 #![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 use super::*;
131
132 #[test]
133 fn no_change() {
134 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}