Skip to main content

rws_server/model/
uuid.rs

1//! [`Uuid`]: a 128-bit RFC 9562 UUID for primary keys (18.30.0).
2//!
3//! rws has its own rather than the `uuid` crate: it isn't in the dependency
4//! tree, and a key needs only parsing, formatting and generation — v7
5//! (time-ordered, so new rows land at the end of a B-tree index) and v4
6//! (random), from the operating system's generator through aws-lc-rs,
7//! which every `model-*` build already links.
8//!
9//! Stored as its 36-character lower-case text on every backend: a `TEXT` /
10//! `CHAR(36)` / `VARCHAR(36)` column. PostgreSQL `UUID` columns are read
11//! too, but rws binds text, which PostgreSQL won't compare with or assign to
12//! `UUID` without a cast — use a text column there.
13
14use super::{DbError, FromColumn, ToColumn, Value};
15
16/// A UUID, kept as its 16 bytes.
17///
18/// ```rust
19/// # extern crate rws_server as rust_web_server;
20/// use rust_web_server::model::Uuid;
21///
22/// let id = Uuid::now_v7();
23/// assert_eq!(id.version(), 7);
24/// assert_eq!(Uuid::parse(&id.to_string()).unwrap(), id);
25/// assert!(Uuid::default().is_nil());
26/// ```
27#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct Uuid([u8; 16]);
29
30impl Uuid {
31    /// `00000000-0000-0000-0000-000000000000`, also `Uuid::default()`: a key
32    /// not chosen yet — `save` generates a v7 in its place.
33    pub const NIL: Uuid = Uuid([0; 16]);
34
35    /// The UUID with these bytes.
36    pub const fn from_bytes(bytes: [u8; 16]) -> Uuid {
37        Uuid(bytes)
38    }
39
40    /// Its 16 bytes.
41    pub const fn as_bytes(&self) -> &[u8; 16] {
42        &self.0
43    }
44
45    /// Whether it is [`Uuid::NIL`].
46    pub fn is_nil(&self) -> bool {
47        self.0 == [0; 16]
48    }
49
50    /// The version field (4 for [`Uuid::new_v4`], 7 for [`Uuid::now_v7`]).
51    pub fn version(&self) -> u8 {
52        self.0[6] >> 4
53    }
54
55    /// A random (version 4) UUID.
56    pub fn new_v4() -> Uuid {
57        let mut bytes = random_bytes();
58        bytes[6] = (bytes[6] & 0x0f) | 0x40;
59        bytes[8] = (bytes[8] & 0x3f) | 0x80;
60        Uuid(bytes)
61    }
62
63    /// A time-ordered (version 7) UUID: the Unix time in milliseconds, then
64    /// 74 random bits. Two made in the same millisecond are in random order.
65    pub fn now_v7() -> Uuid {
66        use std::time::{SystemTime, UNIX_EPOCH};
67        let millis = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0);
68        let mut bytes = random_bytes();
69        bytes[..6].copy_from_slice(&millis.to_be_bytes()[2..]);
70        bytes[6] = (bytes[6] & 0x0f) | 0x70;
71        bytes[8] = (bytes[8] & 0x3f) | 0x80;
72        Uuid(bytes)
73    }
74
75    /// Parse the hyphenated form (`67e55044-10b1-426f-9247-bb680e5fe0c8`),
76    /// either case, or the 32 hex digits without hyphens.
77    pub fn parse(text: &str) -> Result<Uuid, DbError> {
78        parse(text.trim()).ok_or_else(|| {
79            DbError::new(format!("cannot read {text:?} as a UUID: expected 8-4-4-4-12 hex digits, e.g. 67e55044-10b1-426f-9247-bb680e5fe0c8"))
80        })
81    }
82}
83
84fn random_bytes() -> [u8; 16] {
85    use aws_lc_rs::rand::SecureRandom;
86    let mut bytes = [0u8; 16];
87    // The system generator failing is not something a caller can handle;
88    // aws-lc aborts on it itself in FIPS mode.
89    aws_lc_rs::rand::SystemRandom::new().fill(&mut bytes).expect("the operating system's random number generator failed");
90    bytes
91}
92
93fn parse(text: &str) -> Option<Uuid> {
94    let hex: Vec<u8> = match text.len() {
95        36 => {
96            let b = text.as_bytes();
97            if [8, 13, 18, 23].iter().any(|&i| b[i] != b'-') {
98                return None;
99            }
100            b.iter().copied().filter(|&c| c != b'-').collect()
101        }
102        32 => text.as_bytes().to_vec(),
103        _ => return None,
104    };
105    if hex.len() != 32 {
106        return None;
107    }
108    let mut bytes = [0u8; 16];
109    for (i, pair) in hex.chunks(2).enumerate() {
110        let hi = (pair[0] as char).to_digit(16)?;
111        let lo = (pair[1] as char).to_digit(16)?;
112        bytes[i] = (hi * 16 + lo) as u8;
113    }
114    Some(Uuid(bytes))
115}
116
117impl std::fmt::Display for Uuid {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        for (i, b) in self.0.iter().enumerate() {
120            if matches!(i, 4 | 6 | 8 | 10) {
121                f.write_str("-")?;
122            }
123            write!(f, "{b:02x}")?;
124        }
125        Ok(())
126    }
127}
128
129impl std::fmt::Debug for Uuid {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(f, "Uuid({self})")
132    }
133}
134
135impl std::str::FromStr for Uuid {
136    type Err = DbError;
137    fn from_str(s: &str) -> Result<Self, DbError> {
138        Uuid::parse(s)
139    }
140}
141
142impl FromColumn for Uuid {
143    fn from_column(v: Value) -> Result<Self, DbError> {
144        match v {
145            Value::Text(s) => Uuid::parse(&s),
146            Value::Bytes(b) if b.len() == 16 => {
147                let mut bytes = [0u8; 16];
148                bytes.copy_from_slice(&b);
149                Ok(Uuid(bytes))
150            }
151            Value::Null => Err(DbError::new("unexpected NULL for Uuid (use Option<Uuid>)")),
152            other => Err(DbError::new(format!("cannot convert {other:?} to Uuid"))),
153        }
154    }
155}
156
157impl ToColumn for Uuid {
158    fn to_column(&self) -> Value {
159        Value::Text(self.to_string())
160    }
161}
162
163#[cfg(feature = "serde")]
164impl serde::Serialize for Uuid {
165    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166        serializer.collect_str(self)
167    }
168}
169
170#[cfg(feature = "serde")]
171impl<'de> serde::Deserialize<'de> for Uuid {
172    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
173        let text = String::deserialize(deserializer)?;
174        Uuid::parse(&text).map_err(|e| serde::de::Error::custom(e.0))
175    }
176}