use std::fmt;
#[derive(Clone)]
pub struct Secret<T: Clone = String> {
inner: T,
}
impl<T: Clone> Secret<T> {
pub fn new(v: T) -> Self {
Self { inner: v }
}
pub fn expose(&self) -> &T {
&self.inner
}
}
impl<T: Clone> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
impl<T: Clone> fmt::Display for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
impl<T: Clone> From<T> for Secret<T> {
fn from(v: T) -> Self {
Self { inner: v }
}
}
pub fn redact_in_str(s: &str, secrets: &[&str]) -> String {
let mut out = s.to_string();
for secret in secrets {
if !secret.is_empty() && out.contains(secret) {
out = out.replace(secret, "[REDACTED]");
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_is_redacted() {
let s: Secret = Secret::new("SECRET-VALUE-DO-NOT-LEAK".into());
let formatted = format!("{s:?}");
assert_eq!(formatted, "[REDACTED]");
assert!(!formatted.contains("SECRET-VALUE"));
}
#[test]
fn display_is_redacted() {
let s: Secret = Secret::new("SECRET-VALUE-DO-NOT-LEAK".into());
let formatted = format!("{s}");
assert_eq!(formatted, "[REDACTED]");
}
#[test]
fn debug_of_containing_struct_is_redacted() {
#[derive(Debug)]
#[allow(dead_code)]
struct Carrier {
name: String,
credential: Secret,
}
let c = Carrier {
name: "bearer".into(),
credential: Secret::new("sk_live_abcdef1234567890".into()),
};
let formatted = format!("{c:?}");
assert!(!formatted.contains("sk_live"));
assert!(formatted.contains("[REDACTED]"));
}
#[test]
fn expose_returns_the_raw_value() {
let s: Secret = Secret::new("x".into());
assert_eq!(s.expose(), "x");
}
#[test]
fn redact_in_str_replaces_known_secrets() {
let log_line = "Authorization: Bearer sk_live_abcdef sent to webhook.example.com";
let cleaned = redact_in_str(log_line, &["sk_live_abcdef"]);
assert_eq!(
cleaned,
"Authorization: Bearer [REDACTED] sent to webhook.example.com"
);
}
#[test]
fn redact_in_str_with_empty_secret_is_identity() {
let log_line = "some log line";
let cleaned = redact_in_str(log_line, &[""]);
assert_eq!(cleaned, log_line);
}
#[test]
fn redact_in_str_handles_multiple_secrets() {
let log_line = "cred=sk_live_abc, token=tok_xyz, other=fine";
let cleaned = redact_in_str(log_line, &["sk_live_abc", "tok_xyz"]);
assert_eq!(cleaned, "cred=[REDACTED], token=[REDACTED], other=fine");
}
}