use std::collections::BTreeMap;
use std::sync::OnceLock;
use regex::Regex;
#[derive(Debug, Clone, Copy)]
pub struct RedactionClass {
pub name: &'static str,
pub pattern: &'static str,
pub keeps_leading_group: bool,
}
pub const REDACTION_CLASSES: &[RedactionClass] = &[
RedactionClass {
name: "absolute-user-paths",
pattern: r"/Users/(dasboe|bjornbosenberg)",
keeps_leading_group: false,
},
RedactionClass {
name: "secrets",
pattern: r"(-----BEGIN [A-Z]+ PRIVATE KEY|ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|gho_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{24,}|sk-[A-Za-z0-9]{24,})",
keeps_leading_group: false,
},
RedactionClass {
name: "private-infra",
pattern: r"(railway\.app|\.up\.railway\.app|railway\.json)",
keeps_leading_group: false,
},
RedactionClass {
name: "internal-refs",
pattern: r"(dev/plans|dev/strategy|dev/ci|LAUNCH\.md)",
keeps_leading_group: false,
},
RedactionClass {
name: "stale-product-name",
pattern: r"\b[Mm]emgno\b",
keeps_leading_group: false,
},
RedactionClass {
name: "excluded-private-dirs",
pattern: r#"(^|[[:space:]"'`(:,])(macos|websites|graph|inspector|local-ai)/"#,
keeps_leading_group: true,
},
RedactionClass {
name: "legacy-domain",
pattern: r"(mdgv\.io|dasboe/mdgv|dasboe\.github\.io)",
keeps_leading_group: false,
},
];
pub fn sentinel(class: &str) -> String {
format!("[redacted:{class}]")
}
fn compiled() -> &'static [(RedactionClass, Regex)] {
static COMPILED: OnceLock<Vec<(RedactionClass, Regex)>> = OnceLock::new();
COMPILED.get_or_init(|| {
REDACTION_CLASSES
.iter()
.map(|c| {
(
*c,
Regex::new(c.pattern).unwrap_or_else(|e| {
panic!("redaction class {} has an invalid pattern: {e}", c.name)
}),
)
})
.collect()
})
}
pub fn redact(text: &str) -> (String, BTreeMap<&'static str, usize>) {
let mut out = text.to_string();
let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
for (class, re) in compiled() {
let mut n = 0usize;
let replaced = re.replace_all(&out, |caps: ®ex::Captures| {
n += 1;
if class.keeps_leading_group {
format!(
"{}{}",
caps.get(1).map(|m| m.as_str()).unwrap_or(""),
sentinel(class.name)
)
} else {
sentinel(class.name)
}
});
if n > 0 {
out = replaced.into_owned();
counts.insert(class.name, n);
}
}
(out, counts)
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RedactionCount {
pub class: String,
pub count: usize,
}
pub fn tally(into: &mut BTreeMap<&'static str, usize>, counts: BTreeMap<&'static str, usize>) {
for (k, v) in counts {
*into.entry(k).or_insert(0) += v;
}
}
pub fn counts_to_list(counts: &BTreeMap<&'static str, usize>) -> Vec<RedactionCount> {
REDACTION_CLASSES
.iter()
.filter_map(|c| {
counts.get(c.name).map(|n| RedactionCount {
class: c.name.to_string(),
count: *n,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redacts_each_class_to_its_sentinel_and_keeps_the_rest() {
let input = format!(
"see {}/x.md and {} from {}/w; reads a {}/ path prefix",
["dev", "plans"].join("/"),
["mdgv", "io"].join("."),
["/Users", "bjornbosenberg"].join("/"),
"graph"
);
let (out, counts) = redact(&input);
assert_eq!(
out,
"see [redacted:internal-refs]/x.md and [redacted:legacy-domain] from [redacted:absolute-user-paths]/w; reads a [redacted:excluded-private-dirs] path prefix"
);
let list = counts_to_list(&counts);
assert_eq!(
list.iter()
.map(|c| (c.class.as_str(), c.count))
.collect::<Vec<_>>(),
vec![
("absolute-user-paths", 1),
("internal-refs", 1),
("excluded-private-dirs", 1),
("legacy-domain", 1)
]
);
let (clean, none) = redact("an ordinary note about engine-graph/ and memstead.io");
assert_eq!(
clean,
"an ordinary note about engine-graph/ and memstead.io"
);
assert!(none.is_empty());
}
#[test]
fn vocabulary_equals_the_leak_scan_classes() {
let script =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../scripts/leak-scan.sh");
let text = std::fs::read_to_string(&script)
.unwrap_or_else(|e| panic!("read {}: {e}", script.display()));
let re = Regex::new(r#"(?m)^scan\s+"([a-z-]+)"\s+'((?:[^']|'"'"')+)'"#).unwrap();
let scanned: Vec<(String, String)> = re
.captures_iter(&text)
.map(|c| (c[1].to_string(), c[2].replace("'\"'\"'", "'")))
.collect();
assert!(
!scanned.is_empty(),
"no scan lines parsed from {}",
script.display()
);
let ours: Vec<(String, String)> = REDACTION_CLASSES
.iter()
.map(|c| (c.name.to_string(), c.pattern.to_string()))
.collect();
for (name, pattern) in &scanned {
let mine = ours.iter().find(|(n, _)| n == name).unwrap_or_else(|| {
panic!(
"leak-scan class `{name}` is not in the engine's redaction vocabulary (ops/redaction.rs)"
)
});
assert_eq!(
&mine.1, pattern,
"class `{name}`: the engine's pattern differs from the leak scan's"
);
}
for (name, _) in &ours {
assert!(
scanned.iter().any(|(n, _)| n == name),
"engine redaction class `{name}` has no leak-scan `scan` line"
);
}
assert_eq!(scanned.len(), ours.len());
}
}