1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct SecretRefError;
10
11impl fmt::Display for SecretRefError {
12 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
13 formatter.write_str("secret reference is empty")
14 }
15}
16
17impl Error for SecretRefError {}
18
19#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
21pub struct SecretRef(String);
22
23impl SecretRef {
24 pub fn new(input: impl AsRef<str>) -> Result<Self, SecretRefError> {
30 let trimmed = input.as_ref().trim();
31
32 if trimmed.is_empty() {
33 Err(SecretRefError)
34 } else {
35 Ok(Self(trimmed.to_owned()))
36 }
37 }
38
39 #[must_use]
41 pub fn id(&self) -> &str {
42 &self.0
43 }
44
45 #[must_use]
47 pub fn into_string(self) -> String {
48 self.0
49 }
50}
51
52impl fmt::Debug for SecretRef {
53 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54 formatter.debug_tuple("SecretRef").field(&self.0).finish()
55 }
56}
57
58impl fmt::Display for SecretRef {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(formatter, "secret:{}", self.0)
61 }
62}
63
64impl FromStr for SecretRef {
65 type Err = SecretRefError;
66
67 fn from_str(input: &str) -> Result<Self, Self::Err> {
68 Self::new(input)
69 }
70}
71
72pub struct Redacted<T> {
74 value: T,
75}
76
77impl<T> Redacted<T> {
78 #[must_use]
80 pub const fn new(value: T) -> Self {
81 Self { value }
82 }
83
84 #[must_use]
86 pub const fn expose(&self) -> &T {
87 &self.value
88 }
89
90 #[must_use]
92 pub fn into_inner(self) -> T {
93 self.value
94 }
95}
96
97impl<T: Clone> Clone for Redacted<T> {
98 fn clone(&self) -> Self {
99 Self::new(self.value.clone())
100 }
101}
102
103impl<T: PartialEq> PartialEq for Redacted<T> {
104 fn eq(&self, other: &Self) -> bool {
105 self.value == other.value
106 }
107}
108
109impl<T: Eq> Eq for Redacted<T> {}
110
111impl<T> fmt::Debug for Redacted<T> {
112 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113 formatter.write_str("[redacted]")
114 }
115}
116
117impl<T> fmt::Display for Redacted<T> {
118 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119 formatter.write_str("[redacted]")
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::{Redacted, SecretRef, SecretRefError};
126
127 #[test]
128 fn debug_does_not_leak_wrapped_secret_value() {
129 let value = Redacted::new("very-secret-token");
130
131 assert_eq!(format!("{value:?}"), "[redacted]");
132 assert!(!format!("{value:?}").contains("very-secret-token"));
133 }
134
135 #[test]
136 fn display_does_not_leak_wrapped_secret_value() {
137 let value = Redacted::new("very-secret-token");
138
139 assert_eq!(value.to_string(), "[redacted]");
140 assert!(!value.to_string().contains("very-secret-token"));
141 }
142
143 #[test]
144 fn secret_reference_displays_safe_identifier() {
145 let reference = SecretRef::new(" database/password ").expect("reference should parse");
146
147 assert_eq!(reference.id(), "database/password");
148 assert_eq!(reference.to_string(), "secret:database/password");
149 assert_eq!(SecretRef::new(" "), Err(SecretRefError));
150 }
151
152 #[test]
153 fn equality_works_where_appropriate() {
154 let first = SecretRef::new("api/key").expect("reference should parse");
155 let second = SecretRef::new("api/key").expect("reference should parse");
156 let redacted_first = Redacted::new("same");
157 let redacted_second = Redacted::new("same");
158
159 assert_eq!(first, second);
160 assert_eq!(redacted_first, redacted_second);
161 }
162}