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`, then
22/// append `...` outside the closing quote. Formatting panics if `max_len` lies
23/// within the final multibyte scalar because no later boundary exists.
24pub struct TruncatedStr<'a> {
25	inner: &'a str,
26	max_len: usize,
27}
28
29/// Creates a tracing debug value that truncates a slice.
30///
31/// The returned value can be recorded directly in a structured tracing field.
32/// At most `max_len` slice elements are formatted before the ellipsis marker.
33pub fn slice_truncated<T: fmt::Debug>(
34	slice: &[T],
35	max_len: usize,
36) -> tracing::field::DebugValue<TruncatedSlice<'_, T>> {
37	tracing::field::debug(TruncatedSlice { inner: slice, max_len })
38}
39
40/// Creates a tracing debug value that truncates a string.
41///
42/// The returned value can be recorded directly in a structured tracing field.
43/// Truncation uses a byte threshold and ends at the first following UTF-8
44/// boundary. Formatting panics if the threshold lies within the final
45/// multibyte scalar.
46#[must_use]
47pub fn str_truncated(s: &str, max_len: usize) -> tracing::field::DebugValue<TruncatedStr<'_>> {
48	tracing::field::debug(TruncatedStr { inner: s, max_len })
49}
50
51/// Produces a debug label for an optional value without revealing its contents.
52///
53/// The macro expands to `"Some(<redacted>)"` when the identifier reports a
54/// present value and to `"None"` otherwise. Its argument must be an identifier
55/// supporting an `is_some` method.
56#[macro_export]
57macro_rules! redacted_debug {
58	($f:ident) => {
59		if $f.is_some() { "Some(<redacted>)" } else { "None" }
60	};
61}
62
63impl<T: fmt::Debug> fmt::Debug for TruncatedSlice<'_, T> {
64	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65		if self.inner.len() <= self.max_len {
66			write!(f, "{:?}", self.inner)
67		} else {
68			f.debug_list()
69				.entries(&self.inner[..self.max_len])
70				.entry(&"...")
71				.finish()
72		}
73	}
74}
75
76impl fmt::Debug for TruncatedStr<'_> {
77	#[expect(clippy::string_slice)]
78	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79		if self.inner.len() <= self.max_len {
80			write!(f, "{:?}", self.inner)
81		} else {
82			let len = self
83				.inner
84				.char_indices()
85				.skip_while(|(i, _)| *i < self.max_len)
86				.map(|(i, _)| i)
87				.next()
88				.expect("At least one char_indice >= len for str");
89
90			write!(f, "{:?}...", &self.inner[..len])
91		}
92	}
93}