radicle-ci-broker 0.24.0

add integration to CI engins or systems to a Radicle node
Documentation
//! A data type for holding sensitive data to avoid accidentally
//! leaking it.
//!
//! Once a `Sensitive` value has been created, it contains a string
//! value. It can be created by de-serializing using `serde`.
//!
//! The sensitive data can be accessed with [`Sensitive::as_str`]. The
//! caller needs to be careful to not leak that.
//!
//! `Sensitive` value itself can't be printed (via the `Display`
//! trait), even in debug mode (`Debug` trait), or serialized with
//! `serde`. Instead, the value is replaced with the string
//! `<REDACTED>`.
//!
//! Note that this does not prevent the value from ending up in a core
//! dump.

use std::fmt;

use serde::{Deserialize, Deserializer, Serialize, Serializer, de};

const PLACEHOLDER: &str = "<REDACTED>";

/// Hold a sensitive string, such as a password or an API token. The
/// value can be accessed ([`Sensitive::as_str`]), but won't be
/// printed, even in debug output, and won't be serialized by `serde`.
#[derive()]
pub struct Sensitive {
    #[allow(dead_code)]
    //#[serde(serialize_with = "serialize")]
    data: String,
}

impl fmt::Display for Sensitive {
    /// Serialize for normal output.
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "{PLACEHOLDER}")
    }
}

impl fmt::Debug for Sensitive {
    /// Serialize for debug output.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        write!(f, "{PLACEHOLDER}")
    }
}

impl Serialize for Sensitive {
    /// Serialize for `serde`.
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(PLACEHOLDER)
    }
}

impl<'de> Deserialize<'de> for Sensitive {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(SensitiveVisitor)
    }
}

struct SensitiveVisitor;

impl<'de> de::Visitor<'de> for SensitiveVisitor {
    type Value = Sensitive;

    fn expecting(&self, _formatter: &mut fmt::Formatter) -> fmt::Result {
        Ok(())
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        Ok(Sensitive { data: s.into() })
    }
}

impl Sensitive {
    #[cfg(test)]
    fn new(data: &str) -> Self {
        Self { data: data.into() }
    }

    /// Return the contained string in cleartext. Do not leak this string.
    pub fn as_str(&self) -> &str {
        &self.data
    }
}

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

    #[test]
    fn displayed() {
        let s = Sensitive::new("foo");
        let output = format!("{s}");
        assert!(!output.contains("foo"));
    }

    #[test]
    fn debugged() {
        let s = Sensitive::new("foo");
        let output = format!("{s:?}");
        assert!(!output.contains("foo"));
    }

    #[test]
    fn ser() -> Result<(), Box<dyn std::error::Error>> {
        let s = Sensitive::new("foo");
        let output = serde_norway::to_string(&s)?;
        println!("{output:#?}");
        assert!(!output.contains("foo"));
        Ok(())
    }

    #[test]
    fn deser() -> Result<(), Box<dyn std::error::Error>> {
        #[derive(Deserialize)]
        struct Foo {
            secret: Sensitive,
        }
        let s: Foo = serde_norway::from_str("secret: foo")?;
        assert_eq!(s.secret.data, "foo");
        Ok(())
    }
}