Skip to main content

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}