1use crate::{codename, crypto, types};
4
5const PUBKEY_DISPLAY_LEN: usize = 12;
7
8#[derive(Debug)]
10pub struct InfoEntry {
11 pub key: String,
12 pub description: String,
13 pub example: Option<String>,
14 pub tags: Vec<String>,
15 pub scoped_recipients: Vec<String>,
17}
18
19#[derive(Debug)]
21pub struct VaultInfo {
22 pub vault_name: String,
23 pub codename: String,
24 pub repo: String,
25 pub created: String,
26 pub recipient_count: usize,
27 pub entries: Vec<InfoEntry>,
28}
29
30pub fn vault_info(
36 raw_bytes: &[u8],
37 tags: &[String],
38 secret_key: Option<&str>,
39) -> Result<VaultInfo, String> {
40 let vault: types::Vault = serde_json::from_slice(raw_bytes).map_err(|e| e.to_string())?;
41
42 let codename = codename::from_bytes(raw_bytes);
43
44 let filtered: Vec<(&String, &types::SchemaEntry)> = if tags.is_empty() {
46 vault.schema.iter().collect()
47 } else {
48 vault
49 .schema
50 .iter()
51 .filter(|(_, e)| e.tags.iter().any(|t| tags.contains(t)))
52 .collect()
53 };
54
55 let meta_data = secret_key.and_then(|sk| {
57 let identity = crypto::parse_identity(sk).ok()?;
58 crate::decrypt_meta(&vault, &identity)
59 });
60
61 let entries = filtered
62 .iter()
63 .map(|(key, entry)| {
64 let scoped_recipients = if let Some(ref meta) = meta_data {
65 vault
66 .secrets
67 .get(key.as_str())
68 .map(|s| {
69 s.scoped
70 .keys()
71 .map(|pk| {
72 meta.recipients.get(pk).cloned().unwrap_or_else(|| {
73 pk.chars().take(PUBKEY_DISPLAY_LEN).collect::<String>()
74 + "\u{2026}"
75 })
76 })
77 .collect()
78 })
79 .unwrap_or_default()
80 } else {
81 vec![]
82 };
83
84 InfoEntry {
85 key: (*key).clone(),
86 description: entry.description.clone(),
87 example: entry.example.clone(),
88 tags: entry.tags.clone(),
89 scoped_recipients,
90 }
91 })
92 .collect();
93
94 Ok(VaultInfo {
95 vault_name: vault.vault_name.clone(),
96 codename,
97 repo: vault.repo.clone(),
98 created: vault.created.clone(),
99 recipient_count: vault.recipients.len(),
100 entries,
101 })
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::collections::BTreeMap;
108
109 fn test_vault_bytes(schema: BTreeMap<String, types::SchemaEntry>) -> Vec<u8> {
110 let vault = types::Vault {
111 version: types::VAULT_VERSION.into(),
112 created: "2026-01-01T00:00:00Z".into(),
113 vault_name: ".murk".into(),
114 repo: "https://github.com/test/repo".into(),
115 recipients: vec!["age1test".into()],
116 schema,
117 secrets: BTreeMap::new(),
118 meta: String::new(),
119 };
120 serde_json::to_vec(&vault).unwrap()
121 }
122
123 #[test]
124 fn vault_info_basic() {
125 let mut schema = BTreeMap::new();
126 schema.insert(
127 "DB_URL".into(),
128 types::SchemaEntry {
129 description: "database url".into(),
130 example: Some("postgres://...".into()),
131 tags: vec!["db".into()],
132 },
133 );
134 let bytes = test_vault_bytes(schema);
135
136 let info = vault_info(&bytes, &[], None).unwrap();
137 assert_eq!(info.vault_name, ".murk");
138 assert!(!info.codename.is_empty());
139 assert_eq!(info.repo, "https://github.com/test/repo");
140 assert_eq!(info.recipient_count, 1);
141 assert_eq!(info.entries.len(), 1);
142 assert_eq!(info.entries[0].key, "DB_URL");
143 assert_eq!(info.entries[0].description, "database url");
144 assert_eq!(info.entries[0].example.as_deref(), Some("postgres://..."));
145 }
146
147 #[test]
148 fn vault_info_tag_filter() {
149 let mut schema = BTreeMap::new();
150 schema.insert(
151 "DB_URL".into(),
152 types::SchemaEntry {
153 description: "db".into(),
154 example: None,
155 tags: vec!["db".into()],
156 },
157 );
158 schema.insert(
159 "API_KEY".into(),
160 types::SchemaEntry {
161 description: "api".into(),
162 example: None,
163 tags: vec!["api".into()],
164 },
165 );
166 let bytes = test_vault_bytes(schema);
167
168 let info = vault_info(&bytes, &["db".into()], None).unwrap();
169 assert_eq!(info.entries.len(), 1);
170 assert_eq!(info.entries[0].key, "DB_URL");
171 }
172
173 #[test]
174 fn vault_info_empty_schema() {
175 let bytes = test_vault_bytes(BTreeMap::new());
176 let info = vault_info(&bytes, &[], None).unwrap();
177 assert!(info.entries.is_empty());
178 }
179
180 #[test]
181 fn vault_info_invalid_json() {
182 let result = vault_info(b"not json", &[], None);
183 assert!(result.is_err());
184 }
185
186 #[test]
187 fn vault_info_valid_json_missing_fields() {
188 let result = vault_info(b"{\"foo\": \"bar\"}", &[], None);
190 assert!(result.is_err());
191 }
192}