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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! What:
//! The process that owns one item of a copy transaction (a journal, a stage claim, a temporary file), written into the
//! item's own name or text, and the proof of whether that process still runs.
//! Why:
//! Concurrent adds in one repository must never wait for one another, so no lock stands between them. An item has exactly
//! one owner, and any other command may touch an item only when its owner is proven dead: the live copy of an add is never
//! recovered or reaped, a dead one is. The owner is part of the item's name, so taking over a dead owner's journal is one
//! atomic rename (a single winner, no lock file, no stale lock to retire).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const owner = currentCopyOwner(); // { pid, birthIdentity, pidNamespace }
//! const name = `${id}.${owner.token()}.json`;
//! if (!CopyOwner.fromToken(token).isAlive(path)) { /* the item may be taken over */ }
//! ```
use super::pid_namespace::{current_pid_namespace, foreign_owner_standing};
use super::process_identity::{current_birth_identity, process_birth_identity};
use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
use std::path::Path;
/// What:
/// The process identity that owns an item: the process id, the identity of its birth, and its process-id namespace.
/// Why:
/// A process id alone is reused; the birth identity tells a reused id from the owner, and the namespace tells an owner
/// this process cannot interpret from one that is gone.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type CopyOwner = { pid: number; birthIdentity: string; pidNamespace?: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopyOwner {
/// The owner's process id.
pub pid: i64,
/// The owner's birth identity as the platform reports it.
pub birth_identity: String,
/// The owner's process-id namespace, when the platform has one.
pub pid_namespace: Option<String>,
}
/// The separator between the fields of an owner in a token; it is never a character a field is written with unescaped.
const FIELD_SEPARATOR: char = '~';
/// The escape that introduces two hexadecimal digits for a byte a name cannot carry as it is.
const ESCAPE: char = '%';
/// Whether a byte stands in a token as it is: letters, digits and the few marks the identities use.
/// A dot is never one of them, because a dot separates the parts of a journal's name.
fn is_plain(byte: u8) -> bool {
return byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'-' | b'[' | b']');
}
/// A field of an owner as text a file name carries: plain bytes as they are, every other byte as `%` and two hexadecimal digits.
fn escape_field(field: &str) -> String {
let mut written: String = String::with_capacity(field.len());
for byte in field.bytes() {
if is_plain(byte) {
written.push(char::from(byte));
} else {
written.push_str(format!("{ESCAPE}{byte:02x}").as_str());
}
}
return written;
}
/// The field an escaped text names, or nothing when an escape is cut short, is not hexadecimal, or the bytes are not UTF-8.
fn unescape_field(text: &str) -> Option<String> {
let mut bytes: Vec<u8> = Vec::with_capacity(text.len());
let mut remaining = text.bytes();
while let Some(byte) = remaining.next() {
if char::from(byte) == ESCAPE {
let high: u8 = remaining.next()?;
let low: u8 = remaining.next()?;
let pair: [u8; 2] = [high, low];
bytes.push(u8::from_str_radix(std::str::from_utf8(&pair).ok()?, 16).ok()?);
} else {
bytes.push(byte);
}
}
return String::from_utf8(bytes).ok();
}
/// What:
/// The owner of the running process.
/// Why:
/// Every item this process creates names it, so a later command can tell a live copy from an abandoned one.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function currentCopyOwner(): CopyOwner;
/// ```
///
/// # Errors
/// When the platform cannot report the running process's birth identity.
pub fn current_copy_owner() -> Result<CopyOwner, WorktreeCopyError> {
let birth: String = current_birth_identity().map_err(|error| {
return failure_with_cause("cli-git: the identity of this process is unavailable, so a copy cannot name its owner.", Some(&error));
})?;
return Ok(CopyOwner {
pid: i64::from(std::process::id()),
birth_identity: birth,
pid_namespace: current_pid_namespace(),
});
}
impl CopyOwner {
/// The owner as text that holds no dot, slash, space or other character a file name's parts cannot carry, in about forty
/// characters for a Linux process.
pub fn token(&self) -> String {
let namespace: String = escape_field(self.pid_namespace.as_deref().unwrap_or(""));
return format!("{}{FIELD_SEPARATOR}{}{FIELD_SEPARATOR}{namespace}", self.pid, escape_field(self.birth_identity.as_str()));
}
/// The owner a token names, or nothing when the text is no token (a name an older release wrote, or a stranger's).
pub fn from_token(token: &str) -> Option<CopyOwner> {
let mut fields = token.split(FIELD_SEPARATOR);
let pid: i64 = fields.next()?.parse::<i64>().ok().filter(|found| { return *found >= 1; })?;
let birth: String = unescape_field(fields.next()?).filter(|found| { return !found.is_empty(); })?;
let namespace: String = unescape_field(fields.next()?)?;
if fields.next().is_some() {
return None;
}
return Some(CopyOwner {
pid,
birth_identity: birth,
pid_namespace: if namespace.is_empty() { None } else { Some(namespace) },
});
}
/// What:
/// Whether the owner still runs.
/// Why:
/// An owner in a PID namespace this process cannot see is judged by how recently its item changed (`item`'s age),
/// because its process id means nothing here; any other owner is alive when a process with its id and its birth exists.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// owner.isAlive(itemPath);
/// ```
///
/// # Errors
/// When the platform refuses to report the process.
pub fn is_alive(&self, item: &Path) -> Result<bool, WorktreeCopyError> {
if let Some(standing) = foreign_owner_standing(self.pid_namespace.as_deref(), item) {
return Ok(standing);
}
// `match` on the platform's answer: a live process with the recorded birth is the owner; no process is a dead owner.
match process_birth_identity(self.pid).map_err(|error| return raw_failure(&error))? {
Some(current) => return Ok(current == self.birth_identity),
None => return Ok(false),
}
}
}
/// What:
/// Whether the owner a token names is proven dead; an absent or unreadable token names nobody, so nobody can be alive there.
/// Why:
/// Items written before owners existed, and strangers' files, must not stay in the journal root forever; and an owner the
/// platform will not report on is not proven dead, so its item is kept rather than taken.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function ownerIsDead(token: string | undefined, itemPath: string): boolean;
/// ```
pub fn token_owner_is_dead(token: Option<&str>, item: &Path) -> bool {
let Some(owner) = token.and_then(CopyOwner::from_token) else {
return true;
};
// `match` on the proof: only a clean answer of "not running" is a dead owner.
match owner.is_alive(item) {
Ok(alive) => return !alive,
Err(_unproven) => return false,
}
}
#[cfg(test)]
#[path = "worktree_copy_owner_tests.rs"]
mod tests;