spinoso_securerandom/uuid.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
//! Generator for Version 4 UUIDs.
//!
//! See [RFC 4122], Section 4.4.
//!
//! [RFC 4122]: https://tools.ietf.org/html/rfc4122#section-4.4
use rand::rngs::OsRng;
use rand::{CryptoRng, RngCore};
use scolapasta_hex as hex;
use crate::{Error, RandomBytesError};
/// The number of octets (bytes) in a UUID, as defined in [RFC4122].
///
/// According to RFC4122, a UUID consists of 16 octets (128 bits) and is
/// represented as a hexadecimal string of 32 characters, typically separated by
/// hyphens into five groups: 8-4-4-4-12.
///
/// [RFC4122]: https://tools.ietf.org/html/rfc4122#section-4.1
const OCTETS: usize = 16;
/// The length of an encoded UUID string, including hyphens, as defined in
/// [RFC4122].
///
/// According to RFC4122, an encoded UUID string consists of 36 characters,
/// which includes the hexadecimal digits and four hyphens in the format
/// 8-4-4-4-12.
///
/// [RFC4122]: https://tools.ietf.org/html/rfc4122
const ENCODED_LENGTH: usize = 36;
#[inline]
pub fn v4() -> Result<String, Error> {
fn get_random_bytes<T: RngCore + CryptoRng>(mut rng: T, slice: &mut [u8]) -> Result<(), RandomBytesError> {
rng.try_fill_bytes(slice)?;
Ok(())
}
let mut bytes = [0; OCTETS];
get_random_bytes(OsRng, &mut bytes)?;
// Per RFC 4122, Section 4.4, set bits for version and `clock_seq_hi_and_reserved`.
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
let mut buf = String::new();
buf.try_reserve(ENCODED_LENGTH)?;
let mut iter = bytes.into_iter();
for byte in iter.by_ref().take(4) {
let escaped = hex::escape_byte(byte);
buf.push_str(escaped);
}
buf.push('-');
for byte in iter.by_ref().take(2) {
let escaped = hex::escape_byte(byte);
buf.push_str(escaped);
}
buf.push('-');
for byte in iter.by_ref().take(2) {
let escaped = hex::escape_byte(byte);
buf.push_str(escaped);
}
buf.push('-');
for byte in iter.by_ref().take(2) {
let escaped = hex::escape_byte(byte);
buf.push_str(escaped);
}
buf.push('-');
for byte in iter {
let escaped = hex::escape_byte(byte);
buf.push_str(escaped);
}
debug_assert_eq!(buf.len(), ENCODED_LENGTH, "UUID had unexpected length");
Ok(buf)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
// Number of iterations for UUID generation tests.
//
// Chosen to provide a high level of confidence in the correctness and
// uniqueness of the UUID generation function, striking a balance between
// test coverage and reasonable execution time. Can be adjusted based on
// specific application requirements.
const ITERATIONS: usize = 10240;
#[test]
fn test_v4_returns_valid_uuid() {
for _ in 0..ITERATIONS {
let uuid = v4().unwrap();
// Validate the UUID format
assert_eq!(uuid.len(), 36, "UUID length should be 36 characters");
assert_eq!(&uuid[8..9], "-", "Invalid UUID format");
assert_eq!(&uuid[13..14], "-", "Invalid UUID format");
assert_eq!(&uuid[18..19], "-", "Invalid UUID format");
assert_eq!(&uuid[23..24], "-", "Invalid UUID format");
// Validate that the UUID is version 4
assert_eq!(&uuid[14..15], "4", "Invalid UUID version");
// Validate that the non-hyphen positions are lowercase ASCII alphanumeric characters
for (idx, c) in uuid.chars().enumerate() {
if matches!(idx, 8 | 13 | 18 | 23) {
assert_eq!(c, '-', "Expected hyphen at position {idx}");
} else {
assert!(
matches!(c, '0'..='9' | 'a'..='f'),
"Character at position {idx} should match ASCII numeric and lowercase hex"
);
}
}
}
}
#[test]
fn test_v4_generated_uuids_are_unique() {
let mut generated_uuids = HashSet::with_capacity(ITERATIONS);
for _ in 0..ITERATIONS {
let uuid = v4().unwrap();
// Ensure uniqueness of generated UUIDs
assert!(
generated_uuids.insert(uuid.clone()),
"Generated UUID is not unique: {uuid}"
);
}
}
#[test]
fn test_v4_generated_uuids_are_ascii_only() {
for _ in 0..ITERATIONS {
let uuid = v4().unwrap();
assert!(uuid.is_ascii(), "UUID should consist of only ASCII characters: {uuid}");
}
}
#[test]
fn test_v4_clock_seq_hi_and_reserved() {
for _ in 0..ITERATIONS {
let uuid = v4().unwrap();
// Extract the relevant portion of the generated UUID for comparison
//
// Per the RFC, `clock_seq_hi_and_reserved` is octet 8 (zero indexed).
// Additionally: the two most significant bits (bits 6 and 7) are set
// to zero and one, respectively.
let clock_seq_hi_and_reserved = u8::from_str_radix(&uuid[14..16], 16).unwrap();
// Assert that the two most significant bits are correct
assert_eq!(
clock_seq_hi_and_reserved & 0b1100_0000,
0b0100_0000,
"Incorrect clock_seq_hi_and_reserved bits in v4 UUID"
);
}
}
}