1use std::{
19 ffi::CStr,
20 fmt::{Debug, Display},
21 hash::Hash,
22 str::FromStr,
23};
24
25#[cfg(all(feature = "simulation", madsim))]
26use madsim::rand::RngCore as MadsimRngCore;
27use rand::Rng;
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29use uuid::Uuid;
30
31use crate::hex::ENCODE_PAIR;
32
33pub(crate) const UUID4_LEN: usize = 37;
35
36#[repr(C)]
39#[derive(Copy, Clone, Hash, PartialEq, Eq)]
40#[cfg_attr(
41 feature = "python",
42 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core", from_py_object)
43)]
44#[cfg_attr(
45 feature = "python",
46 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.core")
47)]
48pub struct UUID4 {
49 pub(crate) value: [u8; 37], }
52
53impl UUID4 {
54 #[must_use]
58 pub fn new() -> Self {
59 let bytes = Self::new_bytes();
60 Self {
61 value: format_uuid4_bytes(bytes),
62 }
63 }
64
65 #[must_use]
67 pub fn new_bytes() -> [u8; 16] {
68 let mut bytes = [0u8; 16];
69 #[cfg(all(feature = "simulation", madsim))]
70 {
71 if madsim::runtime::Handle::try_current().is_ok() {
76 MadsimRngCore::fill_bytes(&mut madsim::rand::thread_rng(), &mut bytes);
77 } else {
78 rand::rng().fill_bytes(&mut bytes); }
80 }
81 #[cfg(not(all(feature = "simulation", madsim)))]
82 rand::rng().fill_bytes(&mut bytes);
83
84 bytes[6] = (bytes[6] & 0x0F) | 0x40; bytes[8] = (bytes[8] & 0x3F) | 0x80; bytes
88 }
89
90 #[must_use]
95 pub fn from_bytes(mut bytes: [u8; 16]) -> Self {
96 bytes[6] = (bytes[6] & 0x0F) | 0x40;
97 bytes[8] = (bytes[8] & 0x3F) | 0x80;
98 Self::from_validated_uuid(&Uuid::from_bytes(bytes))
99 }
100
101 #[must_use]
107 pub fn to_cstr(&self) -> &CStr {
108 CStr::from_bytes_with_nul(&self.value)
110 .expect("UUID byte representation should be a valid C string")
111 }
112
113 #[must_use]
120 pub fn as_str(&self) -> &str {
121 self.to_cstr().to_str().expect("UUID should be valid UTF-8")
123 }
124
125 #[must_use]
135 pub fn as_bytes(&self) -> [u8; 16] {
136 let uuid_str = self.to_cstr().to_str().expect("Valid UTF-8");
137 let uuid = Uuid::parse_str(uuid_str).expect("Valid UUID4");
138 *uuid.as_bytes()
139 }
140
141 fn validate_v4(uuid: &Uuid) {
142 assert_eq!(
144 uuid.get_version(),
145 Some(uuid::Version::Random),
146 "UUID is not version 4"
147 );
148
149 assert_eq!(
151 uuid.get_variant(),
152 uuid::Variant::RFC4122,
153 "UUID is not RFC 4122 variant"
154 );
155 }
156
157 fn try_validate_v4(uuid: &Uuid) -> Result<(), String> {
158 if uuid.get_version() != Some(uuid::Version::Random) {
159 return Err("UUID is not version 4".to_string());
160 }
161
162 if uuid.get_variant() != uuid::Variant::RFC4122 {
163 return Err("UUID is not RFC 4122 variant".to_string());
164 }
165 Ok(())
166 }
167
168 fn from_validated_uuid(uuid: &Uuid) -> Self {
169 let mut value = [0; UUID4_LEN];
170 let uuid_str = uuid.to_string();
171 value[..uuid_str.len()].copy_from_slice(uuid_str.as_bytes());
172 value[uuid_str.len()] = 0; Self { value }
174 }
175}
176
177impl FromStr for UUID4 {
178 type Err = String;
179
180 fn from_str(value: &str) -> Result<Self, Self::Err> {
188 let uuid = Uuid::try_parse(value).map_err(|e| e.to_string())?;
189 Self::try_validate_v4(&uuid)?;
190 Ok(Self::from_validated_uuid(&uuid))
191 }
192}
193
194impl From<&str> for UUID4 {
195 fn from(value: &str) -> Self {
196 Self::from_str(value).expect("Invalid UUID4 string")
197 }
198}
199
200impl From<String> for UUID4 {
201 fn from(value: String) -> Self {
202 Self::from_str(&value).expect("Invalid UUID4 string")
203 }
204}
205
206impl From<uuid::Uuid> for UUID4 {
207 fn from(value: uuid::Uuid) -> Self {
213 Self::validate_v4(&value);
214 Self::from_validated_uuid(&value)
215 }
216}
217
218impl From<UUID4> for uuid::Uuid {
219 fn from(value: UUID4) -> Self {
221 Self::from_bytes(value.as_bytes())
222 }
223}
224
225impl Default for UUID4 {
226 fn default() -> Self {
230 Self::new()
231 }
232}
233
234impl Debug for UUID4 {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 write!(f, "{}({})", stringify!(UUID4), self)
237 }
238}
239
240impl Display for UUID4 {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 write!(f, "{}", self.to_cstr().to_string_lossy())
243 }
244}
245
246impl Serialize for UUID4 {
247 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248 where
249 S: Serializer,
250 {
251 self.to_string().serialize(serializer)
252 }
253}
254
255impl<'de> Deserialize<'de> for UUID4 {
256 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257 where
258 D: Deserializer<'de>,
259 {
260 let uuid4_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
261 uuid4_str.as_ref().parse().map_err(serde::de::Error::custom)
262 }
263}
264
265fn format_uuid4_bytes(bytes: [u8; 16]) -> [u8; UUID4_LEN] {
266 let mut value = [0u8; UUID4_LEN];
267 let mut pos = 0;
268
269 for (idx, byte) in bytes.into_iter().enumerate() {
270 if matches!(idx, 4 | 6 | 8 | 10) {
271 value[pos] = b'-';
272 pos += 1;
273 }
274
275 value[pos..pos + 2].copy_from_slice(&ENCODE_PAIR[byte as usize]);
276 pos += 2;
277 }
278
279 value[36] = 0; debug_assert_eq!(pos, 36, "Invariant: UUID text must be 36 bytes");
282 debug_assert!(
283 value[14] == b'4',
284 "Invariant: UUID version digit must be '4' (was {})",
285 value[14] as char
286 );
287 debug_assert!(
288 matches!(value[19], b'8' | b'9' | b'a' | b'b'),
289 "Invariant: UUID variant byte must be RFC 4122 (was {})",
290 value[19] as char
291 );
292 debug_assert!(
293 value[36] == 0,
294 "Invariant: UUID null terminator must be at index 36"
295 );
296
297 value
298}
299
300#[cfg(test)]
301mod tests {
302 use std::{
303 collections::hash_map::DefaultHasher,
304 ffi::CStr,
305 hash::{Hash, Hasher},
306 };
307
308 use proptest::prelude::*;
309 use rstest::*;
310 use uuid;
311
312 use super::*;
313
314 #[rstest]
315 fn test_new() {
316 let uuid = UUID4::new();
317 let uuid_string = uuid.to_string();
318 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
319 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
320 assert_eq!(uuid_parsed.to_string().len(), 36);
321
322 assert_eq!(&uuid_string[14..15], "4");
324 let variant_char = &uuid_string[19..20];
326 assert!(matches!(variant_char, "8" | "9" | "a" | "b" | "A" | "B"));
327 }
328
329 #[rstest]
330 fn test_new_bytes() {
331 let bytes = UUID4::new_bytes();
332 let uuid = UUID4::from_bytes(bytes);
333
334 assert_eq!(bytes[6] >> 4, 4);
335 assert!(matches!(bytes[8] >> 6, 0b10));
336 assert_eq!(uuid.as_bytes(), bytes);
337 }
338
339 #[rstest]
340 fn test_uuid_format() {
341 let uuid = UUID4::new();
342 let bytes = uuid.value;
343
344 assert_eq!(bytes[36], 0);
346
347 assert_eq!(bytes[8] as char, '-');
349 assert_eq!(bytes[13] as char, '-');
350 assert_eq!(bytes[18] as char, '-');
351 assert_eq!(bytes[23] as char, '-');
352
353 let s = uuid.to_string();
354 assert_eq!(s.chars().nth(14).unwrap(), '4');
355 }
356
357 #[rstest]
358 fn test_format_uuid4_bytes_golden() {
359 let bytes = [
360 0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
361 0xc7, 0x57,
362 ];
363
364 let formatted = format_uuid4_bytes(bytes);
365 let text = CStr::from_bytes_with_nul(&formatted)
366 .unwrap()
367 .to_str()
368 .unwrap();
369
370 assert_eq!(text, "2d89666b-1a1e-4a75-b193-4eb3b454c757");
371 assert_eq!(formatted[36], 0);
372 }
373
374 #[rstest]
375 #[should_panic(expected = "UUID is not version 4")]
376 fn test_from_str_with_non_version_4_uuid_panics() {
377 let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let _ = UUID4::from(uuid_string);
379 }
380
381 #[rstest]
382 fn test_case_insensitive_parsing() {
383 let upper = "2D89666B-1A1E-4A75-B193-4EB3B454C757";
384 let lower = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
385 let uuid_upper = UUID4::from(upper);
386 let uuid_lower = UUID4::from(lower);
387
388 assert_eq!(uuid_upper, uuid_lower);
389 assert_eq!(uuid_upper.to_string(), lower);
390 }
391
392 #[rstest]
393 #[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")]
398 fn test_invalid_version(#[case] uuid_string: &str) {
399 let _ = UUID4::from(uuid_string);
400 }
401
402 #[rstest]
403 #[should_panic(expected = "UUID is not RFC 4122 variant")]
404 fn test_non_rfc4122_variant() {
405 let uuid = "550e8400-e29b-41d4-0000-446655440000";
407 let _ = UUID4::from(uuid);
408 }
409
410 #[rstest]
411 #[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) {
420 assert!(UUID4::from_str(invalid_uuid).is_err());
421 }
422
423 #[rstest]
424 fn test_default() {
425 let uuid: UUID4 = UUID4::default();
426 let uuid_string = uuid.to_string();
427 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
428 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
429 }
430
431 #[rstest]
432 fn test_from_str() {
433 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
434 let uuid = UUID4::from(uuid_string);
435 let result_string = uuid.to_string();
436 let result_parsed = Uuid::parse_str(&result_string).unwrap();
437 let expected_parsed = Uuid::parse_str(uuid_string).unwrap();
438 assert_eq!(result_parsed, expected_parsed);
439 }
440
441 #[rstest]
442 fn test_from_uuid() {
443 let original = uuid::Uuid::new_v4();
444 let uuid4 = UUID4::from(original);
445 assert_eq!(uuid4.to_string(), original.to_string());
446 }
447
448 #[rstest]
449 fn test_equality() {
450 let uuid1 = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
451 let uuid2 = UUID4::from("46922ecb-4324-4e40-a56c-841e0d774cef");
452 assert_eq!(uuid1, uuid1);
453 assert_ne!(uuid1, uuid2);
454 }
455
456 #[rstest]
457 fn test_debug() {
458 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
459 let uuid = UUID4::from(uuid_string);
460 assert_eq!(format!("{uuid:?}"), format!("UUID4({uuid_string})"));
461 }
462
463 #[rstest]
464 fn test_display() {
465 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
466 let uuid = UUID4::from(uuid_string);
467 assert_eq!(format!("{uuid}"), uuid_string);
468 }
469
470 #[rstest]
471 fn test_to_cstr() {
472 let uuid = UUID4::new();
473 let cstr = uuid.to_cstr();
474
475 assert_eq!(cstr.to_str().unwrap(), uuid.to_string());
476 assert_eq!(cstr.to_bytes_with_nul()[36], 0);
477 }
478
479 #[rstest]
480 fn test_as_str() {
481 let uuid = UUID4::new();
482 let s = uuid.as_str();
483
484 assert_eq!(s, uuid.to_string());
485 assert_eq!(s.len(), 36);
486 }
487
488 #[rstest]
489 fn test_hash_consistency() {
490 let uuid = UUID4::new();
491
492 let mut hasher1 = DefaultHasher::new();
493 let mut hasher2 = DefaultHasher::new();
494
495 uuid.hash(&mut hasher1);
496 uuid.hash(&mut hasher2);
497
498 assert_eq!(hasher1.finish(), hasher2.finish());
499 }
500
501 #[rstest]
502 fn test_serialize_json() {
503 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
504 let uuid = UUID4::from(uuid_string);
505
506 let serialized = serde_json::to_string(&uuid).unwrap();
507 let expected_json = format!("\"{uuid_string}\"");
508 assert_eq!(serialized, expected_json);
509 }
510
511 #[rstest]
512 fn test_deserialize_json() {
513 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
514 let serialized = format!("\"{uuid_string}\"");
515
516 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
517 assert_eq!(deserialized.to_string(), uuid_string);
518 }
519
520 #[rstest]
521 fn test_deserialize_from_owned_value() {
522 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
523 let value = serde_json::Value::String(uuid_string.to_string());
524
525 let deserialized: UUID4 = serde_json::from_value(value).unwrap();
526 assert_eq!(deserialized.to_string(), uuid_string);
527 }
528
529 #[rstest]
530 fn test_serialize_deserialize_round_trip() {
531 let uuid = UUID4::new();
532
533 let serialized = serde_json::to_string(&uuid).unwrap();
534 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
535
536 assert_eq!(uuid, deserialized);
537 }
538
539 #[rstest]
540 fn test_as_bytes() {
541 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
542 let uuid = UUID4::from(uuid_string);
543
544 let bytes = uuid.as_bytes();
545 assert_eq!(bytes.len(), 16);
546
547 let reconstructed = Uuid::from_bytes(bytes);
549 assert_eq!(reconstructed.to_string(), uuid_string);
550
551 assert_eq!(reconstructed.get_version().unwrap(), uuid::Version::Random);
553 }
554
555 #[rstest]
556 fn test_as_bytes_round_trip() {
557 let uuid1 = UUID4::new();
558 let bytes = uuid1.as_bytes();
559 let uuid2 = UUID4::from(Uuid::from_bytes(bytes));
560
561 assert_eq!(uuid1, uuid2);
562 }
563
564 #[rstest]
565 fn test_from_bytes_basic() {
566 let bytes = [
568 0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
569 0xc7, 0x57,
570 ];
571 let uuid = UUID4::from_bytes(bytes);
572 assert_eq!(uuid.to_string(), "2d89666b-1a1e-4a75-b193-4eb3b454c757");
573 assert_eq!(uuid.as_bytes(), bytes);
574 }
575
576 #[rstest]
577 fn test_from_bytes_normalizes_version() {
578 let mut bytes = [0u8; 16];
580 bytes[6] = 0x1a; bytes[8] = 0x80; let uuid = UUID4::from_bytes(bytes);
583 assert_eq!(&uuid.to_string()[14..15], "4");
584 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
585 assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
586 }
587
588 #[rstest]
589 fn test_from_bytes_normalizes_variant() {
590 let mut bytes = [0u8; 16];
592 bytes[6] = 0x40; bytes[8] = 0x00; let uuid = UUID4::from_bytes(bytes);
595 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
596 assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
597 }
598
599 #[rstest]
600 fn test_from_bytes_all_zero_is_valid_v4() {
601 let uuid = UUID4::from_bytes([0u8; 16]);
602 assert_eq!(uuid.to_string(), "00000000-0000-4000-8000-000000000000");
605 }
606
607 #[rstest]
608 fn test_from_bytes_all_ones_is_valid_v4() {
609 let uuid = UUID4::from_bytes([0xFFu8; 16]);
610 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
611 assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
612 assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
613 }
614
615 #[rstest]
616 fn test_from_bytes_round_trip() {
617 let original = UUID4::new();
620 let bytes = original.as_bytes();
621 let reconstructed = UUID4::from_bytes(bytes);
622 assert_eq!(original, reconstructed);
623 }
624
625 #[rstest]
626 #[case("\"not-a-uuid\"")] #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] #[case("\"\"")] fn test_deserialize_invalid_uuid_returns_error(#[case] json: &str) {
630 let result: Result<UUID4, _> = serde_json::from_str(json);
631 assert!(result.is_err());
632 }
633
634 fn uuid4_strategy() -> impl Strategy<Value = UUID4> {
635 any::<[u8; 16]>().prop_map(UUID4::from_bytes)
638 }
639
640 proptest! {
641 #[rstest]
642 fn prop_uuid4_string_roundtrip(uuid in uuid4_strategy()) {
643 let s = uuid.to_string();
644 let parsed = UUID4::from_str(&s);
645 prop_assert!(parsed.is_ok(), "Failed to parse UUID string: {}", s);
646 prop_assert_eq!(parsed.unwrap(), uuid, "String round-trip failed");
647 }
648
649 #[rstest]
650 fn prop_uuid4_serde_roundtrip(uuid in uuid4_strategy()) {
651 let serialized = serde_json::to_string(&uuid).unwrap();
652 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
653 prop_assert_eq!(deserialized, uuid, "Serde JSON round-trip failed");
654 }
655
656 #[rstest]
657 fn prop_uuid4_rfc4122_compliance(uuid in uuid4_strategy()) {
658 let s = uuid.to_string();
659 let bytes = uuid.value;
660
661 prop_assert_eq!(s.len(), 36);
663 prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
664
665 prop_assert_eq!(bytes[8] as char, '-');
667 prop_assert_eq!(bytes[13] as char, '-');
668 prop_assert_eq!(bytes[18] as char, '-');
669 prop_assert_eq!(bytes[23] as char, '-');
670
671 prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
673
674 let variant_char = s.chars().nth(19).unwrap().to_ascii_lowercase();
677 prop_assert!(
678 matches!(variant_char, '8' | '9' | 'a' | 'b'),
679 "Invalid variant character: {}", variant_char
680 );
681 }
682
683 #[rstest]
684 fn prop_uuid4_as_bytes_consistency(uuid in uuid4_strategy()) {
685 let bytes = uuid.as_bytes();
686 let reconstructed = uuid::Uuid::from_bytes(bytes);
687 prop_assert_eq!(reconstructed.to_string(), uuid.to_string(), "Byte reconstruction mismatch");
688 }
689
690 #[rstest]
691 fn prop_uuid4_equality_and_hashing(uuid1 in uuid4_strategy(), uuid2 in uuid4_strategy()) {
692 prop_assert_eq!(uuid1, uuid1);
694
695 if uuid1 == uuid2 {
697 let mut h1 = DefaultHasher::new();
698 let mut h2 = DefaultHasher::new();
699 uuid1.hash(&mut h1);
700 uuid2.hash(&mut h2);
701 prop_assert_eq!(h1.finish(), h2.finish());
702 }
703 }
704
705 #[rstest]
706 fn prop_uuid4_from_str_never_panics(s: String) {
707 let _ = UUID4::from_str(&s);
709 }
710
711 #[rstest]
712 fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
713 let uuid = UUID4::from_bytes(bytes);
716 let parsed = uuid::Uuid::parse_str(uuid.as_str()).unwrap();
717 prop_assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
718 prop_assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
719 }
720
721 #[rstest]
722 fn prop_from_bytes_as_bytes_roundtrip(bytes in any::<[u8; 16]>()) {
723 let mut expected = bytes;
726 expected[6] = (expected[6] & 0x0F) | 0x40;
727 expected[8] = (expected[8] & 0x3F) | 0x80;
728 let uuid = UUID4::from_bytes(bytes);
729 prop_assert_eq!(uuid.as_bytes(), expected);
730 }
731 }
732}