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;