Skip to main content

qubit_redact/domain/
bounded_redacted_display.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//! Byte-bounded display adapter for an already-redacted view.
9
10use std::fmt::{
11    self,
12    Debug,
13    Display,
14    Formatter,
15    Write as _,
16};
17
18use crate::{
19    LogOutputLimit,
20    text::internal::BoundedLogEscapeWriter,
21};
22
23use super::internal::with_mask_byte_limit;
24
25/// A redacted formatting view whose log-safe output cannot exceed a byte limit.
26///
27/// The limit includes the complete `<truncated>` marker. Truncation preserves
28/// UTF-8 character boundaries and never splits a generated escape sequence.
29///
30/// # Type Parameters
31///
32/// * `D` - Already-redacted debug value rendered by this adapter.
33#[must_use = "format the bounded redacted view"]
34pub struct BoundedRedactedDisplay<D> {
35    /// Already-redacted debug view to render.
36    value: D,
37    /// Validated maximum output byte count.
38    limit: LogOutputLimit,
39}
40
41impl<D> BoundedRedactedDisplay<D> {
42    /// Creates a bounded display adapter around an already-redacted view.
43    ///
44    /// # Parameters
45    ///
46    /// * `value` - Redacted view whose compact debug representation is safe.
47    /// * `limit` - Validated maximum output byte count.
48    ///
49    /// # Returns
50    ///
51    /// A bounded adapter implementing both `Debug` and `Display`.
52    #[inline(always)]
53    pub(crate) const fn new(value: D, limit: LogOutputLimit) -> Self {
54        Self { value, limit }
55    }
56}
57
58impl<D: Debug> Display for BoundedRedactedDisplay<D> {
59    /// Writes escaped redacted output within the configured byte budget.
60    ///
61    /// # Parameters
62    ///
63    /// * `formatter` - Destination formatting context.
64    ///
65    /// # Returns
66    ///
67    /// The formatter result for the bounded output.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`fmt::Error`] when redacted formatting or the destination
72    /// rejects output.
73    #[inline(always)]
74    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
75        format_bounded(&self.value, self.limit, formatter)
76    }
77}
78
79impl<D: Debug> Debug for BoundedRedactedDisplay<D> {
80    /// Writes the same bounded, log-safe representation as [`Display`].
81    ///
82    /// # Parameters
83    ///
84    /// * `formatter` - Destination formatting context.
85    ///
86    /// # Returns
87    ///
88    /// The formatter result for the bounded output.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`fmt::Error`] when redacted formatting or the destination
93    /// rejects output.
94    #[inline(always)]
95    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
96        format_bounded(&self.value, self.limit, formatter)
97    }
98}
99
100/// Formats a type-erased debug value through one bounded implementation.
101///
102/// # Parameters
103///
104/// * `value` - Already-redacted debug view to format.
105/// * `limit` - Validated maximum output byte count.
106/// * `formatter` - Destination formatting context.
107///
108/// # Returns
109///
110/// The formatter result for the bounded output.
111///
112/// # Errors
113///
114/// Returns [`fmt::Error`] when redacted formatting or the destination rejects
115/// output.
116pub(super) fn format_bounded(
117    value: &dyn Debug,
118    limit: LogOutputLimit,
119    formatter: &mut Formatter<'_>,
120) -> fmt::Result {
121    let mut writer = BoundedLogEscapeWriter::new(limit);
122    let result = with_mask_byte_limit(limit.max_bytes(), || {
123        write!(&mut writer, "{value:?}")
124    });
125    if result.is_err() && !writer.is_truncated() {
126        return Err(fmt::Error);
127    }
128    formatter.write_str(&writer.finish())
129}
130
131/// Formats a redacted debug value with the policy output limit while preserving
132/// the caller's alternate-debug flag.
133///
134/// Unlike [`format_bounded`], this helper preserves the native `Debug` output
135/// rather than applying log escaping. The redacted value is still bounded
136/// before it reaches the caller's formatter.
137pub(super) fn format_debug_bounded(
138    value: &dyn Debug,
139    limit: LogOutputLimit,
140    formatter: &mut Formatter<'_>,
141) -> fmt::Result {
142    let mut writer = BoundedDebugWriter::new(limit);
143    let result = with_mask_byte_limit(limit.max_bytes(), || {
144        if formatter.alternate() {
145            write!(&mut writer, "{value:#?}")
146        } else {
147            write!(&mut writer, "{value:?}")
148        }
149    });
150    if result.is_err() && !writer.is_truncated() {
151        return Err(fmt::Error);
152    }
153    formatter.write_str(&writer.finish())
154}
155
156/// Retains a bounded native debug prefix and appends the truncation marker.
157struct BoundedDebugWriter {
158    output: String,
159    limit: usize,
160    truncated: bool,
161}
162
163impl BoundedDebugWriter {
164    /// Creates an empty bounded debug writer.
165    fn new(limit: LogOutputLimit) -> Self {
166        Self {
167            output: String::new(),
168            limit: limit.max_bytes(),
169            truncated: false,
170        }
171    }
172
173    /// Returns whether a write exceeded the configured limit.
174    fn is_truncated(&self) -> bool {
175        self.truncated
176    }
177
178    /// Finishes the bounded output with a complete truncation marker.
179    fn finish(mut self) -> String {
180        if self.truncated {
181            let marker = "<truncated>";
182            let prefix_limit = self.limit.saturating_sub(marker.len());
183            self.output.truncate(prefix_limit.min(self.output.len()));
184            self.output.push_str(marker);
185        }
186        self.output
187    }
188}
189
190impl fmt::Write for BoundedDebugWriter {
191    /// Appends a complete UTF-8 prefix or marks the output truncated.
192    fn write_str(&mut self, value: &str) -> fmt::Result {
193        if self.truncated {
194            return Err(fmt::Error);
195        }
196        let next_len = self.output.len().saturating_add(value.len());
197        if next_len <= self.limit {
198            self.output.push_str(value);
199            return Ok(());
200        }
201
202        let payload_limit = self.limit.saturating_sub("<truncated>".len());
203        let remaining = payload_limit.saturating_sub(self.output.len());
204        if remaining > 0 {
205            let prefix = value
206                .get(..remaining)
207                .or_else(|| value.get(..floor_char_boundary(value, remaining)))
208                .unwrap_or_default();
209            self.output.push_str(prefix);
210        }
211        self.truncated = true;
212        Err(fmt::Error)
213    }
214}
215
216/// Returns the greatest UTF-8 boundary not greater than `limit`.
217fn floor_char_boundary(value: &str, limit: usize) -> usize {
218    let mut boundary = limit.min(value.len());
219    while boundary > 0 && !value.is_char_boundary(boundary) {
220        boundary -= 1;
221    }
222    boundary
223}