use qubit_budget::json::JsonDecodeSession;
use qubit_json::decode::JsonDecodeErrorKind;
use qubit_json::decode::JsonDecoder;
use serde_json::Value;
use super::JsonAdmissionError;
use super::bounded_json_redaction::BoundedJsonRedaction;
use super::bounded_json_redaction::redacted_json_value_bounded;
use super::internal::JsonStructureSeed;
use crate::output::log_escape::escape_log_control_characters;
use crate::runtime::OperationSink;
use crate::runtime::RenderedOperation;
use crate::runtime::TextSession;
use crate::runtime::runtime_session::RuntimeSession;
pub(crate) fn admit_json_text_value(session: &mut dyn RuntimeSession, text: &str) -> Result<Value, JsonAdmissionError> {
admit_json_text_value_at_depth(session, text, 1)
}
pub(crate) fn admit_json_text_value_at_depth(
session: &mut dyn RuntimeSession,
text: &str,
root_depth: usize,
) -> Result<Value, JsonAdmissionError> {
#[cfg(test)]
super::parse_counter::record_json_parse();
let mut rejected = false;
let (mut admission, json_budget) = session.split_json_admission();
let mut decoder = JsonDecoder::new(JsonDecodeSession::borrowing_value(json_budget));
let admitted = decoder.decode_seed_str(
JsonStructureSeed {
admission: &mut admission,
depth: root_depth,
collection_item: false,
rejected: &mut rejected,
},
text,
);
match admitted {
Ok(value) => Ok(value),
Err(_) if rejected => Err(JsonAdmissionError::Limit),
Err(error) if error.kind() == JsonDecodeErrorKind::Budget => {
session.record_json_value_limit_reached();
Err(JsonAdmissionError::Limit)
}
Err(_) => Err(JsonAdmissionError::Invalid),
}
}
#[must_use]
pub(crate) fn passthrough_json_text_with_limit(text: &str, max_output_bytes: usize) -> RenderedOperation {
json_output_from_bounded(BoundedJsonRedaction::Complete(text.to_owned()), max_output_bytes)
}
#[must_use]
pub(crate) fn json_output_from_bounded(
bounded: super::bounded_json_redaction::BoundedJsonRedaction,
max_output_bytes: usize,
) -> RenderedOperation {
let (rendered, raw_truncated, invalid_json) = bounded.into_parts();
let output_text = escape_log_control_characters(std::borrow::Cow::Owned(rendered)).into_owned();
if output_text.len() > max_output_bytes {
let fallback = "<truncated>";
let mut output = if fallback.len() <= max_output_bytes {
OperationSink::truncated(fallback, crate::RedactionReason::OutputLimitReached)
} else {
OperationSink::exhausted(String::new())
};
if invalid_json {
output = output.with_reason(crate::RedactionReason::InvalidJson);
}
return output.finish();
}
if raw_truncated {
OperationSink::truncated(output_text, crate::RedactionReason::OutputLimitReached).finish()
} else if invalid_json {
OperationSink::complete_with_reason(output_text, crate::RedactionReason::InvalidJson).finish()
} else {
OperationSink::complete(output_text).finish()
}
}
pub struct JsonRedactionWriter<'session> {
pub(super) session: &'session mut TextSession,
}
impl<'session> JsonRedactionWriter<'session> {
#[must_use]
#[inline(always)]
pub(crate) const fn new(session: &'session mut TextSession) -> Self {
Self { session }
}
pub fn text(&mut self, text: &str) -> &mut Self {
if self.session.skip_aggregate_for_exhausted_output() {
return self;
}
if !self.session.admit_input(text.len()) {
self.session.append_rendered_operation(
crate::runtime::OperationSink::truncated("<truncated>", crate::RedactionReason::InputLimitReached)
.finish(),
);
return self;
}
let result = if self.session.policy().is_disabled() {
self.redact_text_direct(text)
} else {
match admit_json_text_value(self.session, text) {
Ok(value) => self.redact_value_direct(&value),
Err(JsonAdmissionError::Invalid) => {
invalid_json_output(self.session.policy(), self.session.remaining_output_bytes())
}
Err(JsonAdmissionError::Limit) => {
OperationSink::truncated("<truncated>", crate::RedactionReason::TraversalLimitReached).finish()
}
}
};
self.session.append_rendered_operation(result);
self
}
pub fn value(&mut self, value: &Value) -> &mut Self {
if self.session.skip_aggregate_for_exhausted_output() {
return self;
}
if !self.session.admit_json_value(value) {
self.session.append_rendered_operation(
OperationSink::truncated("<truncated>", crate::RedactionReason::TraversalLimitReached).finish(),
);
return self;
}
let result = self.redact_value_direct(value);
self.session.append_rendered_operation(result);
self
}
}
impl JsonRedactionWriter<'_> {
#[must_use]
pub(crate) fn redact_text_direct(&mut self, text: &str) -> RenderedOperation {
passthrough_json_text_with_limit(text, self.session.remaining_output_bytes())
}
#[must_use]
pub(crate) fn redact_value_direct(&mut self, value: &Value) -> RenderedOperation {
redact_json_value_with_limit(self.session.policy(), value, self.session.remaining_output_bytes())
}
}
#[must_use]
pub(crate) fn redact_json_value_with_limit(
policy: &crate::RedactionPolicy,
value: &Value,
max_output_bytes: usize,
) -> RenderedOperation {
json_output_from_bounded(
redacted_json_value_bounded(value, policy, max_output_bytes),
max_output_bytes,
)
}
pub(crate) fn invalid_json_output(policy: &crate::RedactionPolicy, max_output_bytes: usize) -> RenderedOperation {
json_output_from_bounded(
BoundedJsonRedaction::Invalid(policy.masking().mask_opaque(crate::Sensitivity::Secret).to_owned()),
max_output_bytes,
)
}
#[cfg(test)]
mod tests {
use super::passthrough_json_text_with_limit;
use crate::RedactionCompletion;
#[test]
fn test_bounded_json_helper_never_exceeds_the_caller_allowance() {
let output = passthrough_json_text_with_limit(
r#"{"description":"this value is deliberately longer than the allowance"}"#,
16,
);
assert_eq!(output.completion(), RedactionCompletion::Truncated);
assert!(output.reasons().contains(crate::RedactionReason::OutputLimitReached));
assert!(output.text().len() <= 16);
}
}