use alloc::string::{String, ToString};
use core::{fmt::Write, iter::repeat_n, str::from_utf8};
use itoa::Buffer as IntegerBuffer;
use ryu::Buffer as FloatBuffer;
use sha2::{Digest, Sha256};
use super::{duration_to_string, metadata::FieldValue};
pub(crate) const REDACTED_PLACEHOLDER: &str = "[REDACTED]";
struct StackBuffer<const N: usize> {
buf: [u8; N],
len: usize
}
impl<const N: usize> StackBuffer<N> {
const fn new() -> Self {
Self {
buf: [0; N],
len: 0
}
}
fn as_bytes(&self) -> &[u8] {
&self.buf[..self.len]
}
fn as_str(&self) -> Option<&str> {
from_utf8(self.as_bytes()).ok()
}
}
impl<const N: usize> Write for StackBuffer<N> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let remaining = N.saturating_sub(self.len);
if s.len() > remaining {
return Err(core::fmt::Error);
}
self.buf[self.len..self.len + s.len()].copy_from_slice(s.as_bytes());
self.len += s.len();
Ok(())
}
}
pub(crate) fn hash_field_value(value: &FieldValue) -> String {
let mut hasher = Sha256::new();
match value {
FieldValue::Str(value) => hasher.update(value.as_ref().as_bytes()),
FieldValue::I64(value) => {
let mut buffer = IntegerBuffer::new();
hasher.update(buffer.format(*value).as_bytes());
}
FieldValue::U64(value) => {
let mut buffer = IntegerBuffer::new();
hasher.update(buffer.format(*value).as_bytes());
}
FieldValue::F64(value) => hasher.update(value.to_le_bytes()),
FieldValue::Bool(value) => {
if *value {
hasher.update(b"true");
} else {
hasher.update(b"false");
}
}
FieldValue::Uuid(value) => {
let mut repr = [0u8; 36];
let text = value.hyphenated().encode_lower(&mut repr);
hasher.update(text.as_bytes());
}
FieldValue::Duration(value) => {
hasher.update(value.as_secs().to_le_bytes());
hasher.update(value.subsec_nanos().to_le_bytes());
}
FieldValue::Ip(value) => {
let mut buffer = StackBuffer::<46>::new();
if write!(&mut buffer, "{value}").is_ok() {
hasher.update(buffer.as_bytes());
} else {
let fallback = value.to_string();
hasher.update(fallback.as_bytes());
}
}
#[cfg(feature = "serde_json")]
FieldValue::Json(value) => {
if let Ok(serialized) = serde_json::to_vec(value) {
hasher.update(&serialized);
}
}
}
let digest = hasher.finalize();
let mut hex = String::with_capacity(digest.len() * 2);
for byte in digest {
let _ = write!(&mut hex, "{:02x}", byte);
}
hex
}
pub(crate) fn mask_last4_field_value(value: &FieldValue) -> Option<String> {
match value {
FieldValue::Str(value) => Some(mask_last4(value.as_ref())),
FieldValue::I64(value) => {
let mut buffer = IntegerBuffer::new();
Some(mask_last4(buffer.format(*value)))
}
FieldValue::U64(value) => {
let mut buffer = IntegerBuffer::new();
Some(mask_last4(buffer.format(*value)))
}
FieldValue::F64(value) => {
let mut buffer = FloatBuffer::new();
Some(mask_last4(buffer.format(*value)))
}
FieldValue::Uuid(value) => {
let mut repr = [0u8; 36];
let text = value.hyphenated().encode_lower(&mut repr);
Some(mask_last4(text))
}
FieldValue::Duration(value) => Some(mask_last4(&duration_to_string(*value))),
FieldValue::Ip(value) => {
let mut buffer = StackBuffer::<46>::new();
if write!(&mut buffer, "{value}").is_err() {
return Some(mask_last4(&value.to_string()));
}
buffer.as_str().map(mask_last4)
}
#[cfg(feature = "serde_json")]
FieldValue::Json(value) => serde_json::to_string(value)
.ok()
.map(|text| mask_last4(&text)),
FieldValue::Bool(_) => None
}
}
pub(crate) fn mask_last4(value: &str) -> String {
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());
masked.extend(repeat_n('*', mask_len));
masked.extend(chars.skip(mask_len));
masked
}
#[cfg(test)]
mod tests {
use core::{net::IpAddr, time::Duration};
use super::*;
fn assert_hex_digest(digest: &str) {
assert_eq!(digest.len(), 64);
assert!(digest.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hash_covers_all_scalar_variants() {
assert_hex_digest(&hash_field_value(&FieldValue::Str("secret".into())));
assert_hex_digest(&hash_field_value(&FieldValue::I64(-42)));
assert_hex_digest(&hash_field_value(&FieldValue::U64(42)));
assert_hex_digest(&hash_field_value(&FieldValue::F64(1.5)));
assert_hex_digest(&hash_field_value(&FieldValue::Bool(true)));
assert_hex_digest(&hash_field_value(&FieldValue::Bool(false)));
assert_hex_digest(&hash_field_value(&FieldValue::Duration(
Duration::from_millis(1500)
)));
}
#[test]
fn hash_is_deterministic_per_value() {
let first = hash_field_value(&FieldValue::I64(7));
let second = hash_field_value(&FieldValue::I64(7));
let other = hash_field_value(&FieldValue::I64(8));
assert_eq!(first, second);
assert_ne!(first, other);
}
#[test]
fn hash_covers_uuid_and_ip() {
let uuid = uuid::Uuid::nil();
assert_hex_digest(&hash_field_value(&FieldValue::Uuid(uuid)));
let ip: IpAddr = "192.168.0.1".parse().expect("ipv4");
assert_hex_digest(&hash_field_value(&FieldValue::Ip(ip)));
let ip6: IpAddr = "::1".parse().expect("ipv6");
assert_hex_digest(&hash_field_value(&FieldValue::Ip(ip6)));
}
#[cfg(feature = "serde_json")]
#[test]
fn hash_covers_json() {
let value = serde_json::json!({"card": "4111"});
assert_hex_digest(&hash_field_value(&FieldValue::Json(value)));
}
#[test]
fn mask_covers_all_scalar_variants() {
assert_eq!(
mask_last4_field_value(&FieldValue::Str("4111111111111111".into()))
.expect("string masks"),
"************1111"
);
assert_eq!(
mask_last4_field_value(&FieldValue::I64(-123_456)).expect("i64 masks"),
"***3456"
);
assert_eq!(
mask_last4_field_value(&FieldValue::U64(123_456)).expect("u64 masks"),
"**3456"
);
let masked_float = mask_last4_field_value(&FieldValue::F64(1234.5)).expect("f64 masks");
assert!(masked_float.starts_with('*'));
let duration = Duration::from_secs(90);
assert_eq!(
mask_last4_field_value(&FieldValue::Duration(duration)).expect("duration masks"),
mask_last4(&duration_to_string(duration))
);
assert!(mask_last4_field_value(&FieldValue::Bool(true)).is_none());
}
#[test]
fn mask_covers_uuid_and_ip() {
let uuid = uuid::Uuid::nil();
let masked = mask_last4_field_value(&FieldValue::Uuid(uuid)).expect("uuid masks");
assert!(masked.ends_with("0000"));
assert!(masked.starts_with('*'));
let ip: IpAddr = "10.0.0.15".parse().expect("ipv4");
let masked_ip = mask_last4_field_value(&FieldValue::Ip(ip)).expect("ip masks");
assert!(masked_ip.ends_with("0.15"));
}
#[cfg(feature = "serde_json")]
#[test]
fn mask_covers_json() {
let value = serde_json::json!("tok_123456");
let masked = mask_last4_field_value(&FieldValue::Json(value)).expect("json masks");
assert!(masked.ends_with("456\""));
}
#[test]
fn mask_last4_handles_empty_and_short_values() {
assert_eq!(mask_last4(""), "");
assert_eq!(mask_last4("a"), "a");
assert_eq!(mask_last4("abcd"), "***d");
assert_eq!(mask_last4("abcde"), "*bcde");
}
#[test]
fn stack_buffer_rejects_overflow() {
let mut buffer = StackBuffer::<4>::new();
assert!(write!(&mut buffer, "hello").is_err());
assert!(write!(&mut buffer, "hi").is_ok());
assert_eq!(buffer.as_str(), Some("hi"));
}
}