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