use std::fmt;
use serde_json::Value;
#[cfg(feature = "serde")]
use serde::ser::{
SerializeMap as _,
SerializeSeq as _,
};
use crate::{
RedactValue as _,
RedactedValue,
RedactionPolicy,
policy::ResolvedField,
};
#[must_use = "format or serialize the redacted JSON view"]
pub struct RedactedJson<'value, 'policy> {
value: &'value Value,
policy: &'policy RedactionPolicy,
depth: usize,
unkeyed: bool,
}
impl<'value, 'policy> RedactedJson<'value, 'policy> {
#[inline(always)]
pub const fn new(
value: &'value Value,
policy: &'policy RedactionPolicy,
) -> Self {
Self {
value,
policy,
depth: 0,
unkeyed: true,
}
}
#[inline(always)]
fn nested<'nested>(
&self,
value: &'nested Value,
unkeyed: bool,
) -> RedactedJson<'nested, 'policy> {
RedactedJson {
value,
policy: self.policy,
depth: self.depth.saturating_add(1),
unkeyed,
}
}
#[inline(always)]
fn depth_limit_reached(&self) -> bool {
self.depth >= self.policy.json_depth_budget().max_depth()
&& matches!(self.value, Value::Object(_) | Value::Array(_))
}
#[inline(always)]
fn depth_limit_mask(&self) -> RedactedValue<'_> {
RedactedValue::opaque(crate::Sensitivity::Secret, self.policy.masking())
}
#[inline(always)]
fn redact_unkeyed_scalar(&self) -> bool {
self.unkeyed
&& self.policy.unkeyed_json_value_policy()
== crate::UnkeyedJsonValuePolicy::Redact
&& !matches!(self.value, Value::Array(_) | Value::Object(_))
}
}
impl fmt::Debug for RedactedJson<'_, '_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_json(self, formatter)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for RedactedJson<'_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if self.depth_limit_reached() {
return serde::Serialize::serialize(
&self.depth_limit_mask(),
serializer,
);
}
match self.value {
Value::Array(values) => {
let mut output =
serializer.serialize_seq(Some(values.len()))?;
for value in values {
output.serialize_element(&self.nested(value, true))?;
}
output.end()
}
Value::Object(values) => {
let mut output =
serializer.serialize_map(Some(values.len()))?;
for (key, value) in values {
let resolved = self.policy.resolve_field(key);
match resolved {
ResolvedField::Sensitive { sensitivity } => match value
{
Value::String(text) => {
let redacted = text.redact_value(
sensitivity,
self.policy.masking(),
);
output.serialize_entry(key, &redacted)?;
}
_ => {
let redacted = RedactedValue::opaque(
sensitivity,
self.policy.masking(),
);
output.serialize_entry(key, &redacted)?;
}
},
ResolvedField::PassThrough => {
output.serialize_entry(
key,
&self.nested(value, false),
)?;
}
}
}
output.end()
}
_ if self.redact_unkeyed_scalar() => serde::Serialize::serialize(
&self.depth_limit_mask(),
serializer,
),
value => serde::Serialize::serialize(value, serializer),
}
}
}
mod session_view {
use std::fmt::{
self,
Write as _,
};
use serde_json::Value;
use crate::{
LogOutputLimit,
RedactedJson,
RedactionSession,
policy::OutputCharge,
text::internal::{
BoundedLogEscapeWriter,
LogEscapeWriter,
},
};
#[must_use = "format the nested redacted JSON view"]
pub struct RedactedJsonSession<'value, 'session, 'policy> {
value: &'value Value,
session: &'session RedactionSession<'policy>,
}
impl<'value, 'session, 'policy> RedactedJsonSession<'value, 'session, 'policy> {
#[inline(always)]
pub fn new(
value: &'value Value,
session: &'session RedactionSession<'policy>,
) -> Self {
Self { value, session }
}
#[inline]
fn opaque(&self) -> &str {
self.session
.policy()
.masking()
.mask_opaque(crate::Sensitivity::Secret)
}
fn render(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let remaining = self.session.remaining_output_bytes();
let Some(limit) = LogOutputLimit::new(remaining).ok() else {
return self.write_fallback(formatter);
};
let mut writer = BoundedLogEscapeWriter::new(limit);
let view = RedactedJson::new(self.value, self.session.policy());
let _ = if formatter.alternate() {
write!(&mut writer, "{view:#?}")
} else {
write!(&mut writer, "{view:?}")
};
let rendered = writer.finish();
match self
.session
.charge_output_or_fallback(rendered.len(), self.opaque().len())
{
OutputCharge::Complete => formatter.write_str(&rendered),
OutputCharge::Fallback => formatter.write_str(self.opaque()),
OutputCharge::Exhausted => Ok(()),
}
}
fn write_fallback(
&self,
formatter: &mut fmt::Formatter<'_>,
) -> fmt::Result {
let fallback = self.opaque();
match self
.session
.charge_output_or_fallback(fallback.len(), fallback.len())
{
OutputCharge::Complete => formatter.write_str(fallback),
OutputCharge::Fallback | OutputCharge::Exhausted => Ok(()),
}
}
}
impl fmt::Debug for RedactedJsonSession<'_, '_, '_> {
#[inline]
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(formatter)
}
}
impl fmt::Display for RedactedJsonSession<'_, '_, '_> {
#[inline]
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut writer = LogEscapeWriter::new(formatter);
write!(&mut writer, "{self:?}")
}
}
}
pub use session_view::RedactedJsonSession;
fn fmt_json(
view: &RedactedJson<'_, '_>,
formatter: &mut fmt::Formatter<'_>,
) -> fmt::Result {
if view.depth_limit_reached() {
return fmt::Debug::fmt(&view.depth_limit_mask(), formatter);
}
match view.value {
Value::Array(values) => {
let mut output = formatter.debug_list();
for value in values {
output.entry(&view.nested(value, true));
}
output.finish()
}
Value::Object(values) => {
let mut output = formatter.debug_map();
for (key, value) in values {
let resolved = view.policy.resolve_field(key);
match resolved {
ResolvedField::Sensitive { sensitivity } => {
fmt_masked_entry(
&mut output,
key,
value,
sensitivity,
view.policy.masking(),
);
}
ResolvedField::PassThrough => {
output.entry(key, &view.nested(value, false));
}
}
}
output.finish()
}
_ if view.redact_unkeyed_scalar() => {
fmt::Debug::fmt(&view.depth_limit_mask(), formatter)
}
value => fmt::Debug::fmt(value, formatter),
}
}
fn fmt_masked_entry(
output: &mut fmt::DebugMap<'_, '_>,
key: &str,
value: &Value,
sensitivity: crate::Sensitivity,
masking: &crate::MaskingPolicy,
) {
match value {
Value::String(text) => {
let redacted = text.redact_value(sensitivity, masking);
output.entry(&key, &redacted);
}
_ => {
let redacted = RedactedValue::opaque(sensitivity, masking);
output.entry(&key, &redacted);
}
};
}