1#[cfg(feature = "serde")]
2use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
3use std::fmt;
4use std::str::FromStr;
5use url::Url;
6
7#[derive(Debug)]
9pub enum Error {
10 InvalidUrl(String),
12 ParseError(url::ParseError),
14 RedactError(String),
16}
17
18impl fmt::Display for Error {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Error::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
22 Error::ParseError(e) => write!(f, "Parse error: {}", e),
23 Error::RedactError(msg) => write!(f, "Redact error: {}", msg),
24 }
25 }
26}
27
28impl std::error::Error for Error {
29 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
30 match self {
31 Error::ParseError(e) => Some(e),
32 _ => None,
33 }
34 }
35}
36
37#[derive(Clone, PartialEq, Eq, Hash)]
64pub struct SensitiveUrl {
65 full: Url,
66 redacted: String,
67}
68
69impl fmt::Display for SensitiveUrl {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 self.redacted.fmt(f)
72 }
73}
74
75impl fmt::Debug for SensitiveUrl {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.debug_struct("SensitiveUrl")
78 .field("redacted", &self.redacted)
79 .finish_non_exhaustive()
81 }
82}
83
84#[cfg(feature = "serde")]
85impl Serialize for SensitiveUrl {
86 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87 where
88 S: Serializer,
89 {
90 serializer.serialize_str(self.full.as_ref())
91 }
92}
93
94#[cfg(feature = "serde")]
95impl<'de> Deserialize<'de> for SensitiveUrl {
96 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97 where
98 D: Deserializer<'de>,
99 {
100 let s: String = Deserialize::deserialize(deserializer)?;
101 SensitiveUrl::parse(&s)
102 .map_err(|e| de::Error::custom(format!("Failed to deserialize sensitive URL {:?}", e)))
103 }
104}
105
106impl FromStr for SensitiveUrl {
107 type Err = Error;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 Self::parse(s)
111 }
112}
113
114impl SensitiveUrl {
115 pub fn parse(url: &str) -> Result<Self, Error> {
117 let surl = Url::parse(url).map_err(Error::ParseError)?;
118 SensitiveUrl::new(surl)
119 }
120
121 pub fn new(full: Url) -> Result<Self, Error> {
123 let mut redacted = full.clone();
124 redacted
125 .path_segments_mut()
126 .map_err(|_| Error::InvalidUrl("URL cannot be a base.".to_string()))?
127 .clear();
128 redacted.set_query(None);
129
130 if redacted.has_authority() {
131 redacted
132 .set_username("")
133 .map_err(|_| Error::RedactError("Unable to redact username.".to_string()))?;
134 redacted
135 .set_password(None)
136 .map_err(|_| Error::RedactError("Unable to redact password.".to_string()))?;
137 }
138
139 Ok(Self {
140 full,
141 redacted: redacted.to_string(),
142 })
143 }
144
145 pub fn expose_full(&self) -> &Url {
147 &self.full
148 }
149
150 pub fn redacted(&self) -> &str {
152 &self.redacted
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn redact_remote_url() {
162 let full = "https://user:pass@example.com/example?somequery";
163 let surl = SensitiveUrl::parse(full).unwrap();
164 assert_eq!(surl.to_string(), "https://example.com/");
165 assert_eq!(surl.expose_full().to_string(), full);
166 }
167
168 #[test]
169 fn redact_localhost_url() {
170 let full = "http://user:pass@localhost:5052/";
171 let surl = SensitiveUrl::parse(full).unwrap();
172 assert_eq!(surl.to_string(), "http://localhost:5052/");
173 assert_eq!(surl.expose_full().to_string(), full);
174 }
175
176 #[test]
177 fn test_no_credentials() {
178 let full = "https://example.com/path";
179 let surl = SensitiveUrl::parse(full).unwrap();
180 assert_eq!(surl.to_string(), "https://example.com/");
181 assert_eq!(surl.expose_full().to_string(), full);
182 }
183
184 #[test]
185 fn test_display() {
186 let full = "https://user:pass@example.com/api?token=secret";
187 let surl = SensitiveUrl::parse(full).unwrap();
188
189 let display = surl.to_string();
190 assert_eq!(display, "https://example.com/");
191 }
192
193 #[test]
194 fn test_debug() {
195 let full = "https://user:pass@example.com/api?token=secret";
196 let surl = SensitiveUrl::parse(full).unwrap();
197
198 let debug = format!("{:?}", surl);
199
200 assert_eq!(
201 debug,
202 "SensitiveUrl { redacted: \"https://example.com/\", .. }"
203 );
204 }
205
206 #[cfg(feature = "serde")]
207 mod serde_tests {
208 use super::*;
209
210 #[test]
211 fn test_serialize() {
212 let full = "https://user:pass@example.com/api?token=secret";
213 let surl = SensitiveUrl::parse(full).unwrap();
214
215 let json = serde_json::to_string(&surl).unwrap();
216 assert_eq!(json, format!("\"{}\"", full));
217 }
218
219 #[test]
220 fn test_deserialize() {
221 let full = "https://user:pass@example.com/api?token=secret";
222 let json = format!("\"{}\"", full);
223
224 let surl: SensitiveUrl = serde_json::from_str(&json).unwrap();
225 assert_eq!(surl.expose_full().as_str(), full);
226 }
227
228 #[test]
229 fn test_roundtrip() {
230 let full = "https://user:pass@example.com/api?token=secret";
231 let original = SensitiveUrl::parse(full).unwrap();
232
233 let json = serde_json::to_string(&original).unwrap();
234 let deserialized: SensitiveUrl = serde_json::from_str(&json).unwrap();
235
236 assert_eq!(deserialized.expose_full(), original.expose_full());
237 }
238 }
239}