use std::{fmt, str::FromStr};
use zeroize::Zeroize;
#[derive(Clone, serde::Deserialize)]
#[serde(transparent)]
pub struct Sensitive<T: Zeroize>(T);
impl<T: Zeroize> Sensitive<T> {
pub const fn new(value: T) -> Self {
Self(value)
}
pub const fn expose(&self) -> &T {
&self.0
}
pub const fn expose_secret(&self) -> &T {
&self.0
}
}
impl Sensitive<String> {
#[must_use]
pub fn filter_nonempty(opt: Option<&Self>) -> Option<Self> {
opt.filter(|s| !s.expose().is_empty()).cloned()
}
}
impl<T: Zeroize> From<T> for Sensitive<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T: Zeroize> fmt::Debug for Sensitive<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Sensitive(<redacted>)")
}
}
impl<T: Zeroize> fmt::Display for Sensitive<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Sensitive(<redacted>)")
}
}
impl<T: Zeroize> Drop for Sensitive<T> {
fn drop(&mut self) {
self.0.zeroize();
}
}
impl FromStr for Sensitive<String> {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::Sensitive;
#[test]
fn debug_and_display_redact() {
let secret = Sensitive::new("super-secret-token".to_string());
let debug = format!("{secret:?}");
let display = format!("{secret}");
assert_eq!(debug, "Sensitive(<redacted>)");
assert_eq!(display, "Sensitive(<redacted>)");
assert!(!debug.contains("super-secret-token"));
assert!(!display.contains("super-secret-token"));
}
#[test]
fn expose_returns_the_value() {
let secret = Sensitive::new("super-secret-token".to_string());
assert_eq!(secret.expose(), "super-secret-token");
assert_eq!(secret.expose_secret(), "super-secret-token");
}
#[test]
fn from_str_delegates_to_inner() {
let secret: Sensitive<String> = "cli-parsed-secret".parse().expect("infallible");
assert_eq!(secret.expose(), "cli-parsed-secret");
}
#[test]
fn filter_nonempty_treats_empty_as_none() {
let empty = Sensitive::new(String::new());
assert!(Sensitive::filter_nonempty(Some(&empty)).is_none());
}
#[test]
fn filter_nonempty_keeps_non_empty() {
let key = Sensitive::new("sk-live-key".to_string());
let filtered = Sensitive::filter_nonempty(Some(&key)).expect("non-empty stays Some");
assert_eq!(filtered.expose(), "sk-live-key");
}
#[test]
fn filter_nonempty_none_stays_none() {
assert!(Sensitive::filter_nonempty(None).is_none());
}
}