use std::path::Path;
use zeroize::Zeroizing;
pub struct Secret(Zeroizing<String>);
impl Secret {
pub fn from_file(path: &Path) -> Result<Self, std::io::Error> {
let contents = std::fs::read_to_string(path)?;
Ok(Self(Zeroizing::new(contents.trim().to_owned())))
}
pub fn new(value: String) -> Self {
Self(Zeroizing::new(value))
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl Clone for Secret {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("[REDACTED]")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("[REDACTED]")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn from_file_and_expose() {
let mut file = tempfile::NamedTempFile::new().unwrap();
writeln!(file, " my-secret-value ").unwrap();
let secret = Secret::from_file(file.path()).unwrap();
assert_eq!(secret.expose(), "my-secret-value");
}
#[test]
fn debug_is_redacted() {
let secret = Secret::new("hunter2".to_string());
assert_eq!(format!("{:?}", secret), "[REDACTED]");
}
#[test]
fn display_is_redacted() {
let secret = Secret::new("hunter2".to_string());
assert_eq!(format!("{}", secret), "[REDACTED]");
}
#[test]
fn nonexistent_file_returns_error() {
let result = Secret::from_file(Path::new("/nonexistent/path"));
assert!(result.is_err());
}
#[test]
fn clone_preserves_value() {
let secret = Secret::new("cloneable".to_string());
let cloned = secret.clone();
assert_eq!(cloned.expose(), "cloneable");
}
}