Skip to main content

qubit_redact/facade/
redaction_batch_diagnostics.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Fail-closed presentation of independently resolvable batch results.
9
10use std::borrow::Cow;
11
12use super::RedactedText;
13use super::RedactionBatchHandle;
14use super::RedactionBatchOutput;
15use super::RedactionSummary;
16use crate::output::log_escape::escape_log_control_characters;
17
18/// Presents batch items for diagnostics without exposing resolution errors.
19///
20/// A complete item retains its redacted text. An incomplete item, a missing
21/// item, or a handle created by another batch resolves to one caller-selected
22/// marker. This fail-closed behavior is intended for `Debug`, `Display`, logs,
23/// and other presentation paths that cannot recover from individual handle
24/// failures. The public batch contract deliberately maps every unresolved or
25/// incomplete item to the same safe marker.
26///
27/// The marker is escaped once when this object is created, so repeated
28/// resolution neither allocates nor permits control characters to forge log
29/// structure.
30///
31/// # Examples
32///
33/// ```
34/// use qubit_redact::Redactor;
35///
36/// let mut batch = Redactor::strict().batch();
37/// let handle = batch.redact_field("password", "raw-secret");
38/// let diagnostics = batch.finish_for_diagnostics("<redaction incomplete>");
39/// assert_eq!(diagnostics.text(handle).as_str(), "<redacted>");
40/// ```
41pub struct RedactionBatchDiagnostics {
42    /// Strict publication that owns the batch identity, items, and summary.
43    output: RedactionBatchOutput,
44    /// Escaped fallback used for incomplete or unresolvable items.
45    marker: RedactedText,
46}
47
48impl RedactionBatchDiagnostics {
49    /// Creates a diagnostic view over one completed batch publication.
50    ///
51    /// `marker` is escaped immediately and reused for every fail-closed
52    /// resolution.
53    #[must_use]
54    pub(crate) fn new(output: RedactionBatchOutput, marker: &str) -> Self {
55        let marker = escape_log_control_characters(Cow::Borrowed(marker));
56        Self {
57            output,
58            marker: RedactedText::from_escaped(marker.into_owned()),
59        }
60    }
61
62    /// Resolves an item to complete redacted text or the shared marker.
63    ///
64    /// The marker is returned when `handle` belongs to another batch, names a
65    /// missing item, or identifies an item whose completion is `Truncated` or
66    /// `Exhausted`. No allocation occurs during resolution.
67    #[must_use]
68    #[inline]
69    pub fn text(&self, handle: RedactionBatchHandle) -> &RedactedText {
70        self.output
71            .resolve(handle)
72            .ok()
73            .and_then(|output| output.complete_text().ok())
74            .unwrap_or(&self.marker)
75    }
76
77    /// Returns the aggregate accounting summary for the underlying batch.
78    #[must_use]
79    #[inline(always)]
80    pub const fn summary(&self) -> &RedactionSummary {
81        self.output.summary()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use std::ptr;
88
89    use super::RedactionBatchHandle;
90    use crate::Redactor;
91
92    /// A missing item from the same batch reuses the escaped diagnostic marker.
93    #[test]
94    fn test_text_reuses_escaped_marker_for_missing_same_batch_item() {
95        let mut batch = Redactor::standard().batch();
96        let valid = batch.redact_field("name", "Ada");
97        let missing = RedactionBatchHandle {
98            batch_id: valid.batch_id,
99            item_index: usize::MAX,
100        };
101        let diagnostics = batch.finish_for_diagnostics("<redaction\nincomplete>");
102
103        let first = diagnostics.text(missing);
104        let second = diagnostics.text(missing);
105
106        assert_eq!(first.as_str(), "<redaction\\nincomplete>");
107        assert!(ptr::eq(first, second));
108    }
109}