use super::edit::{Edit, chain};
use super::redactor::REDACTED;
use super::values::{is_jwt, issuer_prefix_start};
use std::borrow::Cow;
const PEM_BEGIN: &str = "-----BEGIN ";
const PEM_END: &str = "-----END ";
pub(super) fn mask_standalone(input: &str) -> Cow<'_, str> {
chain(mask_pem_blocks(input), mask_tokens)
}
fn mask_tokens(input: &str) -> Cow<'_, str> {
let mut edit = Edit::new(input);
let mut token_start = None;
for (idx, ch) in input.char_indices() {
if is_token_char(ch) {
token_start.get_or_insert(idx);
continue;
}
if let Some(start) = token_start.take() {
mask_token(&input[start..idx], start, &mut edit);
}
}
if let Some(start) = token_start {
mask_token(&input[start..], start, &mut edit);
}
edit.finish()
}
fn is_token_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '+')
}
fn mask_token(token: &str, at: usize, edit: &mut Edit) {
if is_jwt(token) {
edit.replace(at, at + token.len(), REDACTED);
return;
}
if let Some(offset) = issuer_prefix_start(token) {
edit.replace(at + offset, at + token.len(), REDACTED);
}
}
fn mask_pem_blocks(input: &str) -> Cow<'_, str> {
if !input.contains(PEM_BEGIN) {
return Cow::Borrowed(input);
}
let mut edit = Edit::new(input);
let mut at = 0;
while let Some(begin) = input[at..].find(PEM_BEGIN) {
let after_begin = at + begin + PEM_BEGIN.len();
let Some(header_end) = input[after_begin..].find("-----") else {
break;
};
let body_start = after_begin + header_end + "-----".len();
let body_end = input[body_start..]
.find(PEM_END)
.map_or(input.len(), |e| body_start + e);
if body_end > body_start {
edit.replace(body_start, body_end, REDACTED);
}
at = body_end;
}
edit.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vendor_prefixed_tokens_are_masked_without_a_key() {
assert_eq!(
mask_standalone("retrying with sk-live-4242deadbeef4242 after 401"),
"retrying with [redacted] after 401"
);
assert_eq!(
mask_standalone("(ghp_abcdefghijklmnopqrstuvwxyz0123456789)"),
"([redacted])"
);
}
#[test]
fn jwts_are_masked_wherever_they_appear() {
let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVPmB92K27uhbUJU1p1r";
let line = format!("GET /v1/x?t={jwt} -> 401");
let out = mask_standalone(&line);
assert!(!out.contains(jwt), "{out}");
}
#[test]
fn ordinary_diagnostics_are_left_alone() {
for input in [
"build_id=9f8e7d6c5b4a39281706",
"thread 'main' panicked at src/lib.rs:42",
"page 828 checksum mismatch",
"[redacted]",
] {
assert_eq!(mask_standalone(input), input, "{input}");
}
}
#[test]
fn pem_bodies_are_masked_and_the_markers_kept() {
let pem = "key:\n-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAK\nCAQEA\n-----END RSA PRIVATE KEY-----\ndone";
let out = mask_standalone(pem);
assert!(!out.contains("MIIEpQIBAAK"), "{out}");
assert!(out.contains("-----BEGIN RSA PRIVATE KEY-----"), "{out}");
assert!(out.contains("-----END RSA PRIVATE KEY-----"), "{out}");
assert!(out.ends_with("done"), "{out}");
}
#[test]
fn an_unterminated_pem_block_is_masked_to_the_end() {
let out = mask_standalone("-----BEGIN PRIVATE KEY-----\nMIIEpQIBAAKCAQEA");
assert!(!out.contains("MIIEpQIBAAKCAQEA"), "{out}");
}
}