Skip to main content

openvpn_mgmt_codec/
redacted.rs

1//! A wrapper type that masks sensitive values in `Debug` and `Display`
2//! output to prevent accidental exposure in logs.
3
4use derive_more::{Debug, Display};
5
6/// A string value that prints `<redacted>` in [`Debug`] and [`Display`]
7/// output.
8///
9/// Use [`expose`](Self::expose) to access the inner value when you
10/// genuinely need it (e.g. for sending over the wire). The `Debug` and
11/// `Display` implementations are generated by the [`derive_more`] crate.
12///
13/// # Examples
14///
15/// ```
16/// use openvpn_mgmt_codec::Redacted;
17///
18/// let secret = Redacted::new("hunter2");
19/// assert_eq!(format!("{secret:?}"), "<redacted>");
20/// assert_eq!(format!("{secret}"), "<redacted>");
21/// assert_eq!(secret.expose(), "hunter2");
22/// ```
23#[derive(Clone, PartialEq, Eq, Debug, Display)]
24#[debug("<redacted>")]
25#[display("<redacted>")]
26pub struct Redacted(String);
27
28impl Redacted {
29    /// Wrap a string as redacted.
30    pub fn new(value: impl Into<String>) -> Self {
31        Self(value.into())
32    }
33
34    /// Access the inner value.
35    ///
36    /// Use this when you genuinely need the raw string, for instance when
37    /// encoding it onto the wire. Avoid passing the result to logging or
38    /// display formatting.
39    pub fn expose(&self) -> &str {
40        &self.0
41    }
42
43    /// Consume the wrapper and return the inner string.
44    pub fn into_inner(self) -> String {
45        self.0
46    }
47}
48
49impl From<String> for Redacted {
50    fn from(s: String) -> Self {
51        Self(s)
52    }
53}
54
55impl From<&str> for Redacted {
56    fn from(s: &str) -> Self {
57        Self(s.to_string())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn debug_is_redacted() {
67        let r = Redacted::new("secret");
68        assert_eq!(format!("{r:?}"), "<redacted>");
69    }
70
71    #[test]
72    fn display_is_redacted() {
73        let r = Redacted::new("secret");
74        assert_eq!(format!("{r}"), "<redacted>");
75    }
76
77    #[test]
78    fn expose_returns_inner() {
79        let r = Redacted::new("hunter2");
80        assert_eq!(r.expose(), "hunter2");
81    }
82
83    #[test]
84    fn into_inner_returns_owned() {
85        let r = Redacted::new("pass");
86        assert_eq!(r.into_inner(), "pass");
87    }
88
89    #[test]
90    fn from_string() {
91        let r: Redacted = "hello".to_string().into();
92        assert_eq!(r.expose(), "hello");
93    }
94
95    #[test]
96    fn from_str() {
97        let r: Redacted = "hello".into();
98        assert_eq!(r.expose(), "hello");
99    }
100
101    #[test]
102    fn equality() {
103        let a = Redacted::new("same");
104        let b = Redacted::new("same");
105        let c = Redacted::new("different");
106        assert_eq!(a, b);
107        assert_ne!(a, c);
108    }
109}