1use std::{
19 ffi::CStr,
20 fmt::{Debug, Display},
21 hash::Hash,
22 io::{Cursor, Write},
23 str::FromStr,
24};
25
26#[cfg(all(feature = "simulation", madsim))]
27use madsim::rand::RngCore as MadsimRngCore;
28use rand::Rng;
29use serde::{Deserialize, Deserializer, Serialize, Serializer};
30use uuid::Uuid;
31
32pub(crate) const UUID4_LEN: usize = 37;
34
35#[repr(C)]
38#[derive(Copy, Clone, Hash, PartialEq, Eq)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core", from_py_object)
42)]
43#[cfg_attr(
44 feature = "python",
45 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.core")
46)]
47pub struct UUID4 {
48 pub(crate) value: [u8; 37], }
51
52impl UUID4 {
53 #[must_use]
57 pub fn new() -> Self {
58 let mut bytes = [0u8; 16];
59 #[cfg(all(feature = "simulation", madsim))]
60 {
61 if madsim::runtime::Handle::try_current().is_ok() {
66 MadsimRngCore::fill_bytes(&mut madsim::rand::thread_rng(), &mut bytes);
67 } else {
68 rand::rng().fill_bytes(&mut bytes); }
70 }
71 #[cfg(not(all(feature = "simulation", madsim)))]
72 rand::rng().fill_bytes(&mut bytes);
73
74 bytes[6] = (bytes[6] & 0x0F) | 0x40; bytes[8] = (bytes[8] & 0x3F) | 0x80; let mut value = [0u8; UUID4_LEN];
78 let mut cursor = Cursor::new(&mut value[..36]);
79
80 write!(
81 cursor,
82 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
83 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
84 u16::from_be_bytes([bytes[4], bytes[5]]),
85 u16::from_be_bytes([bytes[6], bytes[7]]),
86 u16::from_be_bytes([bytes[8], bytes[9]]),
87 u64::from_be_bytes([
88 bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], 0, 0
89 ]) >> 16
90 )
91 .expect("Error writing UUID string to buffer");
92
93 value[36] = 0; debug_assert!(
96 value[14] == b'4',
97 "Invariant: UUID version digit must be '4' (was {})",
98 value[14] as char
99 );
100 debug_assert!(
101 matches!(value[19], b'8' | b'9' | b'a' | b'b'),
102 "Invariant: UUID variant byte must be RFC 4122 (was {})",
103 value[19] as char
104 );
105 debug_assert!(
106 value[36] == 0,
107 "Invariant: UUID null terminator must be at index 36"
108 );
109
110 Self { value }
111 }
112
113 #[must_use]
118 pub fn from_bytes(mut bytes: [u8; 16]) -> Self {
119 bytes[6] = (bytes[6] & 0x0F) | 0x40;
120 bytes[8] = (bytes[8] & 0x3F) | 0x80;
121 Self::from_validated_uuid(&Uuid::from_bytes(bytes))
122 }
123
124 #[must_use]
130 pub fn to_cstr(&self) -> &CStr {
131 CStr::from_bytes_with_nul(&self.value)
133 .expect("UUID byte representation should be a valid C string")
134 }
135
136 #[must_use]
143 pub fn as_str(&self) -> &str {
144 self.to_cstr().to_str().expect("UUID should be valid UTF-8")
146 }
147
148 #[must_use]
158 pub fn as_bytes(&self) -> [u8; 16] {
159 let uuid_str = self.to_cstr().to_str().expect("Valid UTF-8");
162 let uuid = Uuid::parse_str(uuid_str).expect("Valid UUID4");
163 *uuid.as_bytes()
164 }
165
166 fn validate_v4(uuid: &Uuid) {
167 assert_eq!(
169 uuid.get_version(),
170 Some(uuid::Version::Random),
171 "UUID is not version 4"
172 );
173
174 assert_eq!(
176 uuid.get_variant(),
177 uuid::Variant::RFC4122,
178 "UUID is not RFC 4122 variant"
179 );
180 }
181
182 fn try_validate_v4(uuid: &Uuid) -> Result<(), String> {
183 if uuid.get_version() != Some(uuid::Version::Random) {
184 return Err("UUID is not version 4".to_string());
185 }
186
187 if uuid.get_variant() != uuid::Variant::RFC4122 {
188 return Err("UUID is not RFC 4122 variant".to_string());
189 }
190 Ok(())
191 }
192
193 fn from_validated_uuid(uuid: &Uuid) -> Self {
194 let mut value = [0; UUID4_LEN];
195 let uuid_str = uuid.to_string();
196 value[..uuid_str.len()].copy_from_slice(uuid_str.as_bytes());
197 value[uuid_str.len()] = 0; Self { value }
199 }
200}
201
202impl FromStr for UUID4 {
203 type Err = String;
204
205 fn from_str(value: &str) -> Result<Self, Self::Err> {
213 let uuid = Uuid::try_parse(value).map_err(|e| e.to_string())?;
214 Self::try_validate_v4(&uuid)?;
215 Ok(Self::from_validated_uuid(&uuid))
216 }
217}
218
219impl From<&str> for UUID4 {
220 fn from(value: &str) -> Self {
221 Self::from_str(value).expect("Invalid UUID4 string")
222 }
223}
224
225impl From<String> for UUID4 {
226 fn from(value: String) -> Self {
227 Self::from_str(&value).expect("Invalid UUID4 string")
228 }
229}
230
231impl From<uuid::Uuid> for UUID4 {
232 fn from(value: uuid::Uuid) -> Self {
238 Self::validate_v4(&value);
239 Self::from_validated_uuid(&value)
240 }
241}
242
243impl From<UUID4> for uuid::Uuid {
244 fn from(value: UUID4) -> Self {
246 Self::from_bytes(value.as_bytes())
247 }
248}
249
250impl Default for UUID4 {
251 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl Debug for UUID4 {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 write!(f, "{}({})", stringify!(UUID4), self)
262 }
263}
264
265impl Display for UUID4 {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(f, "{}", self.to_cstr().to_string_lossy())
268 }
269}
270
271impl Serialize for UUID4 {
272 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
273 where
274 S: Serializer,
275 {
276 self.to_string().serialize(serializer)
277 }
278}
279
280impl<'de> Deserialize<'de> for UUID4 {
281 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
282 where
283 D: Deserializer<'de>,
284 {
285 let uuid4_str: &str = Deserialize::deserialize(deserializer)?;
286 uuid4_str.parse().map_err(serde::de::Error::custom)
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use std::{
293 collections::hash_map::DefaultHasher,
294 hash::{Hash, Hasher},
295 };
296
297 use proptest::prelude::*;
298 use rstest::*;
299 use uuid;
300
301 use super::*;
302
303 #[rstest]
304 fn test_new() {
305 let uuid = UUID4::new();
306 let uuid_string = uuid.to_string();
307 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
308 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
309 assert_eq!(uuid_parsed.to_string().len(), 36);
310
311 assert_eq!(&uuid_string[14..15], "4");
313 let variant_char = &uuid_string[19..20];
315 assert!(matches!(variant_char, "8" | "9" | "a" | "b" | "A" | "B"));
316 }
317
318 #[rstest]
319 fn test_uuid_format() {
320 let uuid = UUID4::new();
321 let bytes = uuid.value;
322
323 assert_eq!(bytes[36], 0);
325
326 assert_eq!(bytes[8] as char, '-');
328 assert_eq!(bytes[13] as char, '-');
329 assert_eq!(bytes[18] as char, '-');
330 assert_eq!(bytes[23] as char, '-');
331
332 let s = uuid.to_string();
333 assert_eq!(s.chars().nth(14).unwrap(), '4');
334 }
335
336 #[rstest]
337 #[should_panic(expected = "UUID is not version 4")]
338 fn test_from_str_with_non_version_4_uuid_panics() {
339 let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let _ = UUID4::from(uuid_string);
341 }
342
343 #[rstest]
344 fn test_case_insensitive_parsing() {
345 let upper = "2D89666B-1A1E-4A75-B193-4EB3B454C757";
346 let lower = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
347 let uuid_upper = UUID4::from(upper);
348 let uuid_lower = UUID4::from(lower);
349
350 assert_eq!(uuid_upper, uuid_lower);
351 assert_eq!(uuid_upper.to_string(), lower);
352 }
353
354 #[rstest]
355 #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8")] #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] #[should_panic(expected = "UUID is not version 4")]
360 fn test_invalid_version(#[case] uuid_string: &str) {
361 let _ = UUID4::from(uuid_string);
362 }
363
364 #[rstest]
365 #[should_panic(expected = "UUID is not RFC 4122 variant")]
366 fn test_non_rfc4122_variant() {
367 let uuid = "550e8400-e29b-41d4-0000-446655440000";
369 let _ = UUID4::from(uuid);
370 }
371
372 #[rstest]
373 #[case("")] #[case("not-a-uuid-at-all")] #[case("6ba7b810-9dad-11d1-80b4")] #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] #[case("6ba7b81019dad111d180b400c04fd430c8")] #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] fn test_invalid_uuid_cases(#[case] invalid_uuid: &str) {
382 assert!(UUID4::from_str(invalid_uuid).is_err());
383 }
384
385 #[rstest]
386 fn test_default() {
387 let uuid: UUID4 = UUID4::default();
388 let uuid_string = uuid.to_string();
389 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
390 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
391 }
392
393 #[rstest]
394 fn test_from_str() {
395 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
396 let uuid = UUID4::from(uuid_string);
397 let result_string = uuid.to_string();
398 let result_parsed = Uuid::parse_str(&result_string).unwrap();
399 let expected_parsed = Uuid::parse_str(uuid_string).unwrap();
400 assert_eq!(result_parsed, expected_parsed);
401 }
402
403 #[rstest]
404 fn test_from_uuid() {
405 let original = uuid::Uuid::new_v4();
406 let uuid4 = UUID4::from(original);
407 assert_eq!(uuid4.to_string(), original.to_string());
408 }
409
410 #[rstest]
411 fn test_equality() {
412 let uuid1 = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
413 let uuid2 = UUID4::from("46922ecb-4324-4e40-a56c-841e0d774cef");
414 assert_eq!(uuid1, uuid1);
415 assert_ne!(uuid1, uuid2);
416 }
417
418 #[rstest]
419 fn test_debug() {
420 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
421 let uuid = UUID4::from(uuid_string);
422 assert_eq!(format!("{uuid:?}"), format!("UUID4({uuid_string})"));
423 }
424
425 #[rstest]
426 fn test_display() {
427 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
428 let uuid = UUID4::from(uuid_string);
429 assert_eq!(format!("{uuid}"), uuid_string);
430 }
431
432 #[rstest]
433 fn test_to_cstr() {
434 let uuid = UUID4::new();
435 let cstr = uuid.to_cstr();
436
437 assert_eq!(cstr.to_str().unwrap(), uuid.to_string());
438 assert_eq!(cstr.to_bytes_with_nul()[36], 0);
439 }
440
441 #[rstest]
442 fn test_as_str() {
443 let uuid = UUID4::new();
444 let s = uuid.as_str();
445
446 assert_eq!(s, uuid.to_string());
447 assert_eq!(s.len(), 36);
448 }
449
450 #[rstest]
451 fn test_hash_consistency() {
452 let uuid = UUID4::new();
453
454 let mut hasher1 = DefaultHasher::new();
455 let mut hasher2 = DefaultHasher::new();
456
457 uuid.hash(&mut hasher1);
458 uuid.hash(&mut hasher2);
459
460 assert_eq!(hasher1.finish(), hasher2.finish());
461 }
462
463 #[rstest]
464 fn test_serialize_json() {
465 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
466 let uuid = UUID4::from(uuid_string);
467
468 let serialized = serde_json::to_string(&uuid).unwrap();
469 let expected_json = format!("\"{uuid_string}\"");
470 assert_eq!(serialized, expected_json);
471 }
472
473 #[rstest]
474 fn test_deserialize_json() {
475 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
476 let serialized = format!("\"{uuid_string}\"");
477
478 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
479 assert_eq!(deserialized.to_string(), uuid_string);
480 }
481
482 #[rstest]
483 fn test_serialize_deserialize_round_trip() {
484 let uuid = UUID4::new();
485
486 let serialized = serde_json::to_string(&uuid).unwrap();
487 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
488
489 assert_eq!(uuid, deserialized);
490 }
491
492 #[rstest]
493 fn test_as_bytes() {
494 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
495 let uuid = UUID4::from(uuid_string);
496
497 let bytes = uuid.as_bytes();
498 assert_eq!(bytes.len(), 16);
499
500 let reconstructed = Uuid::from_bytes(bytes);
502 assert_eq!(reconstructed.to_string(), uuid_string);
503
504 assert_eq!(reconstructed.get_version().unwrap(), uuid::Version::Random);
506 }
507
508 #[rstest]
509 fn test_as_bytes_round_trip() {
510 let uuid1 = UUID4::new();
511 let bytes = uuid1.as_bytes();
512 let uuid2 = UUID4::from(Uuid::from_bytes(bytes));
513
514 assert_eq!(uuid1, uuid2);
515 }
516
517 #[rstest]
518 fn test_from_bytes_basic() {
519 let bytes = [
521 0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
522 0xc7, 0x57,
523 ];
524 let uuid = UUID4::from_bytes(bytes);
525 assert_eq!(uuid.to_string(), "2d89666b-1a1e-4a75-b193-4eb3b454c757");
526 assert_eq!(uuid.as_bytes(), bytes);
527 }
528
529 #[rstest]
530 fn test_from_bytes_normalizes_version() {
531 let mut bytes = [0u8; 16];
533 bytes[6] = 0x1a; bytes[8] = 0x80; let uuid = UUID4::from_bytes(bytes);
536 assert_eq!(&uuid.to_string()[14..15], "4");
537 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
538 assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
539 }
540
541 #[rstest]
542 fn test_from_bytes_normalizes_variant() {
543 let mut bytes = [0u8; 16];
545 bytes[6] = 0x40; bytes[8] = 0x00; let uuid = UUID4::from_bytes(bytes);
548 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
549 assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
550 }
551
552 #[rstest]
553 fn test_from_bytes_all_zero_is_valid_v4() {
554 let uuid = UUID4::from_bytes([0u8; 16]);
555 assert_eq!(uuid.to_string(), "00000000-0000-4000-8000-000000000000");
558 }
559
560 #[rstest]
561 fn test_from_bytes_all_ones_is_valid_v4() {
562 let uuid = UUID4::from_bytes([0xFFu8; 16]);
563 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
564 assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
565 assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
566 }
567
568 #[rstest]
569 fn test_from_bytes_round_trip() {
570 let original = UUID4::new();
573 let bytes = original.as_bytes();
574 let reconstructed = UUID4::from_bytes(bytes);
575 assert_eq!(original, reconstructed);
576 }
577
578 #[rstest]
579 #[case("\"not-a-uuid\"")] #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] #[case("\"\"")] fn test_deserialize_invalid_uuid_returns_error(#[case] json: &str) {
583 let result: Result<UUID4, _> = serde_json::from_str(json);
584 assert!(result.is_err());
585 }
586
587 fn uuid4_strategy() -> impl Strategy<Value = UUID4> {
588 any::<[u8; 16]>().prop_map(UUID4::from_bytes)
591 }
592
593 proptest! {
594 #[rstest]
595 fn prop_uuid4_string_roundtrip(uuid in uuid4_strategy()) {
596 let s = uuid.to_string();
597 let parsed = UUID4::from_str(&s);
598 prop_assert!(parsed.is_ok(), "Failed to parse UUID string: {}", s);
599 prop_assert_eq!(parsed.unwrap(), uuid, "String round-trip failed");
600 }
601
602 #[rstest]
603 fn prop_uuid4_serde_roundtrip(uuid in uuid4_strategy()) {
604 let serialized = serde_json::to_string(&uuid).unwrap();
605 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
606 prop_assert_eq!(deserialized, uuid, "Serde JSON round-trip failed");
607 }
608
609 #[rstest]
610 fn prop_uuid4_rfc4122_compliance(uuid in uuid4_strategy()) {
611 let s = uuid.to_string();
612 let bytes = uuid.value;
613
614 prop_assert_eq!(s.len(), 36);
616 prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
617
618 prop_assert_eq!(bytes[8] as char, '-');
620 prop_assert_eq!(bytes[13] as char, '-');
621 prop_assert_eq!(bytes[18] as char, '-');
622 prop_assert_eq!(bytes[23] as char, '-');
623
624 prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
626
627 let variant_char = s.chars().nth(19).unwrap().to_ascii_lowercase();
630 prop_assert!(
631 matches!(variant_char, '8' | '9' | 'a' | 'b'),
632 "Invalid variant character: {}", variant_char
633 );
634 }
635
636 #[rstest]
637 fn prop_uuid4_as_bytes_consistency(uuid in uuid4_strategy()) {
638 let bytes = uuid.as_bytes();
639 let reconstructed = uuid::Uuid::from_bytes(bytes);
640 prop_assert_eq!(reconstructed.to_string(), uuid.to_string(), "Byte reconstruction mismatch");
641 }
642
643 #[rstest]
644 fn prop_uuid4_equality_and_hashing(uuid1 in uuid4_strategy(), uuid2 in uuid4_strategy()) {
645 prop_assert_eq!(uuid1, uuid1);
647
648 if uuid1 == uuid2 {
650 let mut h1 = DefaultHasher::new();
651 let mut h2 = DefaultHasher::new();
652 uuid1.hash(&mut h1);
653 uuid2.hash(&mut h2);
654 prop_assert_eq!(h1.finish(), h2.finish());
655 }
656 }
657
658 #[rstest]
659 fn prop_uuid4_from_str_never_panics(s: String) {
660 let _ = UUID4::from_str(&s);
662 }
663
664 #[rstest]
665 fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
666 let uuid = UUID4::from_bytes(bytes);
669 let parsed = uuid::Uuid::parse_str(uuid.as_str()).unwrap();
670 prop_assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
671 prop_assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
672 }
673
674 #[rstest]
675 fn prop_from_bytes_as_bytes_roundtrip(bytes in any::<[u8; 16]>()) {
676 let mut expected = bytes;
679 expected[6] = (expected[6] & 0x0F) | 0x40;
680 expected[8] = (expected[8] & 0x3F) | 0x80;
681 let uuid = UUID4::from_bytes(bytes);
682 prop_assert_eq!(uuid.as_bytes(), expected);
683 }
684 }
685}