use std::fmt;
#[derive(Clone, PartialEq, Eq, Hash, Default)]
pub struct Redacted<T>(T);
impl<T> Redacted<T> {
pub const fn new(v: T) -> Self {
Redacted(v)
}
pub fn expose(&self) -> &T {
&self.0
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> fmt::Debug for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("***")
}
}
impl<T> fmt::Display for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("***")
}
}
impl<T> From<T> for Redacted<T> {
fn from(v: T) -> Self {
Redacted(v)
}
}
impl<T: serde::Serialize> serde::Serialize for Redacted<T> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Redacted<T> {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
T::deserialize(d).map(Redacted)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_and_display_print_stars_not_the_value() {
let pw = Redacted::new(String::from("hunter2"));
assert_eq!(format!("{pw}"), "***");
assert_eq!(format!("{pw:?}"), "***");
#[derive(Debug)]
#[allow(dead_code)]
struct Cfg {
user: String,
password: Redacted<String>,
}
let c = Cfg {
user: "alice".into(),
password: Redacted::new("supersecret".into()),
};
let dbg = format!("{c:?}");
assert!(dbg.contains("alice"));
assert!(dbg.contains("***"));
assert!(
!dbg.contains("supersecret"),
"Debug of a struct containing Redacted must not leak the secret"
);
}
#[test]
fn expose_returns_the_real_value() {
let key = Redacted::new(vec![0xAAu8; 32]);
assert_eq!(key.expose().len(), 32);
assert_eq!(key.expose()[0], 0xAA);
let raw = key.into_inner();
assert_eq!(raw.len(), 32);
}
#[test]
fn from_impl_and_equality() {
let a: Redacted<u32> = 42.into();
let b = Redacted::new(42u32);
assert_eq!(a, b);
}
#[test]
fn hidden_in_option_and_result_debug() {
let opt: Option<Redacted<&str>> = Some(Redacted::new("secret-token"));
let dbg = format!("{opt:?}");
assert!(dbg.contains("***"));
assert!(!dbg.contains("secret-token"));
}
#[test]
fn serde_is_transparent() {
let raw: String = "hunter2".into();
let wrapped = Redacted::new(raw.clone());
assert_eq!(
serde_json::to_string(&wrapped).unwrap(),
serde_json::to_string(&raw).unwrap(),
);
let s = "\"round-trip\"";
let back: Redacted<String> = serde_json::from_str(s).unwrap();
assert_eq!(back.expose(), "round-trip");
}
}