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