use std::borrow::Cow;
use std::collections::HashSet;
use crate::scanner::substitute::Substituter;
use crate::scanner::{
Direction, Disposition, HoldBack, ScanCtx, ScanResult, Scanner, ScannerId, Verdict,
};
#[derive(Debug, Clone)]
pub struct RestoreScanner {
id: ScannerId,
types: HashSet<String>,
}
impl RestoreScanner {
#[must_use]
pub fn for_pii() -> Self {
let types = [
"EMAIL",
"PHONE",
"US_SSN",
"IP_ADDRESS",
"CREDIT_CARD",
"IBAN",
]
.into_iter()
.map(str::to_owned)
.collect();
Self {
id: ScannerId("native:pii-restore"),
types,
}
}
#[must_use]
pub fn for_types(id: ScannerId, types: impl IntoIterator<Item = String>) -> Self {
Self {
id,
types: types.into_iter().collect(),
}
}
fn sentinel_type(sentinel: &str) -> Option<&str> {
let inner = sentinel.strip_prefix("[REDACTED_")?.strip_suffix(']')?;
let without_nonce = inner.rsplit_once('_')?.0;
let ty = without_nonce.rsplit_once('_')?.0;
Some(ty)
}
}
impl Scanner for RestoreScanner {
fn id(&self) -> ScannerId {
self.id
}
fn direction(&self) -> Direction {
Direction::Output
}
fn disposition(&self) -> Disposition {
Disposition::Transform
}
fn scan<'a>(&self, text: &'a str, ctx: &mut ScanCtx) -> ScanResult<'a> {
let pairs: Vec<(String, String)> = ctx
.vault
.entries()
.filter(|(sentinel, _)| {
ctx.vault.is_restorable(sentinel)
&& Self::sentinel_type(sentinel).is_some_and(|ty| self.types.contains(ty))
})
.map(|(s, o)| (s.to_owned(), o.to_owned()))
.collect();
if pairs.is_empty() {
return Ok(Verdict::transformed(Cow::Borrowed(text)));
}
let sub = Substituter::from_pairs(pairs.iter().map(|(s, o)| (s.as_str(), o.as_str())));
let (restored, counts) = sub.substitute_with_counting(text, &ctx.restore_encoder);
for (sentinel, n) in counts {
if let Some(ty) = Self::sentinel_type(&sentinel) {
ctx.vault.note_restored(ty, n);
}
}
Ok(Verdict::transformed(Cow::Owned(restored)))
}
fn hold_back(&self) -> HoldBack {
HoldBack::TokenBoundary
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "tests assert on known-good values"
)]
use crate::scanners::{PiiScanner, SecretScanner};
use super::*;
#[test]
fn restores_pii_sentinel() {
let pii = PiiScanner::new();
let restore = RestoreScanner::for_pii();
let mut ctx = ScanCtx::new();
let redacted = pii
.scan("mail alice@x.com", &mut ctx)
.unwrap()
.text
.into_owned();
let out = restore.scan(&redacted, &mut ctx).unwrap();
assert_eq!(out.text, "mail alice@x.com");
}
#[test]
fn does_not_restore_secret_sentinel() {
let secrets = SecretScanner::new();
let restore = RestoreScanner::for_pii();
let mut ctx = ScanCtx::new();
let redacted = secrets
.scan("key AKIAIOSFODNN7EXAMPLE", &mut ctx)
.unwrap()
.text
.into_owned();
let out = restore.scan(&redacted, &mut ctx).unwrap();
assert!(out.text.contains("[REDACTED_AWS_ACCESS_KEY_1_"));
assert!(!out.text.contains("AKIAIOSFODNN7EXAMPLE"));
}
#[test]
fn parses_multi_underscore_type() {
assert_eq!(
RestoreScanner::sentinel_type("[REDACTED_AWS_ACCESS_KEY_2_abcd1234]"),
Some("AWS_ACCESS_KEY")
);
assert_eq!(
RestoreScanner::sentinel_type("[REDACTED_US_SSN_1_deadbeef]"),
Some("US_SSN")
);
}
}