use std::fmt;
#[derive(Clone, PartialEq, Eq)]
pub struct Secret(String);
impl Secret {
pub fn new(value: impl Into<String>) -> Self {
Secret(value.into())
}
#[must_use]
pub fn expose(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<String> for Secret {
fn from(value: String) -> Self {
Secret(value)
}
}
impl From<&str> for Secret {
fn from(value: &str) -> Self {
Secret(value.to_owned())
}
}
const REDACTED: &str = "[REDACTED]";
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
#[cfg(test)]
mod tests {
use super::Secret;
#[test]
fn debug_is_redacted() {
let secret = Secret::new("hunter2");
assert_eq!(format!("{secret:?}"), "[REDACTED]");
assert_eq!(format!("{secret}"), "[REDACTED]");
}
#[test]
fn expose_returns_plaintext() {
let secret = Secret::new("hunter2");
assert_eq!(secret.expose(), "hunter2");
assert!(!secret.is_empty());
assert!(Secret::new("").is_empty());
}
#[test]
fn debug_of_container_does_not_leak() {
#[derive(Debug)]
struct Holder {
#[allow(dead_code)]
password: Secret,
}
let holder = Holder {
password: Secret::new("s3cr3t"),
};
let rendered = format!("{holder:?}");
assert!(!rendered.contains("s3cr3t"), "secret leaked: {rendered}");
assert!(rendered.contains("[REDACTED]"));
}
}