1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//! What:
//! What a snapshot's leave-out made and moved, with identities: the trash directory it created and each directory it
//! renamed into it, as the text a work item carries and the handler reads back.
//! Why:
//! A name of forty hexadecimal digits proves nothing about who made a directory. Only what the leave-out itself created or
//! moved may be deleted, so the leave-out writes down the identity of every object it put in the trash and the disposal
//! deletes an object only when it is still the one written down.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const manifest = { name, trash: id, entries: [id0, id1] }; // entry i is named `d${i}` in the trash
//! const text = encodeManifest(manifest); const same = decodeManifest(name, text);
//! ```
use super::fs_beneath::ObjectId;
/// Separates the identities in the encoded manifest; an encoded identity holds only digits, colons and a minus sign.
const SEPARATOR: char = ';';
/// What:
/// The trash directory a leave-out made inside a new worktree and what it moved into it.
/// Why:
/// The disposal needs no scan of the directory it empties: the manifest says which directory is the trash, and which
/// entries of it are the leave-out's.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type TrashManifest = { name: string; trash: ObjectId; entries: ObjectId[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrashManifest {
/// The name of the trash directory below the trash parent: forty lowercase hexadecimal digits.
pub name: String,
/// The identity of the trash directory, taken when the leave-out created it.
pub trash: ObjectId,
/// The identity of each directory moved into the trash, in the order moved; the entry at index `i` is named by
/// `entry_name(i)`.
pub entries: Vec<ObjectId>,
}
/// The name, inside the trash directory, of the entry moved `index`th.
pub fn entry_name(index: usize) -> String {
return format!("d{index}");
}
/// Whether a name is one the leave-out gives a trash directory: forty lowercase hexadecimal digits.
pub fn is_trash_name(name: &str) -> bool {
return name.len() == 40 && name.bytes().all(|byte: u8| { return byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte); });
}
/// Methods over a manifest.
impl TrashManifest {
/// The manifest as the text a work item carries: the identity of the trash directory, then one identity per entry.
pub fn encode(&self) -> String {
let mut parts: Vec<String> = vec![self.trash.encode()];
parts.extend(self.entries.iter().map(ObjectId::encode));
return parts.join(&SEPARATOR.to_string());
}
/// The manifest of the trash directory `name` from its encoded text; the reason is the error.
pub fn decode(name: &str, text: &str) -> Result<TrashManifest, String> {
if !is_trash_name(name) {
return Err(format!("{name} is not the name of a trash directory"));
}
let mut parts = text.split(SEPARATOR);
let trash: ObjectId = parts.next().and_then(ObjectId::decode).ok_or_else(|| { return format!("the record of the trash {name} does not say which directory it is"); })?;
let mut entries: Vec<ObjectId> = Vec::new();
for part in parts {
entries.push(ObjectId::decode(part).ok_or_else(|| { return format!("the record of the trash {name} holds an identity that cannot be read: {part}"); })?);
}
return Ok(TrashManifest { name: name.to_string(), trash, entries });
}
}
/// Manifest controls stay out of the release executable.
#[cfg(test)]
#[path = "worktree_trash_manifest_tests.rs"]
mod tests;