use crate::error::{Error, Result};
use secrecy::SecretString;
use std::collections::HashMap;
use std::fmt;
const MAX_COMPONENT_LEN: usize = 256;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct SecretReference {
vault: String,
item: String,
section: Option<String>,
field: String,
raw: String,
}
impl SecretReference {
pub fn parse(reference: &str) -> Result<Self> {
if !reference.starts_with("op://") {
return Err(Error::InvalidReference {
reference: reference.to_string(),
reason: "secret reference must start with 'op://'".to_string(),
});
}
let path = &reference[5..];
if path.is_empty() {
return Err(Error::InvalidReference {
reference: reference.to_string(),
reason: "secret reference path is empty".to_string(),
});
}
let parts: Vec<&str> = path.split('/').collect();
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
return Err(Error::InvalidReference {
reference: reference.to_string(),
reason: format!("empty component at position {}", i + 1),
});
}
if part.len() > MAX_COMPONENT_LEN {
return Err(Error::InvalidReference {
reference: reference.to_string(),
reason: format!(
"component at position {} exceeds maximum length of {} bytes",
i + 1,
MAX_COMPONENT_LEN
),
});
}
}
match parts.len() {
3 => Ok(Self {
vault: parts[0].to_string(),
item: parts[1].to_string(),
section: None,
field: parts[2].to_string(),
raw: reference.to_string(),
}),
4 => Ok(Self {
vault: parts[0].to_string(),
item: parts[1].to_string(),
section: Some(parts[2].to_string()),
field: parts[3].to_string(),
raw: reference.to_string(),
}),
n if n < 3 => Err(Error::InvalidReference {
reference: reference.to_string(),
reason: format!(
"expected at least 3 path components (vault/item/field), found {n}"
),
}),
n => Err(Error::InvalidReference {
reference: reference.to_string(),
reason: format!(
"expected 3 or 4 path components (vault/item/[section/]field), found {n}"
),
}),
}
}
pub fn vault(&self) -> &str {
&self.vault
}
pub fn item(&self) -> &str {
&self.item
}
pub fn section(&self) -> Option<&str> {
self.section.as_deref()
}
pub fn field(&self) -> &str {
&self.field
}
pub fn as_str(&self) -> &str {
&self.raw
}
}
impl fmt::Debug for SecretReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretReference")
.field("vault", &self.vault)
.field("item", &self.item)
.field("section", &self.section)
.field("field", &self.field)
.finish()
}
}
impl fmt::Display for SecretReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.raw)
}
}
impl AsRef<str> for SecretReference {
fn as_ref(&self) -> &str {
&self.raw
}
}
pub struct SecretMap {
inner: HashMap<String, SecretString>,
}
impl SecretMap {
pub(crate) fn from_pairs<I, S>(pairs: I) -> Self
where
I: Iterator<Item = (S, SecretString)>,
S: Into<String>,
{
Self {
inner: pairs.map(|(k, v)| (k.into(), v)).collect(),
}
}
pub fn get(&self, name: &str) -> Option<&SecretString> {
self.inner.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.inner.contains_key(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.inner.keys().map(|s| s.as_str())
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl fmt::Debug for SecretMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretMap")
.field("keys", &self.inner.keys().collect::<Vec<_>>())
.field("count", &self.inner.len())
.finish()
}
}
unsafe impl Send for SecretMap {}
unsafe impl Sync for SecretMap {}
#[cfg(test)]
mod tests {
use super::*;
use secrecy::ExposeSecret;
#[test]
fn test_parse_simple_reference() {
let reference = SecretReference::parse("op://vault/item/field").unwrap();
assert_eq!(reference.vault(), "vault");
assert_eq!(reference.item(), "item");
assert_eq!(reference.field(), "field");
assert!(reference.section().is_none());
}
#[test]
fn test_parse_section_reference() {
let reference = SecretReference::parse("op://vault/item/section/field").unwrap();
assert_eq!(reference.vault(), "vault");
assert_eq!(reference.item(), "item");
assert_eq!(reference.section(), Some("section"));
assert_eq!(reference.field(), "field");
}
#[test]
fn test_parse_invalid_prefix() {
let result = SecretReference::parse("ops://vault/item/field");
assert!(matches!(result, Err(Error::InvalidReference { .. })));
}
#[test]
fn test_parse_too_few_parts() {
let result = SecretReference::parse("op://vault/item");
assert!(matches!(result, Err(Error::InvalidReference { .. })));
}
#[test]
fn test_parse_too_many_parts() {
let result = SecretReference::parse("op://a/b/c/d/e");
assert!(matches!(result, Err(Error::InvalidReference { .. })));
}
#[test]
fn test_parse_empty_component() {
let result = SecretReference::parse("op://vault//field");
assert!(matches!(result, Err(Error::InvalidReference { .. })));
}
#[test]
fn test_parse_component_too_long() {
let long_vault = "x".repeat(257);
let reference = format!("op://{long_vault}/item/field");
let result = SecretReference::parse(&reference);
assert!(matches!(result, Err(Error::InvalidReference { .. })));
if let Err(Error::InvalidReference { reason, .. }) = result {
assert!(reason.contains("exceeds maximum length"));
}
}
#[test]
fn test_reference_display() {
let reference = SecretReference::parse("op://vault/item/field").unwrap();
assert_eq!(reference.to_string(), "op://vault/item/field");
}
#[test]
fn test_secret_map_get() {
let map = SecretMap::from_pairs(
vec![
("key1", SecretString::from("value1")),
("key2", SecretString::from("value2")),
]
.into_iter(),
);
assert_eq!(map.get("key1").unwrap().expose_secret(), "value1");
assert_eq!(map.get("key2").unwrap().expose_secret(), "value2");
assert!(map.get("key3").is_none());
}
#[test]
fn test_secret_map_debug_redacted() {
let map = SecretMap::from_pairs(
vec![("password", SecretString::from("super-secret"))].into_iter(),
);
let debug_output = format!("{map:?}");
assert!(!debug_output.contains("super-secret"));
assert!(debug_output.contains("password"));
}
#[test]
fn test_secret_map_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SecretMap>();
}
#[test]
fn test_secret_reference_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SecretReference>();
}
}