openvpn-mgmt-codec 0.8.0

OpenVPN management protocol codecs (tokio-util en/decoder implementations).
Documentation
//! A wrapper type that masks sensitive values in `Debug` and `Display`
//! output to prevent accidental exposure in logs.

use derive_more::{Debug, Display};

/// A string value that prints `<redacted>` in [`Debug`] and [`Display`]
/// output.
///
/// Use [`expose`](Self::expose) to access the inner value when you
/// genuinely need it (e.g. for sending over the wire). The `Debug` and
/// `Display` implementations are generated by the [`derive_more`] crate.
///
/// # Examples
///
/// ```
/// use openvpn_mgmt_codec::Redacted;
///
/// let secret = Redacted::new("hunter2");
/// assert_eq!(format!("{secret:?}"), "<redacted>");
/// assert_eq!(format!("{secret}"), "<redacted>");
/// assert_eq!(secret.expose(), "hunter2");
/// ```
#[derive(Clone, PartialEq, Eq, Debug, Display)]
#[debug("<redacted>")]
#[display("<redacted>")]
pub struct Redacted(String);

impl Redacted {
    /// Wrap a string as redacted.
    pub fn new(value: impl Into<String>) -> Self {
        Self(value.into())
    }

    /// Access the inner value.
    ///
    /// Use this when you genuinely need the raw string, for instance when
    /// encoding it onto the wire. Avoid passing the result to logging or
    /// display formatting.
    pub fn expose(&self) -> &str {
        &self.0
    }

    /// Consume the wrapper and return the inner string.
    pub fn into_inner(self) -> String {
        self.0
    }
}

impl From<String> for Redacted {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for Redacted {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn debug_is_redacted() {
        let redacted = Redacted::new("secret");
        assert_eq!(format!("{redacted:?}"), "<redacted>");
    }

    #[test]
    fn display_is_redacted() {
        let redacted = Redacted::new("secret");
        assert_eq!(format!("{redacted}"), "<redacted>");
    }

    #[test]
    fn expose_returns_inner() {
        let redacted = Redacted::new("hunter2");
        assert_eq!(redacted.expose(), "hunter2");
    }

    #[test]
    fn into_inner_returns_owned() {
        let redacted = Redacted::new("pass");
        assert_eq!(redacted.into_inner(), "pass");
    }

    #[test]
    fn from_string() {
        let redacted: Redacted = "hello".to_string().into();
        assert_eq!(redacted.expose(), "hello");
    }

    #[test]
    fn from_str() {
        let redacted: Redacted = "hello".into();
        assert_eq!(redacted.expose(), "hello");
    }

    #[test]
    fn equality() {
        let first = Redacted::new("same");
        let second = Redacted::new("same");
        let third = Redacted::new("different");
        assert_eq!(first, second);
        assert_ne!(first, third);
    }
}