use serde_json::Value;
use crate::{
JsonDepthBudget,
MaskingPolicy,
RedactionPolicy,
RedactionRules,
Sensitivity,
policy::ResolvedField,
};
use super::{
JsonRedactionOutcome,
JsonUnkeyedValuePolicy,
};
pub(crate) struct JsonRedactionState<'policy, 'budget, 'marker> {
base_rules: &'policy RedactionRules,
context_rules: &'policy RedactionRules,
masking: &'policy MaskingPolicy,
json_depth_budget: JsonDepthBudget,
unkeyed: JsonUnkeyedValuePolicy<'marker>,
remaining_mask_bytes: &'budget mut usize,
}
impl<'policy, 'budget, 'marker> JsonRedactionState<'policy, 'budget, 'marker> {
#[inline(always)]
pub(crate) const fn new(
base_rules: &'policy RedactionRules,
context_rules: &'policy RedactionRules,
masking: &'policy MaskingPolicy,
json_depth_budget: JsonDepthBudget,
unkeyed: JsonUnkeyedValuePolicy<'marker>,
remaining_mask_bytes: &'budget mut usize,
) -> Self {
Self {
base_rules,
context_rules,
masking,
json_depth_budget,
unkeyed,
remaining_mask_bytes,
}
}
#[inline(always)]
pub(crate) fn from_policy(
policy: &'policy RedactionPolicy,
unkeyed: JsonUnkeyedValuePolicy<'marker>,
remaining_mask_bytes: &'budget mut usize,
) -> Self {
Self::new(
policy.rules(),
policy.rules(),
policy.masking(),
policy.json_depth_budget(),
unkeyed,
remaining_mask_bytes,
)
}
pub(crate) fn redact(&mut self, value: &mut Value) -> JsonRedactionOutcome {
self.redact_value(value, false, 0)
}
fn redact_value(
&mut self,
value: &mut Value,
has_field: bool,
depth: usize,
) -> JsonRedactionOutcome {
if depth >= self.json_depth_budget.max_depth()
&& matches!(value, Value::Object(_) | Value::Array(_))
{
self.mask_keyed_value(value, Sensitivity::Secret);
return JsonRedactionOutcome::default();
}
match value {
Value::Object(values) => self.redact_object(values, depth),
Value::Array(values) => self.redact_array(values, has_field, depth),
Value::Null
| Value::Bool(_)
| Value::Number(_)
| Value::String(_) => self.redact_scalar(value, has_field),
}
}
fn redact_object(
&mut self,
values: &mut serde_json::Map<String, Value>,
depth: usize,
) -> JsonRedactionOutcome {
let mut outcome = JsonRedactionOutcome::default();
for (key, value) in values {
let resolved = stronger(
self.base_rules.resolve_field(key),
self.context_rules.resolve_field(key),
);
match resolved {
ResolvedField::Sensitive { sensitivity } => {
self.mask_keyed_value(value, sensitivity);
}
ResolvedField::PassThrough => {
outcome.merge(self.redact_value(
value,
true,
depth.saturating_add(1),
));
}
}
}
outcome
}
fn redact_array(
&mut self,
values: &mut Vec<Value>,
has_field: bool,
depth: usize,
) -> JsonRedactionOutcome {
let mut outcome = JsonRedactionOutcome::default();
for value in values {
outcome.merge(self.redact_value(
value,
has_field,
depth.saturating_add(1),
));
}
outcome
}
fn redact_scalar(
&mut self,
value: &mut Value,
has_field: bool,
) -> JsonRedactionOutcome {
if has_field {
return JsonRedactionOutcome::default();
}
match self.unkeyed {
JsonUnkeyedValuePolicy::PassThrough => {
JsonRedactionOutcome::passed_unkeyed()
}
JsonUnkeyedValuePolicy::Redact {
marker,
truncated_marker,
} => {
*value = Value::String(
self.take_unkeyed_marker(marker, truncated_marker),
);
JsonRedactionOutcome::default()
}
}
}
fn mask_keyed_value(&mut self, value: &mut Value, level: Sensitivity) {
let masked = match value {
Value::String(text) => self
.masking
.mask_bounded(level, text, *self.remaining_mask_bytes)
.into_owned(),
_ => self
.masking
.mask_opaque_bounded(level, *self.remaining_mask_bytes),
};
*self.remaining_mask_bytes =
self.remaining_mask_bytes.saturating_sub(masked.len());
*value = Value::String(masked);
}
fn take_unkeyed_marker(
&mut self,
marker: &str,
truncated_marker: &str,
) -> String {
let selected = if *self.remaining_mask_bytes >= marker.len() {
marker
} else if *self.remaining_mask_bytes >= truncated_marker.len() {
truncated_marker
} else {
return String::new();
};
*self.remaining_mask_bytes =
self.remaining_mask_bytes.saturating_sub(selected.len());
selected.to_owned()
}
}
fn stronger(base: ResolvedField, context: ResolvedField) -> ResolvedField {
match (base, context) {
(
ResolvedField::Sensitive { sensitivity: base },
ResolvedField::Sensitive {
sensitivity: context,
},
) => ResolvedField::Sensitive {
sensitivity: base.max(context),
},
(
ResolvedField::Sensitive { sensitivity },
ResolvedField::PassThrough,
)
| (
ResolvedField::PassThrough,
ResolvedField::Sensitive { sensitivity },
) => ResolvedField::Sensitive { sensitivity },
(ResolvedField::PassThrough, ResolvedField::PassThrough) => {
ResolvedField::PassThrough
}
}
}