125 lines
3.4 KiB
Rust
125 lines
3.4 KiB
Rust
use serde_json::Value;
|
|
|
|
pub fn redact_paths(value: &mut Value, paths: &[String]) {
|
|
for path in paths {
|
|
let segments = parse_path(path);
|
|
if segments.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
redact_path(value, &segments);
|
|
}
|
|
}
|
|
|
|
pub fn truncate_item_fields(value: &mut Value, max_len: usize) {
|
|
match value {
|
|
Value::String(text) => {
|
|
if text != "[REDACTED]" && text.chars().count() > max_len {
|
|
let truncated = text.chars().take(max_len).collect::<String>();
|
|
*text = format!("{truncated}...");
|
|
}
|
|
}
|
|
Value::Array(items) => {
|
|
for item in items {
|
|
truncate_item_fields(item, max_len);
|
|
}
|
|
}
|
|
Value::Object(map) => {
|
|
for value in map.values_mut() {
|
|
truncate_item_fields(value, max_len);
|
|
}
|
|
}
|
|
Value::Null | Value::Bool(_) | Value::Number(_) => {}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
enum PathSegment {
|
|
Field(String),
|
|
Wildcard,
|
|
}
|
|
|
|
fn parse_path(path: &str) -> Vec<PathSegment> {
|
|
let path = path.strip_prefix("$.").or_else(|| path.strip_prefix('$'));
|
|
let Some(path) = path else {
|
|
return Vec::new();
|
|
};
|
|
|
|
path.split('.')
|
|
.flat_map(|segment| {
|
|
if let Some(field) = segment.strip_suffix("[*]") {
|
|
vec![PathSegment::Field(field.to_owned()), PathSegment::Wildcard]
|
|
} else if segment == "*" {
|
|
vec![PathSegment::Wildcard]
|
|
} else {
|
|
vec![PathSegment::Field(segment.to_owned())]
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn redact_path(value: &mut Value, segments: &[PathSegment]) {
|
|
let Some((current, rest)) = segments.split_first() else {
|
|
*value = Value::String("[REDACTED]".to_owned());
|
|
return;
|
|
};
|
|
|
|
match current {
|
|
PathSegment::Field(field) => {
|
|
if let Some(next) = value
|
|
.as_object_mut()
|
|
.and_then(|object| object.get_mut(field))
|
|
{
|
|
redact_path(next, rest);
|
|
}
|
|
}
|
|
PathSegment::Wildcard => {
|
|
if let Some(items) = value.as_array_mut() {
|
|
for item in items {
|
|
redact_path(item, rest);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
|
|
use super::{redact_paths, truncate_item_fields};
|
|
|
|
#[test]
|
|
fn redacts_simple_and_wildcard_paths() {
|
|
let mut value = json!({
|
|
"summary": { "token": "secret" },
|
|
"items": [
|
|
{ "token": "a", "message": "keep" },
|
|
{ "token": "b", "message": "keep" }
|
|
]
|
|
});
|
|
|
|
redact_paths(
|
|
&mut value,
|
|
&["$.summary.token".to_owned(), "$.items[*].token".to_owned()],
|
|
);
|
|
|
|
assert_eq!(value["summary"]["token"], json!("[REDACTED]"));
|
|
assert_eq!(value["items"][0]["token"], json!("[REDACTED]"));
|
|
assert_eq!(value["items"][1]["token"], json!("[REDACTED]"));
|
|
}
|
|
|
|
#[test]
|
|
fn truncates_long_strings_recursively() {
|
|
let mut value = json!({
|
|
"message": "abcdefghijklmnop",
|
|
"nested": [{ "message": "qrstuvwxyz" }]
|
|
});
|
|
|
|
truncate_item_fields(&mut value, 4);
|
|
|
|
assert_eq!(value["message"], json!("abcd..."));
|
|
assert_eq!(value["nested"][0]["message"], json!("qrst..."));
|
|
}
|
|
}
|