tuwunel_core/utils/debug.rs
1//! Bounded and redacted debug-formatting utilities.
2//!
3//! The wrappers cap debug output from slices and strings before values enter
4//! tracing fields. The exported macro reports optional presence without
5//! exposing contents.
6
7use std::fmt;
8
9/// Wraps a slice for length-limited `Debug` output.
10///
11/// Slices at or below `max_len` keep ordinary slice formatting. Longer slices
12/// show the first `max_len` elements followed by a quoted `"..."` list entry.
13pub struct TruncatedSlice<'a, T> {
14 inner: &'a [T],
15 max_len: usize,
16}
17
18/// Wraps a UTF-8 string for threshold-limited `Debug` output.
19///
20/// Strings no longer than `max_len` bytes keep ordinary quoted formatting.
21/// Longer strings end at the first scalar boundary at or after `max_len`,
22/// including the string's end, then append `...` outside the closing quote.
23pub struct TruncatedStr<'a> {
24 inner: &'a str,
25 max_len: usize,
26}
27
28/// Creates a tracing debug value that truncates a slice.
29///
30/// The returned value can be recorded directly in a structured tracing field.
31/// At most `max_len` slice elements are formatted before the ellipsis marker.
32pub fn slice_truncated<T: fmt::Debug>(
33 slice: &[T],
34 max_len: usize,
35) -> tracing::field::DebugValue<TruncatedSlice<'_, T>> {
36 tracing::field::debug(TruncatedSlice { inner: slice, max_len })
37}
38
39/// Creates a tracing debug value that truncates a string.
40///
41/// The returned value can be recorded directly in a structured tracing field.
42/// Strings exceeding the byte threshold end at the first UTF-8 boundary at or
43/// after it, including the string's end, with `...` outside the closing quote.
44#[must_use]
45pub fn str_truncated(s: &str, max_len: usize) -> tracing::field::DebugValue<TruncatedStr<'_>> {
46 tracing::field::debug(TruncatedStr { inner: s, max_len })
47}
48
49/// Produces a debug label for an optional value without revealing its contents.
50///
51/// The macro expands to `"Some(<redacted>)"` when the identifier reports a
52/// present value and to `"None"` otherwise. Its argument must be an identifier
53/// supporting an `is_some` method.
54#[macro_export]
55macro_rules! redacted_debug {
56 ($f:ident) => {
57 if $f.is_some() { "Some(<redacted>)" } else { "None" }
58 };
59}
60
61impl<T: fmt::Debug> fmt::Debug for TruncatedSlice<'_, T> {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 if self.inner.len() <= self.max_len {
64 write!(f, "{:?}", self.inner)
65 } else {
66 f.debug_list()
67 .entries(&self.inner[..self.max_len])
68 .entry(&"...")
69 .finish()
70 }
71 }
72}
73
74impl fmt::Debug for TruncatedStr<'_> {
75 #[expect(clippy::string_slice)]
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 if self.inner.len() <= self.max_len {
78 write!(f, "{:?}", self.inner)
79 } else {
80 let len = self.inner.ceil_char_boundary(self.max_len);
81
82 write!(f, "{:?}...", &self.inner[..len])
83 }
84 }
85}