use crate::{
helpers::{convert_rel_path_to_forward_slash, FormattedDuration},
list::RustBuildMeta,
};
use camino::{Utf8Path, Utf8PathBuf};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{collections::BTreeMap, fmt, sync::Arc, time::Duration};
static CRATE_NAME_HASH_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^([a-zA-Z0-9_-]+)-[a-f0-9]{16}$").unwrap());
static TARGET_DIR_REDACTION: &str = "<target-dir>";
static FILE_COUNT_REDACTION: &str = "<file-count>";
static DURATION_REDACTION: &str = "<duration>";
#[derive(Clone, Debug)]
pub struct Redactor {
kind: Arc<RedactorKind>,
}
impl Redactor {
pub fn noop() -> Self {
Self::new_with_kind(RedactorKind::Noop)
}
fn new_with_kind(kind: RedactorKind) -> Self {
Self {
kind: Arc::new(kind),
}
}
pub fn build_active<State>(build_meta: &RustBuildMeta<State>) -> RedactorBuilder {
let mut redactions = Vec::new();
let linked_path_redactions =
build_linked_path_redactions(build_meta.linked_paths.keys().map(|p| p.as_ref()));
for (source, replacement) in linked_path_redactions {
redactions.push(Redaction::Path {
path: build_meta.target_directory.join(&source),
replacement: format!("{TARGET_DIR_REDACTION}/{replacement}"),
});
redactions.push(Redaction::Path {
path: source,
replacement,
});
}
redactions.push(Redaction::Path {
path: build_meta.target_directory.clone(),
replacement: "<target-dir>".to_string(),
});
RedactorBuilder { redactions }
}
pub fn redact_path<'a>(&self, orig: &'a Utf8Path) -> RedactorOutput<&'a Utf8Path> {
for redaction in self.kind.iter_redactions() {
match redaction {
Redaction::Path { path, replacement } => {
if let Ok(suffix) = orig.strip_prefix(path) {
if suffix.as_str().is_empty() {
return RedactorOutput::Redacted(replacement.clone());
} else {
let path = Utf8PathBuf::from(format!("{replacement}/{suffix}"));
return RedactorOutput::Redacted(
convert_rel_path_to_forward_slash(&path).into(),
);
}
}
}
}
}
RedactorOutput::Unredacted(orig)
}
pub fn redact_file_count(&self, orig: usize) -> RedactorOutput<usize> {
if self.kind.is_active() {
RedactorOutput::Redacted(FILE_COUNT_REDACTION.to_string())
} else {
RedactorOutput::Unredacted(orig)
}
}
pub(crate) fn redact_duration(&self, orig: Duration) -> RedactorOutput<FormattedDuration> {
if self.kind.is_active() {
RedactorOutput::Redacted(DURATION_REDACTION.to_string())
} else {
RedactorOutput::Unredacted(FormattedDuration(orig))
}
}
}
#[derive(Debug)]
pub struct RedactorBuilder {
redactions: Vec<Redaction>,
}
impl RedactorBuilder {
pub fn with_path(mut self, path: Utf8PathBuf, replacement: String) -> Self {
self.redactions.push(Redaction::Path { path, replacement });
self
}
pub fn build(self) -> Redactor {
Redactor::new_with_kind(RedactorKind::Active {
redactions: self.redactions,
})
}
}
#[derive(Debug)]
pub enum RedactorOutput<T> {
Unredacted(T),
Redacted(String),
}
impl<T: fmt::Display> fmt::Display for RedactorOutput<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RedactorOutput::Unredacted(value) => value.fmt(f),
RedactorOutput::Redacted(replacement) => replacement.fmt(f),
}
}
}
#[derive(Debug)]
enum RedactorKind {
Noop,
Active {
redactions: Vec<Redaction>,
},
}
impl RedactorKind {
fn is_active(&self) -> bool {
matches!(self, Self::Active { .. })
}
fn iter_redactions(&self) -> impl Iterator<Item = &Redaction> {
match self {
Self::Active { redactions } => redactions.iter(),
Self::Noop => [].iter(),
}
}
}
#[derive(Debug)]
enum Redaction {
Path {
path: Utf8PathBuf,
replacement: String,
},
}
fn build_linked_path_redactions<'a>(
linked_paths: impl Iterator<Item = &'a Utf8Path>,
) -> BTreeMap<Utf8PathBuf, String> {
let mut linked_path_redactions = BTreeMap::new();
for linked_path in linked_paths {
let mut source = Utf8PathBuf::new();
let mut replacement = ReplacementBuilder::new();
for elem in linked_path {
if let Some(captures) = CRATE_NAME_HASH_REGEX.captures(elem) {
let crate_name = captures.get(1).expect("regex had one capture");
source.push(elem);
replacement.push(&format!("<{}-hash>", crate_name.as_str()));
linked_path_redactions.insert(source, replacement.into_string());
break;
} else {
source.push(elem);
replacement.push(elem);
}
}
}
linked_path_redactions
}
#[derive(Debug)]
struct ReplacementBuilder {
replacement: String,
}
impl ReplacementBuilder {
fn new() -> Self {
Self {
replacement: String::new(),
}
}
fn push(&mut self, s: &str) {
if self.replacement.is_empty() {
self.replacement.push_str(s);
} else {
self.replacement.push('/');
self.replacement.push_str(s);
}
}
fn into_string(self) -> String {
self.replacement
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_path() {
let abs_path = make_abs_path();
let redactor = Redactor::new_with_kind(RedactorKind::Active {
redactions: vec![
Redaction::Path {
path: "target/debug".into(),
replacement: "<target-debug>".to_string(),
},
Redaction::Path {
path: "target".into(),
replacement: "<target-dir>".to_string(),
},
Redaction::Path {
path: abs_path.clone(),
replacement: "<abs-target>".to_string(),
},
],
});
let examples: &[(Utf8PathBuf, &str)] = &[
("target/foo".into(), "<target-dir>/foo"),
("target/debug/bar".into(), "<target-debug>/bar"),
("target2/foo".into(), "target2/foo"),
(
["target", "foo", "bar"].iter().collect(),
"<target-dir>/foo/bar",
),
(abs_path.clone(), "<abs-target>"),
(abs_path.join("foo"), "<abs-target>/foo"),
];
for (orig, expected) in examples {
assert_eq!(
redactor.redact_path(orig).to_string(),
*expected,
"redacting {orig:?}"
);
}
}
#[cfg(unix)]
fn make_abs_path() -> Utf8PathBuf {
"/path/to/target".into()
}
#[cfg(windows)]
fn make_abs_path() -> Utf8PathBuf {
"C:\\path\\to\\target".into()
}
}