1use crate::{
9 helpers::{FormattedDuration, convert_rel_path_to_forward_slash},
10 list::RustBuildMeta,
11};
12use camino::{Utf8Path, Utf8PathBuf};
13use regex::Regex;
14use std::{
15 collections::BTreeMap,
16 fmt,
17 sync::{Arc, LazyLock},
18 time::Duration,
19};
20
21static CRATE_NAME_HASH_REGEX: LazyLock<Regex> =
22 LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9_-]+)-[a-f0-9]{16}$").unwrap());
23static TARGET_DIR_REDACTION: &str = "<target-dir>";
24static FILE_COUNT_REDACTION: &str = "<file-count>";
25static DURATION_REDACTION: &str = "<duration>";
26
27#[derive(Clone, Debug)]
32pub struct Redactor {
33 kind: Arc<RedactorKind>,
34}
35
36impl Redactor {
37 pub fn noop() -> Self {
39 Self::new_with_kind(RedactorKind::Noop)
40 }
41
42 fn new_with_kind(kind: RedactorKind) -> Self {
43 Self {
44 kind: Arc::new(kind),
45 }
46 }
47
48 pub fn build_active<State>(build_meta: &RustBuildMeta<State>) -> RedactorBuilder {
52 let mut redactions = Vec::new();
53
54 let linked_path_redactions =
55 build_linked_path_redactions(build_meta.linked_paths.keys().map(|p| p.as_ref()));
56
57 for (source, replacement) in linked_path_redactions {
59 redactions.push(Redaction::Path {
60 path: build_meta.target_directory.join(&source),
61 replacement: format!("{TARGET_DIR_REDACTION}/{replacement}"),
62 });
63 redactions.push(Redaction::Path {
64 path: source,
65 replacement,
66 });
67 }
68
69 redactions.push(Redaction::Path {
72 path: build_meta.target_directory.clone(),
73 replacement: "<target-dir>".to_string(),
74 });
75
76 RedactorBuilder { redactions }
77 }
78
79 pub fn redact_path<'a>(&self, orig: &'a Utf8Path) -> RedactorOutput<&'a Utf8Path> {
81 for redaction in self.kind.iter_redactions() {
82 match redaction {
83 Redaction::Path { path, replacement } => {
84 if let Ok(suffix) = orig.strip_prefix(path) {
85 if suffix.as_str().is_empty() {
86 return RedactorOutput::Redacted(replacement.clone());
87 } else {
88 let path = Utf8PathBuf::from(format!("{replacement}/{suffix}"));
91 return RedactorOutput::Redacted(
92 convert_rel_path_to_forward_slash(&path).into(),
93 );
94 }
95 }
96 }
97 }
98 }
99
100 RedactorOutput::Unredacted(orig)
101 }
102
103 pub fn redact_file_count(&self, orig: usize) -> RedactorOutput<usize> {
105 if self.kind.is_active() {
106 RedactorOutput::Redacted(FILE_COUNT_REDACTION.to_string())
107 } else {
108 RedactorOutput::Unredacted(orig)
109 }
110 }
111
112 pub(crate) fn redact_duration(&self, orig: Duration) -> RedactorOutput<FormattedDuration> {
114 if self.kind.is_active() {
115 RedactorOutput::Redacted(DURATION_REDACTION.to_string())
116 } else {
117 RedactorOutput::Unredacted(FormattedDuration(orig))
118 }
119 }
120}
121
122#[derive(Debug)]
126pub struct RedactorBuilder {
127 redactions: Vec<Redaction>,
128}
129
130impl RedactorBuilder {
131 pub fn with_path(mut self, path: Utf8PathBuf, replacement: String) -> Self {
133 self.redactions.push(Redaction::Path { path, replacement });
134 self
135 }
136
137 pub fn build(self) -> Redactor {
139 Redactor::new_with_kind(RedactorKind::Active {
140 redactions: self.redactions,
141 })
142 }
143}
144
145#[derive(Debug)]
147pub enum RedactorOutput<T> {
148 Unredacted(T),
150
151 Redacted(String),
153}
154
155impl<T: fmt::Display> fmt::Display for RedactorOutput<T> {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 RedactorOutput::Unredacted(value) => value.fmt(f),
159 RedactorOutput::Redacted(replacement) => replacement.fmt(f),
160 }
161 }
162}
163
164#[derive(Debug)]
165enum RedactorKind {
166 Noop,
167 Active {
168 redactions: Vec<Redaction>,
170 },
171}
172
173impl RedactorKind {
174 fn is_active(&self) -> bool {
175 matches!(self, Self::Active { .. })
176 }
177
178 fn iter_redactions(&self) -> impl Iterator<Item = &Redaction> {
179 match self {
180 Self::Active { redactions } => redactions.iter(),
181 Self::Noop => [].iter(),
182 }
183 }
184}
185
186#[derive(Debug)]
188enum Redaction {
189 Path {
191 path: Utf8PathBuf,
193
194 replacement: String,
196 },
197}
198
199fn build_linked_path_redactions<'a>(
200 linked_paths: impl Iterator<Item = &'a Utf8Path>,
201) -> BTreeMap<Utf8PathBuf, String> {
202 let mut linked_path_redactions = BTreeMap::new();
204
205 for linked_path in linked_paths {
206 let mut source = Utf8PathBuf::new();
212 let mut replacement = ReplacementBuilder::new();
213
214 for elem in linked_path {
215 if let Some(captures) = CRATE_NAME_HASH_REGEX.captures(elem) {
216 let crate_name = captures.get(1).expect("regex had one capture");
218 source.push(elem);
219 replacement.push(&format!("<{}-hash>", crate_name.as_str()));
220 linked_path_redactions.insert(source, replacement.into_string());
221 break;
222 } else {
223 source.push(elem);
225 replacement.push(elem);
226 }
227
228 }
230 }
231
232 linked_path_redactions
233}
234
235#[derive(Debug)]
236struct ReplacementBuilder {
237 replacement: String,
238}
239
240impl ReplacementBuilder {
241 fn new() -> Self {
242 Self {
243 replacement: String::new(),
244 }
245 }
246
247 fn push(&mut self, s: &str) {
248 if self.replacement.is_empty() {
249 self.replacement.push_str(s);
250 } else {
251 self.replacement.push('/');
252 self.replacement.push_str(s);
253 }
254 }
255
256 fn into_string(self) -> String {
257 self.replacement
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_redact_path() {
267 let abs_path = make_abs_path();
268 let redactor = Redactor::new_with_kind(RedactorKind::Active {
269 redactions: vec![
270 Redaction::Path {
271 path: "target/debug".into(),
272 replacement: "<target-debug>".to_string(),
273 },
274 Redaction::Path {
275 path: "target".into(),
276 replacement: "<target-dir>".to_string(),
277 },
278 Redaction::Path {
279 path: abs_path.clone(),
280 replacement: "<abs-target>".to_string(),
281 },
282 ],
283 });
284
285 let examples: &[(Utf8PathBuf, &str)] = &[
286 ("target/foo".into(), "<target-dir>/foo"),
287 ("target/debug/bar".into(), "<target-debug>/bar"),
288 ("target2/foo".into(), "target2/foo"),
289 (
290 ["target", "foo", "bar"].iter().collect(),
293 "<target-dir>/foo/bar",
294 ),
295 (abs_path.clone(), "<abs-target>"),
296 (abs_path.join("foo"), "<abs-target>/foo"),
297 ];
298
299 for (orig, expected) in examples {
300 assert_eq!(
301 redactor.redact_path(orig).to_string(),
302 *expected,
303 "redacting {orig:?}"
304 );
305 }
306 }
307
308 #[cfg(unix)]
309 fn make_abs_path() -> Utf8PathBuf {
310 "/path/to/target".into()
311 }
312
313 #[cfg(windows)]
314 fn make_abs_path() -> Utf8PathBuf {
315 "C:\\path\\to\\target".into()
316 }
318}