Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions src/response/problem_json.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use alloc::{
borrow::Cow,
collections::BTreeMap,
string::{String, ToString},
vec::Vec
string::{String, ToString}
};
use core::{fmt::Write, net::IpAddr};

Expand Down Expand Up @@ -598,21 +597,17 @@ fn mask_last4_field_value(value: &FieldValue) -> Option<String> {
}

fn mask_last4(value: &str) -> String {
let chars: Vec<char> = value.chars().collect();
let total = chars.len();
let chars = value.chars();
let total = chars.clone().count();
if total == 0 {
return String::new();
}

let keep = if total <= 4 { 1 } else { 4 };
let mask_len = total.saturating_sub(keep);
let mut masked = String::with_capacity(value.len());
for _ in 0..mask_len {
masked.push('*');
}
for ch in chars.iter().skip(mask_len) {
masked.push(*ch);
}
masked.extend(core::iter::repeat_n('*', mask_len));
masked.extend(chars.skip(mask_len));
masked
}

Expand Down Expand Up @@ -1080,6 +1075,34 @@ mod tests {
}
}

#[test]
fn last4_metadata_handles_multibyte_suffix() {
let multibyte = "💳💳💳💳💳💳";
let err = AppError::internal("oops").with_field(
crate::field::str("emoji", multibyte).with_redaction(FieldRedaction::Last4)
);
let problem = ProblemJson::from_ref(&err);
let metadata = problem.metadata.expect("metadata");
let value = metadata.0.get("emoji").expect("emoji field");
match value {
ProblemMetadataValue::String(text) => {
let total = multibyte.chars().count();
let keep = if total <= 4 { 1 } else { 4 };
let expected_mask_len = total.saturating_sub(keep);
let expected_suffix: String = multibyte.chars().skip(expected_mask_len).collect();

assert!(text.ends_with(&expected_suffix));
assert!(text.chars().take(expected_mask_len).all(|c| c == '*'));
assert_eq!(
text.chars().filter(|c| *c == '*').count(),
expected_mask_len
);
assert_eq!(text.chars().count(), multibyte.chars().count());
}
other => panic!("unexpected metadata value: {other:?}")
}
}

#[test]
fn problem_json_serialization_masks_sensitive_metadata() {
let secret = "super-secret";
Expand Down
Loading