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}