Skip to main content

git_policy_cli/foundation/
random_id.rs

1//! What:
2//!  Random identifiers in the layout of the incumbent's `randomUUID`:
3//!  lock tokens and the unique names of worktree-copy and auto-push scratch files,
4//!  and the check for that layout.
5//! Why:
6//!  A lock token must be unguessable and unique per acquisition,
7//!  and the startup step recognizes the directories an earlier release named with a transaction ID in the same canonical
8//!  layout (`is_canonical_uuid`, `startup_gate.rs`).
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! import { randomUUID } from 'node:crypto'; const token = randomUUID();
13//! ```
14
15/// What:
16///  Why no identifier could be made:
17///  the operating system's random source failed.
18/// Why:
19///   Without randomness no unguessable token exists,
20///  so the caller must stop.
21///
22/// In TS you'd write (pseudocode):
23/// ```ts
24/// class RandomSourceError extends Error {}
25/// ```
26#[derive(Debug)]
27pub struct RandomSourceError(pub String);
28
29/// Lowercase hexadecimal digits,
30///  the alphabet `randomUUID` prints.
31const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
32
33/// What:
34///  Format 16 bytes as a version-4 UUID in canonical lowercase layout.
35///       `[u8; 16]` is a fixed array of sixteen bytes.
36/// Why:
37///   RFC 9562 version 4:
38///  the version nibble is 4 and the variant bits are `10`,
39///  exactly
40///       what `randomUUID` produces;
41///  the formatting is pure so tests can fix the input.
42///
43/// In TS you'd write (pseudocode):
44/// ```ts
45/// function formatUuid(bytes: Uint8Array): string;
46/// ```
47pub fn format_uuid_v4(bytes: [u8; 16]) -> String {
48    // `mut` allows setting the version and variant bits in place.
49    let mut value: [u8; 16] = bytes;
50    value[6] = (value[6] & 0x0f) | 0x40;
51    value[8] = (value[8] & 0x3f) | 0x80;
52    let mut text: String = String::with_capacity(36);
53    for (index, byte) in value.iter().enumerate() {
54        if index == 4 || index == 6 || index == 8 || index == 10 {
55            text.push('-');
56        }
57        // Each byte is two digits: the high nibble, then the low nibble.
58        text.push(char::from(HEX_DIGITS[usize::from(byte >> 4)]));
59        text.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)]));
60    }
61    return text;
62}
63
64/// What:
65///  A fresh random version-4 UUID from the operating system's source.
66///       `Result<String, RandomSourceError>` is "the identifier,
67///  or why not".
68/// Why:
69///   Tokens and transaction IDs must not be predictable or collide between processes.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// function randomUuid(): string { return randomUUID(); }
74/// ```
75pub fn random_uuid() -> Result<String, RandomSourceError> {
76    // `mut` allows the random source to fill the buffer.
77    let mut bytes: [u8; 16] = [0; 16];
78    // `map_err` turns the source's error into this module's error, keeping its text.
79    getrandom::fill(&mut bytes).map_err(random_source_error)?;
80    return Ok(format_uuid_v4(bytes));
81}
82
83/// What:
84///  Describe a random-source failure.
85/// Why:
86///   A named function keeps the conversion out of a closure.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// const randomSourceError = (error: Error) => new RandomSourceError(String(error));
91/// ```
92fn random_source_error(error: getrandom::Error) -> RandomSourceError {
93    return RandomSourceError(format!(
94        "the operating system's random source failed: {error}"
95    ));
96}
97
98/// What:
99///  Whether a name is exactly one canonical lowercase UUID, as `randomUUID` prints it.
100/// Why:
101///   The scratch directories of a commit are named by such an identifier, and a path is recognized as one of them only by that
102///  layout.
103///
104/// In TS you'd write (pseudocode):
105/// ```ts
106/// /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(name)
107/// ```
108pub fn is_canonical_uuid(name: &str) -> bool {
109    let bytes: &[u8] = name.as_bytes();
110    if bytes.len() != 36 {
111        return false;
112    }
113    for (index, byte) in bytes.iter().enumerate() {
114        let separator: bool = index == 8 || index == 13 || index == 18 || index == 23;
115        let valid: bool = if separator {
116            *byte == b'-'
117        } else {
118            byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)
119        };
120        if !valid {
121            return false;
122        }
123    }
124    return true;
125}
126
127/// Formatting controls stay out of the release executable.
128#[cfg(test)]
129#[path = "random_id_tests.rs"]
130mod tests;