use serde_json::{Map, Value};
use crate::pii::REDACTED_SENTINEL;
const TRUNCATION_MARKER: &str = "...";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct HardenLimits {
pub max_value_length: usize,
pub max_attr_count: usize,
pub max_depth: usize,
}
fn truncate_string_value(value: &mut String, max_value_length: usize) {
if max_value_length == 0 || value.len() <= max_value_length {
return;
}
let cutoff = value
.char_indices()
.map(|(index, ch)| index + ch.len_utf8())
.take_while(|end| *end <= max_value_length)
.last()
.unwrap_or(0);
value.truncate(cutoff);
value.push_str(TRUNCATION_MARKER);
}
fn should_strip_value_char(ch: char) -> bool {
matches!(ch, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f' | '\x7f')
}
fn should_strip_key_char(ch: char) -> bool {
matches!(ch, '\x00'..='\x1f' | '\x7f')
}
pub(crate) fn clean_key(key: &str) -> String {
key.chars()
.filter(|ch| !should_strip_key_char(*ch))
.collect()
}
pub(crate) fn cleaned_key_claims_slot(
occupied: bool,
untouched: bool,
holder_is_verbatim: bool,
) -> bool {
!occupied || (untouched && !holder_is_verbatim)
}
fn harden_string(text: &str, max_value_length: usize) -> String {
let mut cleaned: String = text
.chars()
.filter(|ch| !should_strip_value_char(*ch))
.collect();
truncate_string_value(&mut cleaned, max_value_length);
cleaned
}
fn redacted() -> Value {
Value::String(REDACTED_SENTINEL.to_string())
}
pub(crate) fn harden_value(value: &Value, limits: HardenLimits, depth: usize) -> Value {
match value {
Value::String(text) => Value::String(harden_string(text, limits.max_value_length)),
Value::Array(items) => {
if depth >= limits.max_depth {
return redacted();
}
Value::Array(
items
.iter()
.map(|item| harden_value(item, limits, depth + 1))
.collect(),
)
}
Value::Object(map) => {
if depth >= limits.max_depth {
return redacted();
}
let mut out = Map::new();
let mut verbatim: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for (key, nested) in retained_entries(map, limits.max_attr_count) {
let name = clean_key(key);
let untouched = name == *key;
if !cleaned_key_claims_slot(
out.contains_key(&name),
untouched,
verbatim.contains(&name),
) {
continue;
}
out.insert(name.clone(), harden_value(nested, limits, depth + 1));
if untouched {
verbatim.insert(name);
}
}
Value::Object(out)
}
scalar => scalar.clone(),
}
}
fn retained_entries(
map: &Map<String, Value>,
max_attr_count: usize,
) -> impl Iterator<Item = (&String, &Value)> {
let limit = if max_attr_count == 0 {
map.len()
} else {
max_attr_count
};
map.iter().take(limit)
}
#[cfg(test)]
#[path = "harden_tests.rs"]
mod tests;