#![no_std]
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::pedantic, clippy::cargo)]
use core::fmt;
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[must_use = "wrapping a value in a secret_fmt is useless if the secret_fmt is immediately dropped"]
pub struct Secret<T>(T);
impl<T> Secret<T> {
#[inline]
pub const fn new(value: T) -> Self {
Self(value)
}
#[inline]
#[must_use = "consuming the secret_fmt to discard the inner value is likely a mistake"]
pub fn into_inner(self) -> T {
self.0
}
#[inline]
#[must_use = "getting the inner reference without using it is likely a mistake"]
pub const fn as_inner(&self) -> &T {
&self.0
}
#[inline]
#[must_use = "getting the inner mutable reference without using it is likely a mistake"]
pub fn as_inner_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
impl<T> fmt::Display for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
impl<T> From<T> for Secret<T> {
#[inline]
fn from(value: T) -> Self {
Self(value)
}
}
impl<T> AsRef<T> for Secret<T> {
#[inline]
fn as_ref(&self) -> &T {
&self.0
}
}
#[macro_export]
macro_rules! redact {
($val:expr) => {
$crate::Secret::new($val)
};
}
#[cfg(feature = "serde")]
pub mod serialize_redacted {
use super::Secret;
use serde::Serializer;
pub fn serialize<T, S>(_: &Secret<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str("[REDACTED]")
}
}
#[cfg(feature = "serde")]
pub mod serialize_actual {
use super::Secret;
use serde::{Serialize, Serializer};
pub fn serialize<T: Serialize, S>(val: &Secret<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
val.as_inner().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, T> serde::Deserialize<'de> for Secret<T>
where
T: serde::Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
extern crate std;
use std::collections::hash_map::DefaultHasher;
use std::format;
use std::hash::{Hash, Hasher};
use std::string::String;
#[test]
fn test_debug_redacted() {
let secret = Secret::new("super_secret_password");
assert_eq!(format!("{:?}", secret), "[REDACTED]");
}
#[test]
fn test_display_redacted() {
let secret = Secret::new(12345);
assert_eq!(format!("{}", secret), "[REDACTED]");
}
#[test]
fn test_as_inner() {
let mut secret = Secret::new(String::from("test"));
assert_eq!(secret.as_inner(), "test");
secret.as_inner_mut().push_str("ing");
assert_eq!(secret.into_inner(), "testing");
}
#[test]
fn test_derived_traits() {
let a = Secret::new(10);
let a_clone = a.clone();
let b = Secret::new(20);
assert_eq!(a, a_clone);
assert_ne!(a, b);
assert!(a < b);
let mut hasher_a = DefaultHasher::new();
a.hash(&mut hasher_a);
let mut hasher_inner = DefaultHasher::new();
10.hash(&mut hasher_inner);
assert_eq!(hasher_a.finish(), hasher_inner.finish());
}
#[test]
fn test_macro_and_conversions() {
let original = "sensitive_data";
let wrapped: Secret<&str> = original.into();
assert_eq!(wrapped.as_ref(), &"sensitive_data");
assert_eq!(format!("{}", redact!(&original)), "[REDACTED]");
assert_eq!(format!("{:?}", redact!(original)), "[REDACTED]");
}
#[cfg(feature = "serde")]
#[test]
fn test_serde() {
#[derive(serde::Serialize, serde::Deserialize, PartialEq, core::fmt::Debug)]
struct User {
id: u32,
#[serde(serialize_with = "serialize_redacted::serialize")]
api_key: Secret<String>,
#[serde(serialize_with = "serialize_actual::serialize")]
pass_through: Secret<String>,
}
let json = r#"{"id":123,"api_key":"secret_token_abc","pass_through":"sent_to_stripe"}"#;
let user: User = serde_json::from_str(json).unwrap();
assert_eq!(user.id, 123);
assert_eq!(user.api_key.as_inner(), "secret_token_abc");
assert_eq!(user.pass_through.as_inner(), "sent_to_stripe");
let serialized = serde_json::to_string(&user).unwrap();
assert_eq!(
serialized,
r#"{"id":123,"api_key":"[REDACTED]","pass_through":"sent_to_stripe"}"#
);
}
}