nautilus_core/
uuid.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A `UUID4` Universally Unique Identifier (UUID) version 4 (RFC 4122).
17
18use std::{
19    ffi::CStr,
20    fmt::{Debug, Display},
21    hash::Hash,
22    io::{Cursor, Write},
23    str::FromStr,
24};
25
26use rand::RngCore;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use uuid::Uuid;
29
30/// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator).
31pub(crate) const UUID4_LEN: usize = 37;
32
33/// Represents a Universally Unique Identifier (UUID)
34/// version 4 based on a 128-bit label as specified in RFC 4122.
35#[repr(C)]
36#[derive(Copy, Clone, Hash, PartialEq, Eq)]
37#[cfg_attr(
38    feature = "python",
39    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core")
40)]
41pub struct UUID4 {
42    /// The UUID v4 value as a fixed-length C string byte array (includes null terminator).
43    pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array
44}
45
46impl UUID4 {
47    /// Creates a new [`UUID4`] instance.
48    ///
49    /// The UUID value is stored as a fixed-length C string byte array.
50    #[must_use]
51    pub fn new() -> Self {
52        let mut rng = rand::rng();
53        let mut bytes = [0u8; 16];
54        rng.fill_bytes(&mut bytes);
55
56        bytes[6] = (bytes[6] & 0x0F) | 0x40; // Set the version to 4
57        bytes[8] = (bytes[8] & 0x3F) | 0x80; // Set the variant to RFC 4122
58
59        let mut value = [0u8; UUID4_LEN];
60        let mut cursor = Cursor::new(&mut value[..36]);
61
62        write!(
63            cursor,
64            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
65            u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
66            u16::from_be_bytes([bytes[4], bytes[5]]),
67            u16::from_be_bytes([bytes[6], bytes[7]]),
68            u16::from_be_bytes([bytes[8], bytes[9]]),
69            u64::from_be_bytes([
70                bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], 0, 0
71            ]) >> 16
72        )
73        .expect("Error writing UUID string to buffer");
74
75        value[36] = 0; // Add the null terminator
76
77        Self { value }
78    }
79
80    /// Converts the [`UUID4`] to a C string reference.
81    ///
82    /// # Panics
83    ///
84    /// Panics if the internal byte array is not a valid C string (does not end with a null terminator).
85    #[must_use]
86    pub fn to_cstr(&self) -> &CStr {
87        // SAFETY: We always store valid C strings
88        CStr::from_bytes_with_nul(&self.value)
89            .expect("UUID byte representation should be a valid C string")
90    }
91
92    /// Returns the UUID as a string slice.
93    #[must_use]
94    pub fn as_str(&self) -> &str {
95        // SAFETY: We always store valid ASCII UUID strings
96        self.to_cstr().to_str().expect("UUID should be valid UTF-8")
97    }
98
99    /// Returns the raw UUID bytes (16 bytes).
100    ///
101    /// This method is optimized for serialization where the UUID bytes
102    /// are needed directly without string conversion overhead.
103    #[must_use]
104    pub fn as_bytes(&self) -> [u8; 16] {
105        // Parse the string representation to extract the raw bytes
106        // This is done once at read time to avoid repeated parsing
107        let uuid_str = self.to_cstr().to_str().expect("Valid UTF-8");
108        let uuid = Uuid::parse_str(uuid_str).expect("Valid UUID4");
109        *uuid.as_bytes()
110    }
111
112    fn validate_v4(uuid: &Uuid) {
113        // Validate this is a v4 UUID
114        assert_eq!(
115            uuid.get_version(),
116            Some(uuid::Version::Random),
117            "UUID is not version 4"
118        );
119
120        // Validate RFC4122 variant
121        assert_eq!(
122            uuid.get_variant(),
123            uuid::Variant::RFC4122,
124            "UUID is not RFC 4122 variant"
125        );
126    }
127
128    fn try_validate_v4(uuid: &Uuid) -> Result<(), String> {
129        if uuid.get_version() != Some(uuid::Version::Random) {
130            return Err("UUID is not version 4".to_string());
131        }
132        if uuid.get_variant() != uuid::Variant::RFC4122 {
133            return Err("UUID is not RFC 4122 variant".to_string());
134        }
135        Ok(())
136    }
137
138    fn from_validated_uuid(uuid: &Uuid) -> Self {
139        let mut value = [0; UUID4_LEN];
140        let uuid_str = uuid.to_string();
141        value[..uuid_str.len()].copy_from_slice(uuid_str.as_bytes());
142        value[uuid_str.len()] = 0; // Add null terminator
143        Self { value }
144    }
145}
146
147impl FromStr for UUID4 {
148    type Err = String;
149
150    /// Attempts to create a [`UUID4`] from a string representation.
151    ///
152    /// The string should be a valid UUID in the standard format (e.g., "2d89666b-1a1e-4a75-b193-4eb3b454c757").
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if the `value` is not a valid UUID version 4 RFC 4122.
157    fn from_str(value: &str) -> Result<Self, Self::Err> {
158        let uuid = Uuid::try_parse(value).map_err(|e| e.to_string())?;
159        Self::try_validate_v4(&uuid)?;
160        Ok(Self::from_validated_uuid(&uuid))
161    }
162}
163
164impl From<&str> for UUID4 {
165    /// Creates a [`UUID4`] from a string slice.
166    ///
167    /// # Panics
168    ///
169    /// Panics if the `value` string is not a valid UUID version 4 RFC 4122.
170    fn from(value: &str) -> Self {
171        value
172            .parse()
173            .expect("`value` should be a valid UUID version 4 (RFC 4122)")
174    }
175}
176
177impl From<String> for UUID4 {
178    /// Creates a [`UUID4`] from a string.
179    ///
180    /// # Panics
181    ///
182    /// Panics if the `value` string is not a valid UUID version 4 RFC 4122.
183    fn from(value: String) -> Self {
184        Self::from(value.as_str())
185    }
186}
187
188impl From<uuid::Uuid> for UUID4 {
189    /// Creates a [`UUID4`] from a [`uuid::Uuid`].
190    ///
191    /// # Panics
192    ///
193    /// Panics if the `value` is not a valid UUID version 4 RFC 4122.
194    fn from(value: uuid::Uuid) -> Self {
195        Self::validate_v4(&value);
196        Self::from_validated_uuid(&value)
197    }
198}
199
200impl From<UUID4> for uuid::Uuid {
201    /// Creates a [`uuid::Uuid`] from a [`UUID4`].
202    fn from(value: UUID4) -> Self {
203        Self::from_bytes(value.as_bytes())
204    }
205}
206
207impl Default for UUID4 {
208    /// Creates a new default [`UUID4`] instance.
209    ///
210    /// The default UUID4 is simply a newly generated UUID version 4.
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl Debug for UUID4 {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        write!(f, "{}({})", stringify!(UUID4), self)
219    }
220}
221
222impl Display for UUID4 {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        write!(f, "{}", self.to_cstr().to_string_lossy())
225    }
226}
227
228impl Serialize for UUID4 {
229    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230    where
231        S: Serializer,
232    {
233        self.to_string().serialize(serializer)
234    }
235}
236
237impl<'de> Deserialize<'de> for UUID4 {
238    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
239    where
240        D: Deserializer<'de>,
241    {
242        let uuid4_str: &str = Deserialize::deserialize(deserializer)?;
243        uuid4_str.parse().map_err(serde::de::Error::custom)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use std::{
250        collections::hash_map::DefaultHasher,
251        hash::{Hash, Hasher},
252    };
253
254    use rstest::*;
255    use uuid;
256
257    use super::*;
258
259    #[rstest]
260    fn test_new() {
261        let uuid = UUID4::new();
262        let uuid_string = uuid.to_string();
263        let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
264        assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
265        assert_eq!(uuid_parsed.to_string().len(), 36);
266
267        // Version 4 requires bits: 0b0100xxxx
268        assert_eq!(&uuid_string[14..15], "4");
269        // RFC4122 variant requires bits: 0b10xxxxxx
270        let variant_char = &uuid_string[19..20];
271        assert!(matches!(variant_char, "8" | "9" | "a" | "b" | "A" | "B"));
272    }
273
274    #[rstest]
275    fn test_uuid_format() {
276        let uuid = UUID4::new();
277        let bytes = uuid.value;
278
279        // Check null termination
280        assert_eq!(bytes[36], 0);
281
282        // Verify dash positions
283        assert_eq!(bytes[8] as char, '-');
284        assert_eq!(bytes[13] as char, '-');
285        assert_eq!(bytes[18] as char, '-');
286        assert_eq!(bytes[23] as char, '-');
287
288        let s = uuid.to_string();
289        assert_eq!(s.chars().nth(14).unwrap(), '4');
290    }
291
292    #[rstest]
293    #[should_panic(expected = "UUID is not version 4")]
294    fn test_from_str_with_non_version_4_uuid_panics() {
295        let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; // v1 UUID
296        let _ = UUID4::from(uuid_string);
297    }
298
299    #[rstest]
300    fn test_case_insensitive_parsing() {
301        let upper = "2D89666B-1A1E-4A75-B193-4EB3B454C757";
302        let lower = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
303        let uuid_upper = UUID4::from(upper);
304        let uuid_lower = UUID4::from(lower);
305
306        assert_eq!(uuid_upper, uuid_lower);
307        assert_eq!(uuid_upper.to_string(), lower);
308    }
309
310    #[rstest]
311    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8")] // v1 (time-based)
312    #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] // v2 (DCE Security)
313    #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] // v3 (MD5 hash)
314    #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] // v5 (SHA-1 hash)
315    #[should_panic(expected = "UUID is not version 4")]
316    fn test_invalid_version(#[case] uuid_string: &str) {
317        let _ = UUID4::from(uuid_string);
318    }
319
320    #[rstest]
321    #[should_panic(expected = "UUID is not RFC 4122 variant")]
322    fn test_non_rfc4122_variant() {
323        // Valid v4 but wrong variant
324        let uuid = "550e8400-e29b-41d4-0000-446655440000";
325        let _ = UUID4::from(uuid);
326    }
327
328    #[rstest]
329    #[case("")] // Empty string
330    #[case("not-a-uuid-at-all")] // Invalid format
331    #[case("6ba7b810-9dad-11d1-80b4")] // Too short
332    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] // Too long
333    #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] // Wrong separator
334    #[case("6ba7b81019dad111d180b400c04fd430c8")] // No separators
335    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] // Truncated
336    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] // Invalid hex character
337    fn test_invalid_uuid_cases(#[case] invalid_uuid: &str) {
338        assert!(UUID4::from_str(invalid_uuid).is_err());
339    }
340
341    #[rstest]
342    fn test_default() {
343        let uuid: UUID4 = UUID4::default();
344        let uuid_string = uuid.to_string();
345        let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
346        assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
347    }
348
349    #[rstest]
350    fn test_from_str() {
351        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
352        let uuid = UUID4::from(uuid_string);
353        let result_string = uuid.to_string();
354        let result_parsed = Uuid::parse_str(&result_string).unwrap();
355        let expected_parsed = Uuid::parse_str(uuid_string).unwrap();
356        assert_eq!(result_parsed, expected_parsed);
357    }
358
359    #[rstest]
360    fn test_from_uuid() {
361        let original = uuid::Uuid::new_v4();
362        let uuid4 = UUID4::from(original);
363        assert_eq!(uuid4.to_string(), original.to_string());
364    }
365
366    #[rstest]
367    fn test_equality() {
368        let uuid1 = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
369        let uuid2 = UUID4::from("46922ecb-4324-4e40-a56c-841e0d774cef");
370        assert_eq!(uuid1, uuid1);
371        assert_ne!(uuid1, uuid2);
372    }
373
374    #[rstest]
375    fn test_debug() {
376        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
377        let uuid = UUID4::from(uuid_string);
378        assert_eq!(format!("{uuid:?}"), format!("UUID4({uuid_string})"));
379    }
380
381    #[rstest]
382    fn test_display() {
383        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
384        let uuid = UUID4::from(uuid_string);
385        assert_eq!(format!("{uuid}"), uuid_string);
386    }
387
388    #[rstest]
389    fn test_to_cstr() {
390        let uuid = UUID4::new();
391        let cstr = uuid.to_cstr();
392
393        assert_eq!(cstr.to_str().unwrap(), uuid.to_string());
394        assert_eq!(cstr.to_bytes_with_nul()[36], 0);
395    }
396
397    #[rstest]
398    fn test_as_str() {
399        let uuid = UUID4::new();
400        let s = uuid.as_str();
401
402        assert_eq!(s, uuid.to_string());
403        assert_eq!(s.len(), 36);
404    }
405
406    #[rstest]
407    fn test_hash_consistency() {
408        let uuid = UUID4::new();
409
410        let mut hasher1 = DefaultHasher::new();
411        let mut hasher2 = DefaultHasher::new();
412
413        uuid.hash(&mut hasher1);
414        uuid.hash(&mut hasher2);
415
416        assert_eq!(hasher1.finish(), hasher2.finish());
417    }
418
419    #[rstest]
420    fn test_serialize_json() {
421        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
422        let uuid = UUID4::from(uuid_string);
423
424        let serialized = serde_json::to_string(&uuid).unwrap();
425        let expected_json = format!("\"{uuid_string}\"");
426        assert_eq!(serialized, expected_json);
427    }
428
429    #[rstest]
430    fn test_deserialize_json() {
431        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
432        let serialized = format!("\"{uuid_string}\"");
433
434        let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
435        assert_eq!(deserialized.to_string(), uuid_string);
436    }
437
438    #[rstest]
439    fn test_serialize_deserialize_round_trip() {
440        let uuid = UUID4::new();
441
442        let serialized = serde_json::to_string(&uuid).unwrap();
443        let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
444
445        assert_eq!(uuid, deserialized);
446    }
447
448    #[rstest]
449    fn test_as_bytes() {
450        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
451        let uuid = UUID4::from(uuid_string);
452
453        let bytes = uuid.as_bytes();
454        assert_eq!(bytes.len(), 16);
455
456        // Reconstruct UUID from bytes and verify it matches
457        let reconstructed = Uuid::from_bytes(bytes);
458        assert_eq!(reconstructed.to_string(), uuid_string);
459
460        // Verify version 4
461        assert_eq!(reconstructed.get_version().unwrap(), uuid::Version::Random);
462    }
463
464    #[rstest]
465    fn test_as_bytes_round_trip() {
466        let uuid1 = UUID4::new();
467        let bytes = uuid1.as_bytes();
468        let uuid2 = UUID4::from(Uuid::from_bytes(bytes));
469
470        assert_eq!(uuid1, uuid2);
471    }
472
473    #[rstest]
474    #[case("\"not-a-uuid\"")] // Invalid format
475    #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] // v1 UUID (wrong version)
476    #[case("\"\"")] // Empty string
477    fn test_deserialize_invalid_uuid_returns_error(#[case] json: &str) {
478        let result: Result<UUID4, _> = serde_json::from_str(json);
479        assert!(result.is_err());
480    }
481}