use super::edit::Edit;
use super::keys::{KeyKind, classify};
use super::redactor::REDACTED;
use super::values::is_credential_shaped;
use std::borrow::Cow;
const AUTH_SCHEMES: &[&str] = &[
"bearer",
"basic",
"digest",
"negotiate",
"ntlm",
"token",
"apikey",
"hoba",
"mutual",
"oauth",
"sso",
];
pub(super) fn mask(input: &str) -> Cow<'_, str> {
let bytes = input.as_bytes();
let mut edit = Edit::new(input);
let mut i = 0;
while i < bytes.len() {
if (bytes[i] == b'=' || bytes[i] == b':')
&& let Some(kind) = key_kind_ending_at(input, i)
{
i = mask_value(input, i + 1, kind, &mut edit);
continue;
}
i += 1;
while i < bytes.len() && (bytes[i] & 0xC0) == 0x80 {
i += 1;
}
}
edit.finish()
}
fn key_kind_ending_at(input: &str, sep: usize) -> Option<KeyKind> {
let b = input.as_bytes();
let mut end = sep;
while end > 0 && (b[end - 1] == b' ' || b[end - 1] == b'\t') {
end -= 1;
}
let quote = (end > 0 && (b[end - 1] == b'"' || b[end - 1] == b'\'')).then(|| b[end - 1]);
if quote.is_some() {
end -= 1;
}
let mut start = end;
while start > 0 && is_key_byte(b[start - 1]) {
start -= 1;
}
if start == end {
return None;
}
if let Some(q) = quote
&& (start == 0 || b[start - 1] != q)
{
return None;
}
match classify(&input[start..end]) {
KeyKind::Plain => None,
kind => Some(kind),
}
}
fn is_key_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.')
}
fn mask_value(input: &str, mut i: usize, kind: KeyKind, edit: &mut Edit) -> usize {
let b = input.as_bytes();
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
i += 1;
}
if i >= b.len() {
return i;
}
if input[i..].starts_with(REDACTED) {
return i + REDACTED.len();
}
if b[i] == b'"' || b[i] == b'\'' {
let (content_end, after) = quoted_extent(input, i);
return if mask_leaf(input, i + 1, content_end, kind, edit) {
after
} else {
i + 1
};
}
if kind == KeyKind::Strong
&& let Some(open) = group_open(input, i)
{
return mask_group(input, open, kind, edit);
}
let end = scheme_span(input, i).unwrap_or_else(|| token_end(input, i));
if mask_leaf(input, i, end, kind, edit) {
end
} else {
i
}
}
fn mask_group(input: &str, open: usize, kind: KeyKind, edit: &mut Edit) -> usize {
let b = input.as_bytes();
let mut i = open + 1;
let mut depth = 1u32;
while i < b.len() {
if input[i..].starts_with(REDACTED) {
i += REDACTED.len();
continue;
}
match b[i] {
b'"' | b'\'' => {
let (content_end, after) = quoted_extent(input, i);
mask_leaf(input, i + 1, content_end, kind, edit);
i = after;
}
b'(' | b'[' | b'{' => {
depth += 1;
i += 1;
}
b')' | b']' | b'}' => {
depth -= 1;
i += 1;
if depth == 0 {
return i;
}
}
c if is_leaf_byte(c) => {
let start = i;
while i < b.len() && is_leaf_byte(b[i]) {
i += 1;
}
if !introduces_a_value(input, i) {
mask_leaf(input, start, i, kind, edit);
}
}
_ => {
i += 1;
while i < b.len() && (b[i] & 0xC0) == 0x80 {
i += 1;
}
}
}
}
i
}
fn group_open(input: &str, i: usize) -> Option<usize> {
let b = input.as_bytes();
if matches!(b[i], b'(' | b'[' | b'{') {
return Some(i);
}
if !(b[i].is_ascii_alphabetic() || b[i] == b'_') {
return None;
}
let mut j = i;
while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
j += 1;
}
if j < b.len() && matches!(b[j], b'(' | b'[') {
return Some(j);
}
let mut k = j;
while k < b.len() && (b[k] == b' ' || b[k] == b'\t') {
k += 1;
}
(k > j && k < b.len() && b[k] == b'{').then_some(k)
}
fn introduces_a_value(input: &str, at: usize) -> bool {
let b = input.as_bytes();
let mut j = at;
while j < b.len() && (b[j] == b' ' || b[j] == b'\t') {
j += 1;
}
j < b.len() && matches!(b[j], b':' | b'(' | b'{')
}
fn is_leaf_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b'+' | b'/' | b'=' | b'~' | b'@')
}
fn quoted_extent(input: &str, quote_at: usize) -> (usize, usize) {
let b = input.as_bytes();
let quote = b[quote_at];
let mut j = quote_at + 1;
while j < b.len() && b[j] != quote {
j += if b[j] == b'\\' && j + 1 < b.len() {
2
} else {
1
};
}
let end = j.min(b.len());
(end, if end < b.len() { end + 1 } else { end })
}
fn mask_leaf(input: &str, start: usize, end: usize, kind: KeyKind, edit: &mut Edit) -> bool {
let value = &input[start..end];
if value == REDACTED || !masks(value, kind) {
return false;
}
edit.replace(start, end, REDACTED);
true
}
fn scheme_span(input: &str, i: usize) -> Option<usize> {
let b = input.as_bytes();
let mut word_end = i;
while word_end < b.len() && (b[word_end].is_ascii_alphanumeric() || b[word_end] == b'-') {
word_end += 1;
}
let word = &input[i..word_end];
if !AUTH_SCHEMES.iter().any(|s| word.eq_ignore_ascii_case(s)) {
return None;
}
let mut j = word_end;
while j < b.len() && (b[j] == b' ' || b[j] == b'\t') {
j += 1;
}
if j == word_end || j >= b.len() {
return None;
}
let credential_end = token_end(input, j);
(credential_end > j).then_some(credential_end)
}
fn token_end(input: &str, i: usize) -> usize {
let b = input.as_bytes();
let mut j = i;
while j < b.len() && !is_structural(b[j]) && b[j] != b' ' && b[j] != b'\t' {
j += 1;
}
j
}
fn is_structural(b: u8) -> bool {
matches!(b, b',' | b';' | b'&' | b'}' | b')' | b']' | b'\n' | b'\r')
}
fn masks(value: &str, kind: KeyKind) -> bool {
!value.is_empty() && (kind != KeyKind::Weak || is_credential_shaped(value))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_key_is_read_through_quotes_and_spacing() {
assert_eq!(mask(r#"{"api_key":"x"}"#), r#"{"api_key":"[redacted]"}"#);
assert_eq!(mask("token = x"), "token = [redacted]");
assert_eq!(mask("Authorization : x"), "Authorization : [redacted]");
}
#[test]
fn a_url_scheme_is_not_a_key() {
assert_eq!(
mask("see https://example.com/x"),
"see https://example.com/x"
);
}
#[test]
fn an_empty_value_stays_empty() {
assert_eq!(mask("token="), "token=");
assert_eq!(mask(r#"token="""#), r#"token="""#);
assert_eq!(mask("token=, port=1"), "token=, port=1");
}
#[test]
fn deeply_wrapped_values_are_still_reached_without_recursing() {
let deep = format!("token: {}\"x\"{}", "S(".repeat(4096), ")".repeat(4096));
let out = mask(&deep);
assert!(
!out.contains("\"x\""),
"the payload must not survive nesting"
);
}
#[test]
fn multibyte_text_is_copied_through_intact() {
assert_eq!(mask("café → token=abc"), "café → token=[redacted]");
assert_eq!(mask("página=1"), "página=1");
}
}