///|
fn last_path_component(path : String) -> String {
let mut start = 0
for i = 0; i < path.length(); i = i + 1 {
let c = path[i].to_int()
if c == 46 || c == 47 || c == 92 || c == 91 || c == 93 {
start = i + 1
}
}
ascii_lower(path[start:].to_owned())
}
///|
fn path_is_sensitive(path : String) -> Bool {
sensitive_key(last_path_component(path))
}
///|
fn redact_with_findings(
text : String,
findings : Array[Finding],
style : RedactionStyle,
) -> String {
if findings.length() == 0 {
return text
}
let out = StringBuilder()
let mut cursor = 0
for finding in findings {
out.write_string(text[cursor:finding.start].to_owned())
out.write_string(replacement(text, finding, style))
cursor = finding.end
}
out.write_string(text[cursor:].to_owned())
out.to_string()
}
///|
/// Redact already-parsed structured fields. Sensitive paths are fail-closed:
/// their complete value is replaced even when its shape matches no detector.
/// Other fields use content scanning. Paths are retained for caller-side joins.
pub fn redact_fields(
fields : Array[StructuredField],
style? : RedactionStyle = Typed,
policy? : ScanPolicy = ScanPolicy::standard(),
) -> StructuredResult {
let output : Array[StructuredFieldResult] = []
let mut changed_fields = 0
let mut count = 0
for field in fields {
let sensitive_path = path_is_sensitive(field.path)
let findings = if sensitive_path && field.value.length() > 0 {
[
{
kind: CredentialAssignment,
start: 0,
end: field.value.length(),
confidence: High,
},
]
} else {
scan(field.value, policy~)
}
let changed = findings.length() > 0
if changed {
changed_fields += 1
}
count += findings.length()
output.push({
path: field.path,
value: redact_with_findings(field.value, findings, style),
findings,
changed,
sensitive_path,
})
}
{ fields: output, changed_fields, findings: count, }
}
///|
/// Redact structured fields under a full configuration, enabling
/// post-0.1.0 families for callers that normalize records themselves.
pub fn redact_fields_with_config(
fields : Array[StructuredField],
config : ScanConfig,
style? : RedactionStyle = Typed,
) -> StructuredResult {
let output : Array[StructuredFieldResult] = []
let mut changed_fields = 0
let mut count = 0
for field in fields {
let sensitive_path = path_is_sensitive(field.path)
let findings = if sensitive_path && field.value.length() > 0 {
[
{
kind: CredentialAssignment,
start: 0,
end: field.value.length(),
confidence: High,
},
]
} else {
scan_with_config(field.value, config)
}
let changed = findings.length() > 0
if changed {
changed_fields += 1
}
count += findings.length()
output.push({
path: field.path,
value: redact_with_findings(field.value, findings, style),
findings,
changed,
sensitive_path,
})
}
{ fields: output, changed_fields, findings: count, }
}
///|
/// Redact text with built-in and caller-supplied exact-value rules.
pub fn redact_with_rules(
text : String,
rules : Array[CustomRule],
style? : RedactionStyle = Typed,
policy? : ScanPolicy = ScanPolicy::standard(),
) -> RedactionResult {
let findings = scan_with_rules(text, rules, policy~)
{
text: redact_with_findings(text, findings, style),
findings,
changed: findings.length() > 0,
}
}