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;