1use crate::error::{Error, Result};
7use secrecy::SecretString;
8use std::collections::HashMap;
9use std::fmt;
10
11const MAX_COMPONENT_LEN: usize = 256;
14
15#[derive(Clone, PartialEq, Eq, Hash)]
37pub struct SecretReference {
38 vault: String,
39 item: String,
40 section: Option<String>,
41 field: String,
42 raw: String,
43}
44
45impl SecretReference {
46 pub fn parse(reference: &str) -> Result<Self> {
71 if !reference.starts_with("op://") {
73 return Err(Error::InvalidReference {
74 reference: reference.to_string(),
75 reason: "secret reference must start with 'op://'".to_string(),
76 });
77 }
78
79 let path = &reference[5..]; if path.is_empty() {
83 return Err(Error::InvalidReference {
84 reference: reference.to_string(),
85 reason: "secret reference path is empty".to_string(),
86 });
87 }
88
89 let parts: Vec<&str> = path.split('/').collect();
90
91 for (i, part) in parts.iter().enumerate() {
93 if part.is_empty() {
94 return Err(Error::InvalidReference {
95 reference: reference.to_string(),
96 reason: format!("empty component at position {}", i + 1),
97 });
98 }
99 if part.len() > MAX_COMPONENT_LEN {
100 return Err(Error::InvalidReference {
101 reference: reference.to_string(),
102 reason: format!(
103 "component at position {} exceeds maximum length of {} bytes",
104 i + 1,
105 MAX_COMPONENT_LEN
106 ),
107 });
108 }
109 }
110
111 match parts.len() {
112 3 => Ok(Self {
113 vault: parts[0].to_string(),
114 item: parts[1].to_string(),
115 section: None,
116 field: parts[2].to_string(),
117 raw: reference.to_string(),
118 }),
119 4 => Ok(Self {
120 vault: parts[0].to_string(),
121 item: parts[1].to_string(),
122 section: Some(parts[2].to_string()),
123 field: parts[3].to_string(),
124 raw: reference.to_string(),
125 }),
126 n if n < 3 => Err(Error::InvalidReference {
127 reference: reference.to_string(),
128 reason: format!(
129 "expected at least 3 path components (vault/item/field), found {n}"
130 ),
131 }),
132 n => Err(Error::InvalidReference {
133 reference: reference.to_string(),
134 reason: format!(
135 "expected 3 or 4 path components (vault/item/[section/]field), found {n}"
136 ),
137 }),
138 }
139 }
140
141 pub fn vault(&self) -> &str {
143 &self.vault
144 }
145
146 pub fn item(&self) -> &str {
148 &self.item
149 }
150
151 pub fn section(&self) -> Option<&str> {
153 self.section.as_deref()
154 }
155
156 pub fn field(&self) -> &str {
158 &self.field
159 }
160
161 pub fn as_str(&self) -> &str {
163 &self.raw
164 }
165}
166
167impl fmt::Debug for SecretReference {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.debug_struct("SecretReference")
170 .field("vault", &self.vault)
171 .field("item", &self.item)
172 .field("section", &self.section)
173 .field("field", &self.field)
174 .finish()
175 }
176}
177
178impl fmt::Display for SecretReference {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(f, "{}", self.raw)
181 }
182}
183
184impl AsRef<str> for SecretReference {
185 fn as_ref(&self) -> &str {
186 &self.raw
187 }
188}
189
190pub struct SecretMap {
219 inner: HashMap<String, SecretString>,
220}
221
222impl SecretMap {
223 pub(crate) fn from_pairs<I, S>(pairs: I) -> Self
225 where
226 I: Iterator<Item = (S, SecretString)>,
227 S: Into<String>,
228 {
229 Self {
230 inner: pairs.map(|(k, v)| (k.into(), v)).collect(),
231 }
232 }
233
234 pub fn get(&self, name: &str) -> Option<&SecretString> {
238 self.inner.get(name)
239 }
240
241 pub fn contains(&self, name: &str) -> bool {
243 self.inner.contains_key(name)
244 }
245
246 pub fn names(&self) -> impl Iterator<Item = &str> {
248 self.inner.keys().map(|s| s.as_str())
249 }
250
251 pub fn len(&self) -> usize {
253 self.inner.len()
254 }
255
256 pub fn is_empty(&self) -> bool {
258 self.inner.is_empty()
259 }
260}
261
262impl fmt::Debug for SecretMap {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.debug_struct("SecretMap")
266 .field("keys", &self.inner.keys().collect::<Vec<_>>())
267 .field("count", &self.inner.len())
268 .finish()
269 }
270}
271
272unsafe impl Send for SecretMap {}
275unsafe impl Sync for SecretMap {}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use secrecy::ExposeSecret;
281
282 #[test]
283 fn test_parse_simple_reference() {
284 let reference = SecretReference::parse("op://vault/item/field").unwrap();
285 assert_eq!(reference.vault(), "vault");
286 assert_eq!(reference.item(), "item");
287 assert_eq!(reference.field(), "field");
288 assert!(reference.section().is_none());
289 }
290
291 #[test]
292 fn test_parse_section_reference() {
293 let reference = SecretReference::parse("op://vault/item/section/field").unwrap();
294 assert_eq!(reference.vault(), "vault");
295 assert_eq!(reference.item(), "item");
296 assert_eq!(reference.section(), Some("section"));
297 assert_eq!(reference.field(), "field");
298 }
299
300 #[test]
301 fn test_parse_invalid_prefix() {
302 let result = SecretReference::parse("ops://vault/item/field");
303 assert!(matches!(result, Err(Error::InvalidReference { .. })));
304 }
305
306 #[test]
307 fn test_parse_too_few_parts() {
308 let result = SecretReference::parse("op://vault/item");
309 assert!(matches!(result, Err(Error::InvalidReference { .. })));
310 }
311
312 #[test]
313 fn test_parse_too_many_parts() {
314 let result = SecretReference::parse("op://a/b/c/d/e");
315 assert!(matches!(result, Err(Error::InvalidReference { .. })));
316 }
317
318 #[test]
319 fn test_parse_empty_component() {
320 let result = SecretReference::parse("op://vault//field");
321 assert!(matches!(result, Err(Error::InvalidReference { .. })));
322 }
323
324 #[test]
325 fn test_parse_component_too_long() {
326 let long_vault = "x".repeat(257);
327 let reference = format!("op://{long_vault}/item/field");
328 let result = SecretReference::parse(&reference);
329 assert!(matches!(result, Err(Error::InvalidReference { .. })));
330
331 if let Err(Error::InvalidReference { reason, .. }) = result {
333 assert!(reason.contains("exceeds maximum length"));
334 }
335 }
336
337 #[test]
338 fn test_reference_display() {
339 let reference = SecretReference::parse("op://vault/item/field").unwrap();
340 assert_eq!(reference.to_string(), "op://vault/item/field");
341 }
342
343 #[test]
344 fn test_secret_map_get() {
345 let map = SecretMap::from_pairs(
346 vec![
347 ("key1", SecretString::from("value1")),
348 ("key2", SecretString::from("value2")),
349 ]
350 .into_iter(),
351 );
352
353 assert_eq!(map.get("key1").unwrap().expose_secret(), "value1");
354 assert_eq!(map.get("key2").unwrap().expose_secret(), "value2");
355 assert!(map.get("key3").is_none());
356 }
357
358 #[test]
359 fn test_secret_map_debug_redacted() {
360 let map = SecretMap::from_pairs(
361 vec![("password", SecretString::from("super-secret"))].into_iter(),
362 );
363
364 let debug_output = format!("{map:?}");
365 assert!(!debug_output.contains("super-secret"));
366 assert!(debug_output.contains("password"));
367 }
368
369 #[test]
370 fn test_secret_map_is_send_sync() {
371 fn assert_send_sync<T: Send + Sync>() {}
372 assert_send_sync::<SecretMap>();
373 }
374
375 #[test]
376 fn test_secret_reference_is_send_sync() {
377 fn assert_send_sync<T: Send + Sync>() {}
378 assert_send_sync::<SecretReference>();
379 }
380}