confium_registry/
trust.rs1use std::path::{Path, PathBuf};
14
15use crate::error::{Error, Result};
16use crate::manifest::TrustRoot;
17use crate::paths::trust_dir;
18
19pub type TrustStoreEntry = TrustRoot;
21
22pub struct TrustStore {
29 override_home: Option<PathBuf>,
30}
31
32impl TrustStore {
33 pub fn new() -> Self {
35 TrustStore {
36 override_home: None,
37 }
38 }
39
40 pub fn for_home(override_home: PathBuf) -> Self {
42 TrustStore {
43 override_home: Some(override_home),
44 }
45 }
46
47 pub fn dir(&self) -> Result<PathBuf> {
49 trust_dir(self.override_home.as_ref())
50 }
51
52 pub fn list(&self) -> Result<Vec<TrustStoreEntry>> {
54 let dir = self.dir()?;
55 if !dir.exists() {
56 return Ok(Vec::new());
57 }
58 let mut entries = Vec::new();
59 let read = std::fs::read_dir(&dir)
60 .map_err(|e| Error::io(e, format!("failed to read {}", dir.display())))?;
61 for entry in read {
62 let entry = entry.map_err(|e| Error::io(e, "directory iteration error"))?;
63 let path = entry.path();
64 if path.extension().and_then(|e| e.to_str()) != Some("toml") {
65 continue;
66 }
67 if let Ok(root) = self.read_file(&path) {
68 entries.push(root);
69 }
70 }
71 entries.sort_by(|a, b| a.name.cmp(&b.name));
72 Ok(entries)
73 }
74
75 pub fn add(&self, root: TrustStoreEntry) -> Result<()> {
77 let dir = self.dir()?;
78 std::fs::create_dir_all(&dir)
79 .map_err(|e| Error::io(e, format!("failed to create {}", dir.display())))?;
80 let path = self.path_for(&root.name);
81 let body = toml::to_string(&root).map_err(|e| Error::TomlSerialize { source: e })?;
84 std::fs::write(&path, body)
85 .map_err(|e| Error::io(e, format!("failed to write {}", path.display())))?;
86 Ok(())
87 }
88
89 pub fn remove(&self, name: &str) -> Result<bool> {
91 let path = self.path_for(name);
92 if !path.exists() {
93 return Ok(false);
94 }
95 std::fs::remove_file(&path)
96 .map_err(|e| Error::io(e, format!("failed to remove {}", path.display())))?;
97 Ok(true)
98 }
99
100 pub fn contains(&self, name: &str) -> Result<bool> {
102 Ok(self.path_for(name).exists())
103 }
104
105 fn path_for(&self, name: &str) -> PathBuf {
106 let safe: String = name
108 .chars()
109 .map(|c| {
110 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
111 c
112 } else {
113 '_'
114 }
115 })
116 .collect();
117 self.dir()
118 .unwrap_or_else(|_| PathBuf::from("."))
119 .join(format!("{safe}.toml"))
120 }
121
122 fn read_file(&self, path: &Path) -> Result<TrustStoreEntry> {
123 let body = std::fs::read_to_string(path)
124 .map_err(|e| Error::io(e, format!("failed to read {}", path.display())))?;
125 toml::from_str(&body).map_err(|e| Error::TomlParse {
126 path: path.display().to_string(),
127 source: e,
128 })
129 }
130}
131
132impl Default for TrustStore {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn root(name: &str) -> TrustStoreEntry {
143 TrustRoot {
144 name: name.to_string(),
145 key_id: "0xABCD".to_string(),
146 fingerprint: "AAAA BBBB".to_string(),
147 key_url: format!("/publishers/{name}.asc"),
148 }
149 }
150
151 #[test]
152 fn list_on_missing_dir_is_empty() {
153 let tmp = tempfile::tempdir().unwrap();
154 let store = TrustStore::for_home(PathBuf::from(tmp.path()));
155 assert!(store.list().unwrap().is_empty());
156 }
157
158 #[test]
159 fn add_then_list() {
160 let tmp = tempfile::tempdir().unwrap();
161 let store = TrustStore::for_home(PathBuf::from(tmp.path()));
162 store.add(root("ribose")).unwrap();
163 let entries = store.list().unwrap();
164 assert_eq!(entries.len(), 1);
165 assert_eq!(entries[0].name, "ribose");
166 }
167
168 #[test]
169 fn add_overwrites() {
170 let tmp = tempfile::tempdir().unwrap();
171 let store = TrustStore::for_home(PathBuf::from(tmp.path()));
172 store.add(root("ribose")).unwrap();
173 let mut updated = root("ribose");
174 updated.fingerprint = "CCCC".to_string();
175 store.add(updated).unwrap();
176 let entries = store.list().unwrap();
177 assert_eq!(entries.len(), 1);
178 assert_eq!(entries[0].fingerprint, "CCCC");
179 }
180
181 #[test]
182 fn remove_returns_true_when_present() {
183 let tmp = tempfile::tempdir().unwrap();
184 let store = TrustStore::for_home(PathBuf::from(tmp.path()));
185 store.add(root("ribose")).unwrap();
186 assert!(store.remove("ribose").unwrap());
187 assert!(!store.contains("ribose").unwrap());
188 }
189
190 #[test]
191 fn remove_returns_false_when_absent() {
192 let tmp = tempfile::tempdir().unwrap();
193 let store = TrustStore::for_home(PathBuf::from(tmp.path()));
194 assert!(!store.remove("ghost").unwrap());
195 }
196
197 #[test]
198 fn path_for_sanitizes_dangerous_names() {
199 let tmp = tempfile::tempdir().unwrap();
200 let store = TrustStore::for_home(PathBuf::from(tmp.path()));
201 let path = store.path_for("../etc/passwd");
202 let name = path.file_name().unwrap().to_str().unwrap();
203 assert!(!name.contains('/'));
204 assert!(!name.contains(".."));
205 }
206}