safelog/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
46
47// TODO: Try making it not Deref and having expose+expose_mut instead; how bad is it?
48
49use educe::Educe;
50#[cfg(feature = "serde")]
51use serde::{Deserialize, Serialize};
52
53mod err;
54mod flags;
55mod impls;
56
57pub use err::Error;
58pub use flags::{Guard, disable_safe_logging, enforce_safe_logging, with_safe_logging_suppressed};
59
60use std::ops::Deref;
61
62/// A `Result` returned by the flag-manipulation functions in `safelog`.
63pub type Result<T> = std::result::Result<T, Error>;
64
65// Re-exported for macros.
66#[doc(hidden)]
67pub use flags::unsafe_logging_enabled;
68
69/// A wrapper type for a sensitive value.
70///
71/// By default, a `Sensitive<T>` behaves the same as a regular `T`, except that
72/// attempts to turn it into a string (via `Display`, `Debug`, etc) all produce
73/// the string `[scrubbed]`.
74///
75/// This behavior can be overridden locally by using
76/// [`with_safe_logging_suppressed`] and globally with [`disable_safe_logging`].
77#[derive(Educe, Clone, Copy)]
78#[educe(
79    Default(bound),
80    Deref,
81    DerefMut,
82    Eq(bound),
83    Hash(bound),
84    Ord(bound),
85    PartialEq(bound),
86    PartialOrd(bound)
87)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89#[cfg_attr(feature = "serde", serde(transparent))]
90pub struct Sensitive<T>(T);
91
92impl<T> Sensitive<T> {
93    /// Create a new `Sensitive<T>`, wrapping a provided `value`.
94    pub fn new(value: T) -> Self {
95        Sensitive(value)
96    }
97
98    /// Extract the inner value from this `Sensitive<T>`.
99    pub fn into_inner(self) -> T {
100        self.0
101    }
102
103    /// Extract the inner value from this `Sensitive<T>`.
104    #[deprecated = "Use the new into_inner method instead"]
105    pub fn unwrap(sensitive: Sensitive<T>) -> T {
106        sensitive.into_inner()
107    }
108
109    /// Converts `&Sensitive<T>` to `Sensitive<&T>`
110    pub fn as_ref(&self) -> Sensitive<&T> {
111        Sensitive(&self.0)
112    }
113
114    /// Return a reference to the inner value
115    //
116    // This isn't `AsRef` or `as_ref` because we don't want to offer "de-sensitivisation"
117    // via what is usually a semantically-neutral interface.
118    pub fn as_inner(&self) -> &T {
119        &self.0
120    }
121}
122
123/// Wrap a value as `Sensitive`.
124///
125/// This function is an alias for [`Sensitive::new`].
126pub fn sensitive<T>(value: T) -> Sensitive<T> {
127    Sensitive(value)
128}
129
130impl<T> From<T> for Sensitive<T> {
131    fn from(value: T) -> Self {
132        Sensitive::new(value)
133    }
134}
135
136/// Helper: Declare one or more Display-like implementations for a
137/// Sensitive-like type.  These implementations will delegate to their std::fmt
138/// types if safe logging is disabled, and write `[scrubbed]` otherwise.
139macro_rules! impl_display_traits {
140    { $($trait:ident),* } => {
141    $(
142        impl<T: std::fmt::$trait> std::fmt::$trait for Sensitive<T> {
143            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144                if flags::unsafe_logging_enabled() {
145                    std::fmt::$trait::fmt(&self.0, f)
146                } else {
147                    write!(f, "[scrubbed]")
148                }
149            }
150        }
151
152        impl<T: std::fmt::$trait> std::fmt::$trait for BoxSensitive<T> {
153            #[inline]
154            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155                std::fmt::$trait::fmt(&*self.0, f)
156            }
157        }
158   )*
159   }
160}
161
162/// A wrapper suitable for logging and including in errors
163///
164/// This is a newtype around `Box<Sensitive<T>>`.
165///
166/// This is useful particularly in errors,
167/// where the box can help reduce the size of error variants
168/// (for example ones containing large values like an `OwnedChanTarget`).
169///
170/// `BoxSensitive<T>` dereferences to [`Sensitive<T>`].
171//
172// Making it be a newtype rather than a type alias allows us to implement
173// `into_inner` and `From<T>` and so on.
174#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
175pub struct BoxSensitive<T>(Box<Sensitive<T>>);
176
177impl<T> From<T> for BoxSensitive<T> {
178    fn from(t: T) -> BoxSensitive<T> {
179        BoxSensitive(Box::new(sensitive(t)))
180    }
181}
182
183impl<T> BoxSensitive<T> {
184    /// Return the innermost `T`
185    pub fn into_inner(self) -> T {
186        // TODO want unstable Box::into_inner(self.0) rust-lang/rust/issues/80437
187        let unboxed = *self.0;
188        unboxed.into_inner()
189    }
190}
191
192impl<T> Deref for BoxSensitive<T> {
193    type Target = Sensitive<T>;
194
195    fn deref(&self) -> &Sensitive<T> {
196        &self.0
197    }
198}
199
200impl_display_traits! {
201    Display, Debug, Binary, Octal, LowerHex, UpperHex, LowerExp, UpperExp, Pointer
202}
203
204/// A `redactable` object is one where we know a way to display _part_ of it
205/// when we are running with safe logging enabled.
206///
207/// For example, instead of referring to a user as `So-and-So` or `[scrubbed]`,
208/// this trait would allow referring to the user as `S[...]`.
209///
210/// # Privacy notes
211///
212/// Displaying some information about an object is always less safe than
213/// displaying no information about it!
214///
215/// For example, in an environment with only a small number of users, the first
216/// letter of a user's name might be plenty of information to identify them
217/// uniquely.
218///
219/// Even if a piece of redacted information is safe on its own, several pieces
220/// of redacted information, when taken together, can be enough for an adversary
221/// to infer more than you want.  For example, if you log somebody's first
222/// initial, month of birth, and last-two-digits of ID number, you have just
223/// discarded 99.9% of potential individuals from the attacker's consideration.
224pub trait Redactable: std::fmt::Display + std::fmt::Debug {
225    /// As `Display::fmt`, but produce a redacted representation.
226    fn display_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
227    /// As `Debug::fmt`, but produce a redacted representation.
228    fn debug_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        self.display_redacted(f)
230    }
231    /// Return a smart pointer that will display or debug this object as its
232    /// redacted form.
233    fn redacted(&self) -> Redacted<&Self> {
234        Redacted(self)
235    }
236    /// Return a smart pointer that redacts this object if `redact` is true.
237    fn maybe_redacted(&self, redact: bool) -> MaybeRedacted<&Self> {
238        if redact {
239            MaybeRedacted(either::Either::Right(Redacted(self)))
240        } else {
241            MaybeRedacted(either::Either::Left(self))
242        }
243    }
244}
245
246impl<'a, T: Redactable + ?Sized> Redactable for &'a T {
247    fn display_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        (*self).display_redacted(f)
249    }
250}
251
252/// A wrapper around a `Redactable` that displays it in redacted format.
253#[derive(Educe, Clone, Copy)]
254#[educe(
255    Default(bound),
256    Deref,
257    DerefMut,
258    Eq(bound),
259    Hash(bound),
260    Ord(bound),
261    PartialEq(bound),
262    PartialOrd(bound)
263)]
264#[derive(derive_more::From)]
265#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
266#[cfg_attr(feature = "serde", serde(transparent))]
267pub struct Redacted<T: Redactable>(T);
268
269impl<T: Redactable> Redacted<T> {
270    /// Create a new `Redacted`.
271    pub fn new(value: T) -> Self {
272        Self(value)
273    }
274
275    /// Consume this wrapper and return its inner value.
276    pub fn unwrap(self) -> T {
277        self.0
278    }
279
280    /// Converts `&Redacted<T>` to `Redacted<&T>`
281    pub fn as_ref(&self) -> Redacted<&T> {
282        Redacted(&self.0)
283    }
284
285    /// Return a reference to the inner value
286    //
287    // This isn't `AsRef` or `as_ref` because we don't want to offer "de-redaction"
288    // via what is usually a semantically-neutral interface.
289    pub fn as_inner(&self) -> &T {
290        &self.0
291    }
292}
293
294impl<T: Redactable> std::fmt::Display for Redacted<T> {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        if flags::unsafe_logging_enabled() {
297            std::fmt::Display::fmt(&self.0, f)
298        } else {
299            self.0.display_redacted(f)
300        }
301    }
302}
303
304impl<T: Redactable> std::fmt::Debug for Redacted<T> {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        if flags::unsafe_logging_enabled() {
307            std::fmt::Debug::fmt(&self.0, f)
308        } else {
309            self.0.debug_redacted(f)
310        }
311    }
312}
313
314/// An object that may or may not be redacted.
315///
316/// Used to implement conditional redaction
317#[derive(Clone, derive_more::Display)]
318pub struct MaybeRedacted<T: Redactable>(either::Either<T, Redacted<T>>);
319
320impl<T: Redactable> std::fmt::Debug for MaybeRedacted<T> {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        use std::fmt::Debug;
323        match &self.0 {
324            either::Either::Left(v) => Debug::fmt(v, f),
325            either::Either::Right(v) => Debug::fmt(v, f),
326        }
327    }
328}
329
330/// A type that can be displayed in a redacted or un-redacted form,
331/// but which forces the caller to choose.
332///
333/// See [`Redactable`] for more discussion on redaction.
334///
335/// Unlike [`Redactable`], this type is "inherently sensitive":
336/// Types implementing `DisplayRedacted` should not typically implement
337/// [`Display`](std::fmt::Display).
338///
339/// For external types that implement `Display`,
340/// or for types which are usually _not_ sensitive,
341/// `Redacted` is likely a better choice.
342pub trait DisplayRedacted {
343    /// As [`Display::fmt`](std::fmt::Display::fmt), but write this object
344    /// in its redacted form.
345    fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
346    /// As [`Display::fmt`](std::fmt::Display::fmt), but write this object
347    /// in its un-redacted form.
348    fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
349
350    // TODO: At some point in the future, when default values are supported for GATs,
351    // it might be good to turn these RPIT functions into associated types.
352
353    /// Return a pointer wrapping this object that can be Displayed in redacted form
354    /// if safe-logging is enabled.
355    ///
356    /// (If safe-logging is not enabled, it will de displayed in its unredacted form.)
357    fn display_redacted(&self) -> impl std::fmt::Display + '_ {
358        DispRedacted(self)
359    }
360    /// Return a pointer wrapping this object that can be Displayed in unredacted form.
361    fn display_unredacted(&self) -> impl std::fmt::Display + '_ {
362        DispUnredacted(self)
363    }
364}
365
366impl<'a, T> DisplayRedacted for &'a T
367where
368    T: DisplayRedacted + ?Sized,
369{
370    fn display_redacted(&self) -> impl std::fmt::Display + '_ {
371        (*self).display_redacted()
372    }
373    fn display_unredacted(&self) -> impl std::fmt::Display + '_ {
374        (*self).display_unredacted()
375    }
376    fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        (*self).fmt_redacted(f)
378    }
379    fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        (*self).fmt_unredacted(f)
381    }
382}
383
384/// A wrapper around a [`DisplayRedacted`] that implements [`Display`](std::fmt::Display)
385/// by displaying the object in its redacted form
386/// if safe-logging is enabled.
387///
388/// (If safe-logging is not enabled, it will de displayed in its unredacted form.)
389#[allow(clippy::exhaustive_structs)]
390#[derive(derive_more::AsRef)]
391pub struct DispRedacted<T: ?Sized>(pub T);
392
393/// A wrapper around a [`DisplayRedacted`] that implements [`Display`](std::fmt::Display)
394/// by displaying the object in its un-redacted form.
395#[allow(clippy::exhaustive_structs)]
396#[derive(derive_more::AsRef)]
397pub struct DispUnredacted<T: ?Sized>(pub T);
398
399impl<T> std::fmt::Display for DispRedacted<T>
400where
401    T: DisplayRedacted + ?Sized,
402{
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        if crate::flags::unsafe_logging_enabled() {
405            self.0.fmt_unredacted(f)
406        } else {
407            self.0.fmt_redacted(f)
408        }
409    }
410}
411
412impl<T> std::fmt::Display for DispUnredacted<T>
413where
414    T: DisplayRedacted + ?Sized,
415{
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        self.0.fmt_unredacted(f)
418    }
419}
420
421/// A type that can be debugged in a redacted or un-redacted form,
422/// but which forces the caller to choose.
423///
424/// See [`Redactable`] for more discussion on redaction.
425///
426/// Unlike [`Redactable`], this type is "inherently sensitive":
427/// [`Debug`](std::fmt::Debug) will display it in redacted or un-redacted format
428/// depending on whether safe logging is enabled.
429///
430/// For external types that implement `Debug`,
431/// or for types which are usually _not_ sensitive,
432/// `Redacted` is likely a better choice.
433pub trait DebugRedacted {
434    /// As [`Debug::fmt`](std::fmt::Debug::fmt), but write this object
435    /// in its redacted form.
436    fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
437    /// As [`Debug::fmt`](std::fmt::Debug::fmt), but write this object
438    /// in its unredacted form.
439    fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
440}
441
442/// Implement [`std::fmt::Debug`] for a type that implements [`DebugRedacted`].
443///
444/// The implementation will use fmt_redacted() when safe-logging is enabled,
445/// and fmt_unredacted() otherwise.
446///
447/// (NOTE we can't just write 'impl<T:DebugRedacted> Debug for T`;
448/// Rust doesn't like it.)
449#[macro_export]
450macro_rules! derive_redacted_debug {
451    {$t:ty} => {
452    impl std::fmt::Debug for $t {
453        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454            if $crate::unsafe_logging_enabled() {
455                $crate::DebugRedacted::fmt_unredacted(self, f)
456            } else {
457                $crate::DebugRedacted::fmt_redacted(self, f)
458            }
459        }
460    }
461}}
462
463#[cfg(test)]
464mod test {
465    // @@ begin test lint list maintained by maint/add_warning @@
466    #![allow(clippy::bool_assert_comparison)]
467    #![allow(clippy::clone_on_copy)]
468    #![allow(clippy::dbg_macro)]
469    #![allow(clippy::mixed_attributes_style)]
470    #![allow(clippy::print_stderr)]
471    #![allow(clippy::print_stdout)]
472    #![allow(clippy::single_char_pattern)]
473    #![allow(clippy::unwrap_used)]
474    #![allow(clippy::unchecked_duration_subtraction)]
475    #![allow(clippy::useless_vec)]
476    #![allow(clippy::needless_pass_by_value)]
477    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
478
479    use super::*;
480    use serial_test::serial;
481    use static_assertions::{assert_impl_all, assert_not_impl_any};
482
483    #[test]
484    fn clone_bound() {
485        // Here we'll make sure that educe bounds work about the way we expect.
486        #[derive(Clone)]
487        struct A;
488        struct B;
489
490        let _x = Sensitive(A).clone();
491        let _y = Sensitive(B);
492
493        assert_impl_all!(Sensitive<A> : Clone);
494        assert_not_impl_any!(Sensitive<B> : Clone);
495    }
496
497    #[test]
498    #[serial]
499    fn debug_vec() {
500        type SVec = Sensitive<Vec<u32>>;
501
502        let mut sv = SVec::default();
503        assert!(sv.is_empty());
504        sv.push(104);
505        sv.push(49);
506        assert_eq!(sv.len(), 2);
507
508        assert!(!flags::unsafe_logging_enabled());
509        assert_eq!(format!("{:?}", &sv), "[scrubbed]");
510        assert_eq!(format!("{:?}", sv.as_ref()), "[scrubbed]");
511        assert_eq!(format!("{:?}", sv.as_inner()), "[104, 49]");
512        let normal = with_safe_logging_suppressed(|| format!("{:?}", &sv));
513        assert_eq!(normal, "[104, 49]");
514
515        let _g = disable_safe_logging().unwrap();
516        assert_eq!(format!("{:?}", &sv), "[104, 49]");
517
518        assert_eq!(sv, SVec::from(vec![104, 49]));
519        assert_eq!(sv.clone().into_inner(), vec![104, 49]);
520        assert_eq!(*sv, vec![104, 49]);
521    }
522
523    #[test]
524    #[serial]
525    #[allow(deprecated)]
526    fn deprecated() {
527        type SVec = Sensitive<Vec<u32>>;
528        let sv = Sensitive(vec![104, 49]);
529
530        assert_eq!(SVec::unwrap(sv), vec![104, 49]);
531    }
532
533    #[test]
534    #[serial]
535    fn display_various() {
536        let val = Sensitive::<u32>::new(0x0ed19a);
537
538        let closure1 = || {
539            format!(
540                "{:?}, {}, {:o}, {:x}, {:X}, {:b}",
541                &val, &val, &val, &val, &val, &val,
542            )
543        };
544        let s1 = closure1();
545        let s2 = with_safe_logging_suppressed(closure1);
546        assert_eq!(
547            s1,
548            "[scrubbed], [scrubbed], [scrubbed], [scrubbed], [scrubbed], [scrubbed]"
549        );
550        assert_eq!(
551            s2,
552            "971162, 971162, 3550632, ed19a, ED19A, 11101101000110011010"
553        );
554
555        let n = 1.0E32;
556        let val = Sensitive::<f64>::new(n);
557        let expect = format!("{:?}, {}, {:e}, {:E}", n, n, n, n);
558        let closure2 = || format!("{:?}, {}, {:e}, {:E}", &val, &val, &val, &val);
559        let s1 = closure2();
560        let s2 = with_safe_logging_suppressed(closure2);
561        assert_eq!(s1, "[scrubbed], [scrubbed], [scrubbed], [scrubbed]");
562        assert_eq!(s2, expect);
563
564        let ptr: *const u8 = std::ptr::null();
565        let val = Sensitive::new(ptr);
566        let expect = format!("{:?}, {:p}", ptr, ptr);
567        let closure3 = || format!("{:?}, {:p}", val, val);
568        let s1 = closure3();
569        let s2 = with_safe_logging_suppressed(closure3);
570        assert_eq!(s1, "[scrubbed], [scrubbed]");
571        assert_eq!(s2, expect);
572    }
573
574    #[test]
575    #[serial]
576    fn box_sensitive() {
577        let b: BoxSensitive<_> = "hello world".into();
578
579        assert_eq!(b.clone().into_inner(), "hello world");
580
581        let closure = || format!("{} {:?}", b, b);
582        assert_eq!(closure(), "[scrubbed] [scrubbed]");
583        assert_eq!(
584            with_safe_logging_suppressed(closure),
585            r#"hello world "hello world""#
586        );
587
588        assert_eq!(b.len(), 11);
589    }
590
591    #[test]
592    #[serial]
593    fn test_redacted() {
594        let localhost = std::net::Ipv4Addr::LOCALHOST;
595        let closure = || format!("{} {:?}", localhost.redacted(), localhost.redacted());
596
597        assert_eq!(closure(), "127.x.x.x 127.x.x.x");
598        assert_eq!(with_safe_logging_suppressed(closure), "127.0.0.1 127.0.0.1");
599
600        let closure = |b| {
601            format!(
602                "{} {:?}",
603                localhost.maybe_redacted(b),
604                localhost.maybe_redacted(b)
605            )
606        };
607        assert_eq!(closure(true), "127.x.x.x 127.x.x.x");
608        assert_eq!(closure(false), "127.0.0.1 127.0.0.1");
609
610        assert_eq!(Redacted::new(localhost).unwrap(), localhost);
611    }
612
613    struct RedactionCheck(u32);
614    impl DisplayRedacted for RedactionCheck {
615        fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
616            write!(f, "{}", self.0)
617        }
618
619        fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620            let v = self.0.to_string();
621            write!(f, "{}xxx", v.chars().next().unwrap())
622        }
623    }
624    impl DebugRedacted for RedactionCheck {
625        fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
626            write!(f, "Num({})", self.display_redacted())
627        }
628
629        fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630            write!(f, "Num({})", self.display_unredacted())
631        }
632    }
633    derive_redacted_debug!(RedactionCheck);
634
635    #[test]
636    #[serial]
637    fn display_redacted() {
638        let n = RedactionCheck(999);
639        assert_eq!(&n.display_unredacted().to_string(), "999");
640        assert_eq!(&n.display_redacted().to_string(), "9xxx");
641        with_safe_logging_suppressed(|| assert_eq!(&n.display_redacted().to_string(), "999"));
642
643        assert_eq!(DispRedacted(&n).to_string(), "9xxx");
644        assert_eq!(DispUnredacted(&n).to_string(), "999");
645
646        assert_eq!(&format!("{n:?}"), "Num(9xxx)");
647        with_safe_logging_suppressed(|| assert_eq!(&format!("{n:?}"), "Num(999)"));
648    }
649}