Skip to main content

git_policy_cli/
random_id.rs

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