1use std::fs;
9use std::path::Path;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use serde::{Deserialize, Serialize};
13
14use void_crypto::{CommitCid, EncryptedStash};
15
16use crate::crypto;
17use crate::{cid, Result};
18
19const STASH_DIR: &str = "stash";
20const STASH_META_FILE: &str = "meta.bin";
21const REFS_STASH_DIR: &str = "refs/stash";
22
23#[derive(Serialize, Deserialize, Debug, Clone)]
25pub struct StashEntry {
26 pub index: u32,
28 pub commit_cid: CommitCid,
30 pub original_head: CommitCid,
32 pub message: Option<String>,
34 pub timestamp: u64,
36}
37
38#[derive(Serialize, Deserialize, Debug, Clone)]
40pub struct StashStack {
41 pub version: u32,
43 pub entries: Vec<StashEntry>,
45}
46
47impl StashStack {
48 pub const VERSION: u32 = 1;
49
50 pub fn new() -> Self {
52 Self {
53 version: Self::VERSION,
54 entries: Vec::new(),
55 }
56 }
57
58 pub fn len(&self) -> usize {
60 self.entries.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.entries.is_empty()
66 }
67
68 pub fn get(&self, index: u32) -> Option<&StashEntry> {
70 self.entries.iter().find(|e| e.index == index)
71 }
72
73 pub fn push(&mut self, commit_cid: CommitCid, original_head: CommitCid, message: Option<String>) {
77 for entry in &mut self.entries {
79 entry.index += 1;
80 }
81
82 let timestamp = SystemTime::now()
83 .duration_since(UNIX_EPOCH)
84 .map(|d| d.as_secs())
85 .unwrap_or(0);
86
87 self.entries.insert(
88 0,
89 StashEntry {
90 index: 0,
91 commit_cid,
92 original_head,
93 message,
94 timestamp,
95 },
96 );
97 }
98
99 pub fn remove(&mut self, index: u32) -> Option<StashEntry> {
103 let pos = self.entries.iter().position(|e| e.index == index)?;
104 let entry = self.entries.remove(pos);
105
106 for e in &mut self.entries {
108 if e.index > index {
109 e.index -= 1;
110 }
111 }
112
113 Some(entry)
114 }
115
116 pub fn drop(&mut self, index: u32) -> bool {
118 self.remove(index).is_some()
119 }
120}
121
122impl Default for StashStack {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128fn stash_meta_path(void_dir: impl AsRef<Path>) -> std::path::PathBuf {
133 void_dir.as_ref().join(STASH_DIR).join(STASH_META_FILE)
134}
135
136fn stash_ref_path(void_dir: impl AsRef<Path>, index: u32) -> std::path::PathBuf {
137 void_dir.as_ref().join(REFS_STASH_DIR).join(index.to_string())
138}
139
140pub fn read_stash_stack(void_dir: impl AsRef<Path>, key: &crypto::SecretKey) -> Result<StashStack> {
144 let void_dir = void_dir.as_ref();
145 let path = stash_meta_path(void_dir);
146 if !path.exists() {
147 return Ok(StashStack::new());
148 }
149
150 let raw = fs::read(&path)?;
151 let blob = EncryptedStash::from_bytes(raw);
152 Ok(blob.decrypt_and_parse(key.as_bytes())?)
153}
154
155pub fn write_stash_stack(void_dir: impl AsRef<Path>, key: &crypto::SecretKey, stack: &StashStack) -> Result<()> {
157 let void_dir = void_dir.as_ref();
158 let stash_dir = void_dir.join(STASH_DIR);
159 fs::create_dir_all(&stash_dir)?;
160
161 let bytes = crate::support::cbor_to_vec(stack)?;
162 let blob = EncryptedStash::encrypt(key.as_bytes(), &bytes)?;
163
164 let path = stash_meta_path(void_dir);
165 let temp_path = path.with_extension("tmp");
166 fs::write(&temp_path, blob.as_bytes())?;
167 fs::rename(&temp_path, &path)?;
168
169 let refs_dir = void_dir.join(REFS_STASH_DIR);
171 fs::create_dir_all(&refs_dir)?;
172
173 if refs_dir.exists() {
175 for entry in fs::read_dir(&refs_dir)? {
176 let entry = entry?;
177 fs::remove_file(entry.path())?;
178 }
179 }
180
181 for entry in &stack.entries {
183 let ref_path = stash_ref_path(void_dir, entry.index);
184 let cid_obj = cid::from_bytes(entry.commit_cid.as_bytes())?;
185 fs::write(&ref_path, format!("{}\n", cid_obj))?;
186 }
187
188 Ok(())
189}
190
191pub fn clear_stash(void_dir: impl AsRef<Path>, key: &crypto::SecretKey) -> Result<()> {
193 write_stash_stack(void_dir, key, &StashStack::new())
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use tempfile::TempDir;
200
201 fn setup() -> (TempDir, std::path::PathBuf, crypto::SecretKey) {
202 let temp = TempDir::new().unwrap();
203 let void_dir = temp.path().join(".void");
204 fs::create_dir_all(&void_dir).unwrap();
205 let key = crypto::SecretKey::new(crypto::generate_key());
206 (temp, void_dir, key)
207 }
208
209 fn make_cid(data: &[u8]) -> CommitCid {
210 let cid_obj = cid::create(data);
211 CommitCid::from_bytes(cid::to_bytes(&cid_obj))
212 }
213
214 #[test]
215 fn test_stash_stack_new() {
216 let stack = StashStack::new();
217 assert!(stack.is_empty());
218 assert_eq!(stack.len(), 0);
219 assert_eq!(stack.version, StashStack::VERSION);
220 }
221
222 #[test]
223 fn test_stash_stack_push() {
224 let mut stack = StashStack::new();
225
226 let cid1 = make_cid(b"commit1");
227 let head1 = make_cid(b"head1");
228 stack.push(cid1.clone(), head1.clone(), Some("first".to_string()));
229
230 assert_eq!(stack.len(), 1);
231 let entry = stack.get(0).unwrap();
232 assert_eq!(entry.commit_cid, cid1);
233 assert_eq!(entry.original_head, head1);
234 assert_eq!(entry.message, Some("first".to_string()));
235
236 let cid2 = make_cid(b"commit2");
238 let head2 = make_cid(b"head2");
239 stack.push(cid2.clone(), head2.clone(), None);
240
241 assert_eq!(stack.len(), 2);
242
243 let newest = stack.get(0).unwrap();
245 assert_eq!(newest.commit_cid, cid2);
246
247 let oldest = stack.get(1).unwrap();
249 assert_eq!(oldest.commit_cid, cid1);
250 }
251
252 #[test]
253 fn test_stash_stack_remove() {
254 let mut stack = StashStack::new();
255
256 stack.push(make_cid(b"c1"), make_cid(b"h1"), Some("first".to_string()));
257 stack.push(make_cid(b"c2"), make_cid(b"h2"), Some("second".to_string()));
258 stack.push(make_cid(b"c3"), make_cid(b"h3"), Some("third".to_string()));
259
260 let removed = stack.remove(1).unwrap();
262 assert_eq!(removed.message, Some("second".to_string()));
263
264 assert_eq!(stack.len(), 2);
265
266 assert_eq!(stack.get(0).unwrap().message, Some("third".to_string()));
268
269 assert_eq!(stack.get(1).unwrap().message, Some("first".to_string()));
271 }
272
273 #[test]
274 fn test_stash_stack_drop() {
275 let mut stack = StashStack::new();
276 let cid1 = make_cid(b"c1");
277 let cid2 = make_cid(b"c2");
278 stack.push(cid1.clone(), make_cid(b"h1"), None);
279 stack.push(cid2.clone(), make_cid(b"h2"), None);
280
281 assert!(stack.drop(0));
282 assert_eq!(stack.len(), 1);
283 assert_eq!(stack.get(0).unwrap().commit_cid, cid1);
284
285 assert!(!stack.drop(5)); }
287
288 #[test]
289 fn test_stash_roundtrip() {
290 let (_temp, void_dir, key) = setup();
291
292 let cid1 = make_cid(b"commit1");
293 let cid2 = make_cid(b"commit2");
294
295 let mut stack = StashStack::new();
296 stack.push(
297 cid1.clone(),
298 make_cid(b"head1"),
299 Some("test stash".to_string()),
300 );
301 stack.push(cid2.clone(), make_cid(b"head2"), None);
302
303 write_stash_stack(&void_dir, &key, &stack).unwrap();
304 let loaded = read_stash_stack(&void_dir, &key).unwrap();
305
306 assert_eq!(loaded.len(), 2);
307 assert_eq!(loaded.get(0).unwrap().commit_cid, cid2);
308 assert_eq!(
309 loaded.get(1).unwrap().message,
310 Some("test stash".to_string())
311 );
312 }
313
314 #[test]
315 fn test_read_empty_stash() {
316 let (_temp, void_dir, key) = setup();
317
318 let stack = read_stash_stack(&void_dir, &key).unwrap();
319 assert!(stack.is_empty());
320 }
321
322 #[test]
323 fn test_clear_stash() {
324 let (_temp, void_dir, key) = setup();
325
326 let mut stack = StashStack::new();
327 stack.push(make_cid(b"c1"), make_cid(b"h1"), None);
328 write_stash_stack(&void_dir, &key, &stack).unwrap();
329
330 clear_stash(&void_dir, &key).unwrap();
331
332 let loaded = read_stash_stack(&void_dir, &key).unwrap();
333 assert!(loaded.is_empty());
334 }
335}