Skip to main content

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;