1use std::fmt::Debug;
11use std::fmt::Write as _;
12
13use crate::Sensitivity;
14use crate::domain::RedactionEntries;
15use crate::domain::RedactionFields;
16use crate::domain::RedactionItems;
17use crate::domain::internal::bounded_capture::bounded_debug;
18use crate::domain::internal::bounded_debug_writer::BoundedDebugWriter;
19use crate::runtime::runtime_session::RuntimeSession;
20
21pub struct RedactionWriter<'session> {
56 pub(super) session: &'session mut dyn RuntimeSession,
58}
59
60impl<'session> RedactionWriter<'session> {
61 #[must_use]
63 pub(crate) fn new(session: &'session mut dyn RuntimeSession) -> Self {
64 Self { session }
65 }
66
67 pub(crate) fn new_root(session: &'session mut dyn RuntimeSession) -> Self {
69 Self::new(session)
70 }
71
72 #[inline]
74 pub fn literal(&mut self, text: &'static str) {
75 if self.session.domain_frame_is_truncated() {
76 return;
77 }
78 self.write_fragment(text);
79 }
80
81 pub fn unredacted<T>(&mut self, value: &T) -> &mut Self
92 where
93 T: Debug + ?Sized,
94 {
95 if self.session.domain_frame_is_truncated() {
96 return self;
97 }
98 if self.session.is_inspection() {
99 return self;
100 }
101 self.write_debug(value);
102 self
103 }
104
105 #[inline]
112 pub fn unmarked<T>(&mut self, value: &T) -> &mut Self
113 where
114 T: Debug + ?Sized,
115 {
116 self.unredacted(value)
117 }
118
119 pub(crate) fn trim_trailing_separator(&mut self) {
121 self.session.trim_domain_frame_separator();
122 }
123
124 #[cfg(feature = "json")]
130 pub(super) fn write_json_text(&mut self, value: &str) {
131 if self.session.is_inspection() {
132 crate::formats::json::inspection::inspect_text(self.session, value);
133 return;
134 }
135 if !self.session.admit_input(value.len()) {
136 self.truncate_without_output_limit();
137 return;
138 }
139 let admitted = match crate::formats::json::admit_json_text_value(self.session, value) {
143 Ok(value) => value,
144 Err(crate::formats::json::JsonAdmissionError::Invalid) => {
145 let allowance = self.session.remaining_output_bytes().min(self.remaining_output_bytes());
146 let output = crate::formats::json::invalid_json_output(self.session.policy(), allowance);
147 self.session.record_rendered_provenance(&output);
148 self.write_debug(output.text());
149 return;
150 }
151 Err(crate::formats::json::JsonAdmissionError::Limit) => {
152 self.truncate_without_output_limit();
153 return;
154 }
155 };
156 let allowance = self.session.remaining_output_bytes().min(self.remaining_output_bytes());
157 let output = crate::formats::json::redact_json_value_with_limit(self.session.policy(), &admitted, allowance);
158 if output.completion() != crate::RedactionCompletion::Complete {
159 self.truncate_without_output_limit();
160 }
161 self.session.record_rendered_provenance(&output);
162 self.write_debug(output.text());
163 }
164
165 #[cfg(feature = "json")]
167 pub(super) fn write_json_value(&mut self, value: &serde_json::Value) {
168 if self.session.is_inspection() {
169 crate::formats::json::inspection::inspect_borrowed_value(self.session, value);
170 return;
171 }
172 if !self.session.admit_json_value(value) {
173 self.truncate_without_output_limit();
174 return;
175 }
176 let allowance = self.session.remaining_output_bytes().min(self.remaining_output_bytes());
177 let output = crate::formats::json::redact_json_value_with_limit(self.session.policy(), value, allowance);
178 if output.completion() != crate::RedactionCompletion::Complete {
179 self.truncate_without_output_limit();
180 }
181 self.session.record_rendered_provenance(&output);
182 self.write_fragment(output.text());
183 }
184
185 pub fn record<F>(&mut self, name: &'static str, configure: F)
187 where
188 F: for<'writer> FnOnce(&mut RedactionFields<'writer, 'session>),
189 {
190 self.write_field_structure(name, " { ", " }", configure);
191 }
192
193 pub fn tuple<F>(&mut self, name: &'static str, configure: F)
195 where
196 F: for<'writer> FnOnce(&mut RedactionFields<'writer, 'session>),
197 {
198 self.write_field_structure(name, "(", ")", configure);
199 }
200
201 pub fn transparent<F>(&mut self, configure: F)
207 where
208 F: for<'writer> FnOnce(&mut RedactionFields<'writer, 'session>),
209 {
210 let mut fields = RedactionFields {
211 writer: self,
212 named: false,
213 };
214 configure(&mut fields);
215 self.trim_trailing_separator();
216 }
217
218 pub fn sequence<F>(&mut self, configure: F)
220 where
221 F: for<'writer> FnOnce(&mut RedactionItems<'writer, 'session>),
222 {
223 self.write_item_structure("", "[", "]", configure);
224 }
225
226 pub fn map<F>(&mut self, configure: F)
228 where
229 F: for<'writer> FnOnce(&mut RedactionEntries<'writer, 'session>),
230 {
231 self.write_entry_structure("", "{ ", " }", configure);
232 }
233
234 pub fn variant<F>(&mut self, enum_name: &'static str, variant_name: &'static str, configure: F)
236 where
237 F: for<'writer> FnOnce(&mut RedactionFields<'writer, 'session>),
238 {
239 self.write_fragment(enum_name);
240 self.write_fragment("::");
241 self.write_field_structure(variant_name, " { ", " }", configure);
242 }
243
244 #[must_use]
246 pub(crate) fn finish_with_completion(self) -> (String, bool, bool) {
247 self.session.finish_domain_frame()
248 }
249
250 fn write_field_structure<F>(
253 &mut self,
254 name: &'static str,
255 opening: &'static str,
256 closing: &'static str,
257 configure: F,
258 ) where
259 F: for<'writer> FnOnce(&mut RedactionFields<'writer, 'session>),
260 {
261 if !self.session.begin_domain_value() {
262 self.truncate_without_output_limit();
263 return;
264 }
265 self.write_fragment(name);
266 self.write_fragment(opening);
267 if self.can_write() {
268 let mut fields = RedactionFields {
269 writer: self,
270 named: opening == " { ",
271 };
272 configure(&mut fields);
273 }
274 if self.can_write() {
275 self.trim_trailing_separator();
276 self.write_fragment(closing);
277 }
278 self.session.leave_domain_value();
279 }
280
281 fn write_item_structure<F>(
283 &mut self,
284 name: &'static str,
285 opening: &'static str,
286 closing: &'static str,
287 configure: F,
288 ) where
289 F: for<'writer> FnOnce(&mut RedactionItems<'writer, 'session>),
290 {
291 if !self.session.begin_domain_value() {
292 self.truncate_without_output_limit();
293 return;
294 }
295 self.write_fragment(name);
296 self.write_fragment(opening);
297 if self.can_write() {
298 configure(&mut RedactionItems { writer: self });
299 }
300 if self.can_write() {
301 self.trim_trailing_separator();
302 self.write_fragment(closing);
303 }
304 self.session.leave_domain_value();
305 }
306
307 fn write_entry_structure<F>(
309 &mut self,
310 name: &'static str,
311 opening: &'static str,
312 closing: &'static str,
313 configure: F,
314 ) where
315 F: for<'writer> FnOnce(&mut RedactionEntries<'writer, 'session>),
316 {
317 if !self.session.begin_domain_value() {
318 self.truncate_without_output_limit();
319 return;
320 }
321 self.write_fragment(name);
322 self.write_fragment(opening);
323 if self.can_write() {
324 configure(&mut RedactionEntries { writer: self });
325 }
326 if self.can_write() {
327 self.trim_trailing_separator();
328 self.write_fragment(closing);
329 }
330 self.session.leave_domain_value();
331 }
332
333 pub(super) fn truncate_for_output_limit(&mut self) {
335 self.session.mark_domain_frame_output_limit_reached();
336 self.truncate_without_output_limit();
337 }
338
339 pub(super) fn truncate_without_output_limit(&mut self) {
345 self.session.truncate_domain_frame_without_output_limit();
346 }
347
348 pub(super) fn write_fragment(&mut self, text: &str) -> bool {
353 self.session.write_domain_fragment(text)
354 }
355
356 pub(crate) fn write_debug<T>(&mut self, value: &T)
358 where
359 T: Debug + ?Sized,
360 {
361 if self.session.is_inspection() {
362 return;
363 }
364 let mut formatter = BoundedDebugWriter { writer: self };
365 let _ = write!(&mut formatter, "{value:?}");
366 }
367
368 pub(super) fn write_masked_debug<T>(&mut self, level: Sensitivity, value: &T)
371 where
372 T: Debug + ?Sized,
373 {
374 if self.session.is_inspection() {
375 self.session.observe_sensitivity(level);
376 return;
377 }
378 if matches!(level, Sensitivity::High | Sensitivity::Secret) {
379 let masked = self
380 .session
381 .policy()
382 .masking()
383 .mask_opaque_bounded(level, self.remaining_output_bytes());
384 self.write_debug(&masked);
385 return;
386 }
387 let raw_limit = self.remaining_output_bytes();
388 let (raw, raw_truncated) = bounded_debug(value, raw_limit);
389 let (masked, mask_truncated) =
390 self.session
391 .policy()
392 .masking()
393 .mask_bounded_with_truncation(level, &raw, self.remaining_output_bytes());
394 self.write_debug(masked.as_ref());
395 if raw_truncated || mask_truncated {
396 self.truncate_for_output_limit();
397 }
398 }
399
400 pub(crate) fn write_level_scalar<T>(&mut self, level: Sensitivity, value: &T)
402 where
403 T: Debug + ?Sized,
404 {
405 if self.session.policy().is_disabled() {
406 self.write_debug(value);
407 } else {
408 self.write_masked_debug(level, value);
409 }
410 }
411
412 pub(crate) fn level_tuple<F>(&mut self, configure: F)
414 where
415 F: for<'writer> FnOnce(&mut RedactionItems<'writer, 'session>),
416 {
417 self.write_item_structure("", "(", ")", configure);
418 }
419
420 #[inline]
422 pub(super) fn can_write(&self) -> bool {
423 !self.session.domain_frame_is_truncated() && self.remaining_output_bytes() > 0
424 }
425
426 #[inline]
428 pub(super) fn remaining_output_bytes(&self) -> usize {
429 self.session.remaining_domain_frame_output_bytes()
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::RedactionWriter;
436 use crate::Redact;
437 use crate::Redactor;
438
439 struct Nested;
440
441 impl Redact for Nested {
442 fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
443 writer.record("Nested", |fields| {
444 fields.unredacted("id", || 7_u8);
445 });
446 }
447 }
448
449 struct Container;
450
451 impl Redact for Container {
452 fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
453 writer.record("Container", |fields| {
454 fields.nested("nested", &Nested);
455 });
456 }
457 }
458
459 #[test]
462 fn nested_values_use_the_active_writer_transaction() {
463 let output = Redactor::standard().redact(&Container);
464
465 assert!(output.text().as_str().contains("Nested { id: 7 }"));
466 assert_eq!(output.summary().usage().output_bytes(), output.text().as_str().len());
467 }
468
469 #[cfg(feature = "json")]
470 struct JsonContainer;
471
472 #[cfg(feature = "json")]
473 impl Redact for JsonContainer {
474 fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
475 writer.record("JsonContainer", |fields| {
476 fields.json("payload", "{invalid json");
477 });
478 }
479 }
480
481 #[cfg(feature = "json")]
484 #[test]
485 fn writer_json_uses_the_active_session_summary() {
486 let output = Redactor::standard().text_composer().value(&JsonContainer).finish();
487
488 assert_eq!(output.summary().usage().presented_input_bytes(), "{invalid json".len());
489 assert!(output.summary().reasons().contains(crate::RedactionReason::InvalidJson));
490 }
491
492 #[cfg(feature = "json")]
496 #[test]
497 fn writer_json_uses_shared_structure_budget_and_preserves_its_reason() {
498 let policy = crate::RedactionPolicy::builder()
499 .limits(|limits| {
500 limits.max_depth(1);
501 })
502 .expect("the limit draft should build")
503 .build()
504 .expect("the policy should build");
505 let output = Redactor::new(policy)
506 .text_composer()
507 .value(&JsonContainerWithValidNestedValue)
508 .finish();
509
510 assert_eq!(output.summary().completion(), crate::RedactionCompletion::Truncated);
511 assert!(
512 output
513 .summary()
514 .reasons()
515 .contains(crate::RedactionReason::DepthLimitReached)
516 );
517 assert!(
518 !output
519 .summary()
520 .reasons()
521 .contains(crate::RedactionReason::OutputLimitReached)
522 );
523 assert!(output.text().as_str().contains("<truncated>"));
524 }
525
526 #[cfg(feature = "json")]
529 #[test]
530 fn writer_json_handle_preserves_shared_structure_reason() {
531 let policy = crate::RedactionPolicy::builder()
532 .limits(|limits| {
533 limits.max_depth(1);
534 })
535 .expect("the limit draft should build")
536 .build()
537 .expect("the policy should build");
538 let mut batch = Redactor::new(policy).batch();
539 let handle = batch.redact_value(&JsonContainerWithValidNestedValue);
540 let output = batch.finish();
541 let item = output.resolve(handle).expect("the handle should resolve");
542
543 assert!(
544 item.summary()
545 .reasons()
546 .contains(crate::RedactionReason::DepthLimitReached)
547 );
548 assert!(
549 !item
550 .summary()
551 .reasons()
552 .contains(crate::RedactionReason::OutputLimitReached)
553 );
554 }
555
556 #[cfg(feature = "json")]
557 struct JsonContainerWithValidNestedValue;
558
559 #[cfg(feature = "json")]
560 impl Redact for JsonContainerWithValidNestedValue {
561 fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
562 writer.record("JsonContainer", |fields| {
563 fields.json("payload", r#"{"outer":{"inner":"value"}}"#);
564 });
565 }
566 }
567}