void_core/collab/manifest/
io.rs1use std::fs;
12use std::path::Path;
13
14use crate::collab::Identity;
15use crate::{Result, VoidError};
16
17use super::keys::{ecies_unwrap_key, RepoKey};
18use super::types::Manifest;
19
20pub const MANIFEST_JSON_FILE: &str = "contributors.json";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RepoMode {
26 Uninitialized,
28 Collaboration,
30}
31
32pub fn manifest_exists(void_dir: impl AsRef<Path>) -> bool {
37 void_dir.as_ref().join(MANIFEST_JSON_FILE).exists()
38}
39
40pub fn detect_repo_mode(void_dir: impl AsRef<Path>) -> RepoMode {
45 let void_dir = void_dir.as_ref();
46 if !void_dir.exists() {
47 return RepoMode::Uninitialized;
48 }
49
50 if manifest_exists(void_dir) {
51 RepoMode::Collaboration
52 } else {
53 RepoMode::Uninitialized
54 }
55}
56
57pub fn load_manifest(void_dir: impl AsRef<Path>) -> Result<Option<Manifest>> {
67 let json_path = void_dir.as_ref().join(MANIFEST_JSON_FILE);
68 if json_path.exists() {
69 let content = fs::read(&json_path).map_err(|e| {
70 VoidError::Io(std::io::Error::new(
71 e.kind(),
72 format!("failed to read manifest: {}", e),
73 ))
74 })?;
75
76 let manifest: Manifest = serde_json::from_slice(&content).map_err(|e| {
77 VoidError::Serialization(format!("failed to parse manifest: {}", e))
78 })?;
79
80 return Ok(Some(manifest));
81 }
82
83 Ok(None)
84}
85
86pub fn save_manifest(void_dir: impl AsRef<Path>, manifest: &Manifest) -> Result<()> {
99 let void_dir = void_dir.as_ref();
100 let path = void_dir.join(MANIFEST_JSON_FILE);
101 let temp_path = void_dir.join(format!("{}.tmp", MANIFEST_JSON_FILE));
102
103 let content = serde_json::to_vec_pretty(manifest).map_err(|e| {
105 VoidError::Serialization(format!("failed to serialize manifest: {}", e))
106 })?;
107
108 fs::write(&temp_path, &content).map_err(|e| {
110 VoidError::Io(std::io::Error::new(
111 e.kind(),
112 format!("failed to write manifest temp file: {}", e),
113 ))
114 })?;
115
116 fs::rename(&temp_path, &path).map_err(|e| {
117 let _ = fs::remove_file(&temp_path);
119 VoidError::Io(std::io::Error::new(
120 e.kind(),
121 format!("failed to rename manifest file: {}", e),
122 ))
123 })?;
124
125 Ok(())
126}
127
128pub fn delete_manifest(void_dir: impl AsRef<Path>) -> Result<()> {
136 let path = void_dir.as_ref().join(MANIFEST_JSON_FILE);
137 if path.exists() {
138 fs::remove_file(&path)?;
139 }
140
141 Ok(())
142}
143
144pub fn load_repo_key(void_dir: impl AsRef<Path>, identity: Option<&Identity>) -> Result<RepoKey> {
161 let void_dir = void_dir.as_ref();
162 match detect_repo_mode(void_dir) {
163 RepoMode::Uninitialized => Err(VoidError::NotInitialized),
164 RepoMode::Collaboration => {
165 let identity = identity.ok_or_else(|| {
166 VoidError::InvalidKey("identity required for collaboration mode".into())
167 })?;
168 load_collaboration_key(void_dir, identity)
169 }
170 }
171}
172
173fn load_collaboration_key(void_dir: impl AsRef<Path>, identity: &Identity) -> Result<RepoKey> {
178 let manifest = load_manifest(void_dir)?
179 .ok_or_else(|| VoidError::NotFound("Manifest not found".into()))?;
180
181 let signing_pubkey = identity.signing_pubkey();
183
184 let wrapped_key = manifest
185 .read_keys
186 .wrapped
187 .get(&signing_pubkey)
188 .ok_or_else(|| {
189 VoidError::InvalidKey(
190 "no wrapped key found for this identity in the manifest".into(),
191 )
192 })?;
193
194 Ok(ecies_unwrap_key(wrapped_key, identity)?)
196}
197
198#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::collab::manifest::keys::SigningPubKey;
206 use tempfile::tempdir;
207
208 fn test_manifest() -> Manifest {
209 Manifest::new(SigningPubKey::from_bytes([0xaa; 32]), None)
210 }
211
212 #[test]
213 fn detect_repo_mode_uninitialized() {
214 let mode = detect_repo_mode(Path::new("/nonexistent/path"));
215 assert_eq!(mode, RepoMode::Uninitialized);
216 }
217
218 #[test]
219 fn detect_repo_mode_no_manifest() {
220 let dir = tempdir().unwrap();
221 let void_dir = dir.path().join(".void");
222 fs::create_dir(&void_dir).unwrap();
223
224 let mode = detect_repo_mode(&void_dir);
225 assert_eq!(mode, RepoMode::Uninitialized);
226 }
227
228 #[test]
229 fn detect_repo_mode_collaboration_json() {
230 let dir = tempdir().unwrap();
231 let void_dir = dir.path().join(".void");
232 fs::create_dir(&void_dir).unwrap();
233
234 let manifest_path = void_dir.join(MANIFEST_JSON_FILE);
235 fs::write(manifest_path, b"{}").unwrap();
236
237 let mode = detect_repo_mode(&void_dir);
238 assert_eq!(mode, RepoMode::Collaboration);
239 }
240
241 #[test]
242 fn manifest_exists_false_when_missing() {
243 let dir = tempdir().unwrap();
244 let void_dir = dir.path().join(".void");
245 fs::create_dir(&void_dir).unwrap();
246
247 assert!(!manifest_exists(&void_dir));
248 }
249
250 #[test]
251 fn manifest_exists_true_when_json() {
252 let dir = tempdir().unwrap();
253 let void_dir = dir.path().join(".void");
254 fs::create_dir(&void_dir).unwrap();
255
256 let manifest_path = void_dir.join(MANIFEST_JSON_FILE);
257 fs::write(manifest_path, b"{}").unwrap();
258
259 assert!(manifest_exists(&void_dir));
260 }
261
262 #[test]
263 fn load_manifest_returns_none_when_missing() {
264 let dir = tempdir().unwrap();
265 let void_dir = dir.path().join(".void");
266 fs::create_dir(&void_dir).unwrap();
267
268 let result = load_manifest(&void_dir).unwrap();
269 assert!(result.is_none());
270 }
271
272 #[test]
273 fn save_and_load_manifest_roundtrip() {
274 let dir = tempdir().unwrap();
275 let void_dir = dir.path().join(".void");
276 fs::create_dir(&void_dir).unwrap();
277
278 let manifest = test_manifest();
279
280 save_manifest(&void_dir, &manifest).unwrap();
282 assert!(manifest_exists(&void_dir));
283
284 let loaded = load_manifest(&void_dir).unwrap().unwrap();
286 assert_eq!(loaded.version, manifest.version);
287 assert!(loaded.is_owner(&SigningPubKey::from_bytes([0xaa; 32])));
288 }
289
290 #[test]
291 fn load_manifest_invalid_json_fails() {
292 let dir = tempdir().unwrap();
293 let void_dir = dir.path().join(".void");
294 fs::create_dir(&void_dir).unwrap();
295
296 fs::write(void_dir.join(MANIFEST_JSON_FILE), b"not valid json").unwrap();
298
299 let result = load_manifest(&void_dir);
300 assert!(result.is_err());
301 }
302
303 #[test]
304 fn delete_manifest_removes_file() {
305 let dir = tempdir().unwrap();
306 let void_dir = dir.path().join(".void");
307 fs::create_dir(&void_dir).unwrap();
308
309 let manifest = test_manifest();
310
311 save_manifest(&void_dir, &manifest).unwrap();
312 assert!(manifest_exists(&void_dir));
313
314 delete_manifest(&void_dir).unwrap();
315 assert!(!manifest_exists(&void_dir));
316 }
317
318 #[test]
319 fn delete_manifest_succeeds_when_missing() {
320 let dir = tempdir().unwrap();
321 let void_dir = dir.path().join(".void");
322 fs::create_dir(&void_dir).unwrap();
323
324 delete_manifest(&void_dir).unwrap();
326 }
327
328 #[test]
329 fn atomic_write_no_temp_file_left() {
330 let dir = tempdir().unwrap();
331 let void_dir = dir.path().join(".void");
332 fs::create_dir(&void_dir).unwrap();
333
334 let manifest = test_manifest();
335
336 save_manifest(&void_dir, &manifest).unwrap();
337
338 let temp_path = void_dir.join(format!("{}.tmp", MANIFEST_JSON_FILE));
340 assert!(!temp_path.exists());
341 }
342
343 #[test]
348 fn load_repo_key_uninitialized() {
349 let result = load_repo_key(Path::new("/nonexistent"), None);
350 assert!(result.is_err());
351 }
352
353 #[test]
354 fn load_repo_key_collab_mode_needs_identity() {
355 let dir = tempdir().unwrap();
356 let void_dir = dir.path().join(".void");
357 fs::create_dir(&void_dir).unwrap();
358
359 let manifest = test_manifest();
361 save_manifest(&void_dir, &manifest).unwrap();
362
363 let result = load_repo_key(&void_dir, None);
365 assert!(result.is_err());
366 }
367}