const MAX_PREVIEW: usize = 8;
pub(crate) fn mask_secret_value(value: &str) -> String {
if value.is_empty() {
return "(empty)".to_string();
}
let length = value.encode_utf16().count();
if length < 3 {
return format!("({length} chars)");
}
let shown = MAX_PREVIEW.min(length / 2);
format!("{}… ({length} chars)", take_utf16(value, shown))
}
pub(crate) fn mask_within(context: &str, value: &str) -> String {
if value.is_empty() {
return context.to_string();
}
context.replace(value, &mask_secret_value(value))
}
fn take_utf16(value: &str, units: usize) -> &str {
let mut seen = 0;
for (offset, character) in value.char_indices() {
if seen >= units {
return &value[..offset];
}
seen += character.len_utf16();
}
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_value_is_named_rather_than_previewed() {
assert_eq!(mask_secret_value(""), "(empty)");
}
#[test]
fn a_value_too_short_to_preview_gives_only_its_length() {
assert_eq!(mask_secret_value("a"), "(1 chars)");
assert_eq!(mask_secret_value("ab"), "(2 chars)");
}
#[test]
fn the_preview_is_capped_at_half_the_length() {
assert_eq!(mask_secret_value("abcd"), "ab… (4 chars)");
assert_eq!(mask_secret_value("abcdef"), "abc… (6 chars)");
}
#[test]
fn the_preview_is_capped_at_eight_characters() {
let value = "a".repeat(200);
assert_eq!(mask_secret_value(&value), "aaaaaaaa… (200 chars)");
}
#[test]
fn every_occurrence_in_a_context_line_is_masked() {
let masked = mask_within("a=secret and again secret", "secret");
assert!(!masked.contains("secret and"), "{masked}");
assert_eq!(masked.matches("sec…").count(), 2, "{masked}");
}
#[test]
fn a_context_without_the_value_is_unchanged() {
assert_eq!(mask_within("nothing here", "absent"), "nothing here");
assert_eq!(mask_within("empty value", ""), "empty value");
}
#[test]
fn a_value_too_short_to_preview_discloses_nothing_but_its_length() {
for value in ["a", "x", "1", "ab", "xy", "()"] {
let length = value.encode_utf16().count();
assert_eq!(mask_secret_value(value), format!("({length} chars)"));
}
}
#[test]
fn no_preview_ever_contains_its_whole_value() {
for length in 3..=300 {
let value: String = std::iter::repeat_n('x', length).collect();
let preview = mask_secret_value(&value);
assert!(
!preview.contains(&value),
"a {length}-character value leaked through its preview: {preview}"
);
let context = format!("KEY={value}");
let masked = mask_within(&context, &value);
assert!(
!masked.contains(&value),
"a {length}-character value leaked through its context line: {masked}"
);
}
}
#[test]
fn no_preview_leaks_a_varied_value() {
let alphabet: Vec<char> = "aB3xY7zQ9mK2pL5vN8wR4tS6/+=-_.".chars().collect();
for length in 3..=300 {
let value: String = (0..length).map(|i| alphabet[i % alphabet.len()]).collect();
assert!(!mask_secret_value(&value).contains(&value), "{length}");
let context = format!("KEY={value} trailing");
assert!(!mask_within(&context, &value).contains(&value), "{length}");
}
}
#[test]
fn multibyte_values_are_previewed_by_code_unit() {
let value = "ééééééééééééééé";
let preview = mask_secret_value(value);
assert_eq!(preview, "ééééééé… (15 chars)");
assert!(!preview.contains(value));
}
#[test]
fn an_astral_value_counts_in_utf16_units_like_the_extension() {
let value = "🎯🎯🎯🎯";
let preview = mask_secret_value(value);
assert!(preview.contains("(8 chars)"), "{preview}");
assert!(!preview.contains(value), "{preview}");
}
}