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::{DECODE_NIBBLE, 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 #[allow(
124 clippy::missing_panics_doc,
125 reason = "the stored UUID representation contains only valid ASCII"
126 )]
127 #[must_use]
128 pub fn as_str(&self) -> &str {
129 self.to_cstr().to_str().expect("UUID should be valid UTF-8")
131 }
132
133 #[must_use]
142 pub fn as_bytes(&self) -> [u8; 16] {
143 const BYTE_POSITIONS: [usize; 16] =
145 [0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34];
146 let mut bytes = [0u8; 16];
147 let mut invalid_nibbles = 0u8;
148
149 for (byte, pos) in bytes.iter_mut().zip(BYTE_POSITIONS) {
150 let hi = DECODE_NIBBLE[self.value[pos] as usize];
151 let lo = DECODE_NIBBLE[self.value[pos + 1] as usize];
152 invalid_nibbles |= hi | lo;
153 *byte = (hi << 4) | lo;
154 }
155
156 let invalid_delimiters = self.value[36]
157 | (self.value[8] ^ b'-')
158 | (self.value[13] ^ b'-')
159 | (self.value[18] ^ b'-')
160 | (self.value[23] ^ b'-');
161 if (invalid_nibbles & 0xf0) | invalid_delimiters != 0 {
162 return self.as_bytes_checked();
163 }
164
165 bytes
166 }
167
168 #[cold]
169 #[track_caller]
170 fn as_bytes_checked(&self) -> [u8; 16] {
171 let text = self.to_cstr().to_str().expect("Valid UTF-8");
173 *Uuid::parse_str(text).expect("Valid UUID4").as_bytes()
174 }
175
176 fn validate_v4(uuid: &Uuid) {
177 assert_eq!(
179 uuid.get_version(),
180 Some(uuid::Version::Random),
181 "UUID is not version 4"
182 );
183
184 assert_eq!(
186 uuid.get_variant(),
187 uuid::Variant::RFC4122,
188 "UUID is not RFC 4122 variant"
189 );
190 }
191
192 fn try_validate_v4(uuid: &Uuid) -> Result<(), String> {
193 if uuid.get_version() != Some(uuid::Version::Random) {
194 return Err("UUID is not version 4".to_string());
195 }
196
197 if uuid.get_variant() != uuid::Variant::RFC4122 {
198 return Err("UUID is not RFC 4122 variant".to_string());
199 }
200 Ok(())
201 }
202
203 fn from_validated_uuid(uuid: &Uuid) -> Self {
204 Self {
205 value: format_uuid4_bytes(*uuid.as_bytes()),
206 }
207 }
208}
209
210impl FromStr for UUID4 {
211 type Err = String;
212
213 fn from_str(value: &str) -> Result<Self, Self::Err> {
221 let uuid = Uuid::try_parse(value).map_err(|e| e.to_string())?;
222 Self::try_validate_v4(&uuid)?;
223 Ok(Self::from_validated_uuid(&uuid))
224 }
225}
226
227impl From<&str> for UUID4 {
228 fn from(value: &str) -> Self {
229 Self::from_str(value).expect("Invalid UUID4 string")
230 }
231}
232
233impl From<String> for UUID4 {
234 fn from(value: String) -> Self {
235 Self::from_str(&value).expect("Invalid UUID4 string")
236 }
237}
238
239impl From<uuid::Uuid> for UUID4 {
240 fn from(value: uuid::Uuid) -> Self {
246 Self::validate_v4(&value);
247 Self::from_validated_uuid(&value)
248 }
249}
250
251impl From<UUID4> for uuid::Uuid {
252 fn from(value: UUID4) -> Self {
254 Self::from_bytes(value.as_bytes())
255 }
256}
257
258impl Default for UUID4 {
259 fn default() -> Self {
263 Self::new()
264 }
265}
266
267impl Debug for UUID4 {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 write!(f, "{}({})", stringify!(UUID4), self)
270 }
271}
272
273impl Display for UUID4 {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 write!(f, "{}", self.to_cstr().to_string_lossy())
276 }
277}
278
279impl Serialize for UUID4 {
280 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
281 where
282 S: Serializer,
283 {
284 serializer.serialize_str(self.as_str())
285 }
286}
287
288impl<'de> Deserialize<'de> for UUID4 {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: Deserializer<'de>,
292 {
293 let uuid4_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
294 uuid4_str.as_ref().parse().map_err(serde::de::Error::custom)
295 }
296}
297
298fn format_uuid4_bytes(bytes: [u8; 16]) -> [u8; UUID4_LEN] {
299 let mut value = [0u8; UUID4_LEN];
300 let mut pos = 0;
301
302 for (idx, byte) in bytes.into_iter().enumerate() {
303 if matches!(idx, 4 | 6 | 8 | 10) {
304 value[pos] = b'-';
305 pos += 1;
306 }
307
308 value[pos..pos + 2].copy_from_slice(&ENCODE_PAIR[byte as usize]);
309 pos += 2;
310 }
311
312 value[36] = 0; debug_assert_eq!(pos, 36, "Invariant: UUID text must be 36 bytes");
315 debug_assert!(
316 value[14] == b'4',
317 "Invariant: UUID version digit must be '4' (was {})",
318 value[14] as char
319 );
320 debug_assert!(
321 matches!(value[19], b'8' | b'9' | b'a' | b'b'),
322 "Invariant: UUID variant byte must be RFC 4122 (was {})",
323 value[19] as char
324 );
325 debug_assert!(
326 value[36] == 0,
327 "Invariant: UUID null terminator must be at index 36"
328 );
329
330 value
331}
332
333#[cfg(test)]
334mod tests {
335 use std::{
336 collections::hash_map::DefaultHasher,
337 ffi::CStr,
338 hash::{Hash, Hasher},
339 mem::{align_of, size_of},
340 };
341
342 use proptest::prelude::*;
343 use rstest::*;
344 use uuid;
345
346 use super::*;
347
348 #[rstest]
349 fn uuid4_layout_stays_c_string_compatible() {
350 assert_eq!(size_of::<UUID4>(), 37);
351 assert_eq!(align_of::<UUID4>(), 1);
352
353 let uuid = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
354 let text = uuid.as_str();
355
356 assert_eq!(text.len(), 36);
357 assert_eq!(&text[8..9], "-");
358 assert_eq!(&text[13..14], "-");
359 assert_eq!(&text[18..19], "-");
360 assert_eq!(&text[23..24], "-");
361 assert_eq!(&text[14..15], "4");
362 assert!(matches!(text.as_bytes()[19], b'8' | b'9' | b'a' | b'b'));
363 }
364
365 #[rstest]
366 fn test_new() {
367 let uuid = UUID4::new();
368 let uuid_string = uuid.to_string();
369 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
370 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
371 assert_eq!(uuid_parsed.to_string().len(), 36);
372
373 assert_eq!(&uuid_string[14..15], "4");
375 let variant_char = &uuid_string[19..20];
377 assert!(matches!(variant_char, "8" | "9" | "a" | "b" | "A" | "B"));
378 }
379
380 #[rstest]
381 fn test_new_bytes() {
382 let bytes = UUID4::new_bytes();
383 let uuid = UUID4::from_bytes(bytes);
384
385 assert_eq!(bytes[6] >> 4, 4);
386 assert!(matches!(bytes[8] >> 6, 0b10));
387 assert_eq!(uuid.as_bytes(), bytes);
388 }
389
390 #[cfg(all(feature = "simulation", madsim))]
391 #[rstest]
392 fn test_new_bytes_is_deterministic_in_virtual_time_runtime() {
393 let generate = |seed| {
394 let runtime =
395 madsim::runtime::Runtime::with_seed_and_config(seed, madsim::Config::default());
396 runtime.block_on(async { (0..4).map(|_| UUID4::new_bytes()).collect::<Vec<_>>() })
397 };
398
399 let first = generate(42);
400 let repeated = generate(42);
401 let different = generate(43);
402
403 assert_eq!(first, repeated);
404 assert_ne!(first, different);
405 }
406
407 #[rstest]
408 fn test_uuid_format() {
409 let uuid = UUID4::new();
410 let bytes = uuid.value;
411
412 assert_eq!(bytes[36], 0);
414
415 assert_eq!(bytes[8] as char, '-');
417 assert_eq!(bytes[13] as char, '-');
418 assert_eq!(bytes[18] as char, '-');
419 assert_eq!(bytes[23] as char, '-');
420
421 let s = uuid.to_string();
422 assert_eq!(s.chars().nth(14).unwrap(), '4');
423 }
424
425 #[rstest]
426 fn test_format_uuid4_bytes_golden() {
427 let bytes = [
428 0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
429 0xc7, 0x57,
430 ];
431
432 let formatted = format_uuid4_bytes(bytes);
433 let text = CStr::from_bytes_with_nul(&formatted)
434 .unwrap()
435 .to_str()
436 .unwrap();
437
438 assert_eq!(text, "2d89666b-1a1e-4a75-b193-4eb3b454c757");
439 assert_eq!(formatted[36], 0);
440 }
441
442 #[rstest]
443 #[should_panic(expected = "UUID is not version 4")]
444 fn test_from_str_with_non_version_4_uuid_panics() {
445 let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let _ = UUID4::from(uuid_string);
447 }
448
449 #[rstest]
450 fn test_case_insensitive_parsing() {
451 let upper = "2D89666B-1A1E-4A75-B193-4EB3B454C757";
452 let lower = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
453 let uuid_upper = UUID4::from(upper);
454 let uuid_lower = UUID4::from(lower);
455
456 assert_eq!(uuid_upper, uuid_lower);
457 assert_eq!(uuid_upper.to_string(), lower);
458 }
459
460 #[rstest]
461 #[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")]
466 fn test_invalid_version(#[case] uuid_string: &str) {
467 let _ = UUID4::from(uuid_string);
468 }
469
470 #[rstest]
471 #[should_panic(expected = "UUID is not RFC 4122 variant")]
472 fn test_non_rfc4122_variant() {
473 let uuid = "550e8400-e29b-41d4-0000-446655440000";
475 let _ = UUID4::from(uuid);
476 }
477
478 #[rstest]
479 #[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) {
488 assert!(UUID4::from_str(invalid_uuid).is_err());
489 }
490
491 #[rstest]
492 fn test_default() {
493 let uuid: UUID4 = UUID4::default();
494 let uuid_string = uuid.to_string();
495 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
496 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
497 }
498
499 #[rstest]
500 fn test_from_str() {
501 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
502 let uuid = UUID4::from(uuid_string);
503 let result_string = uuid.to_string();
504 let result_parsed = Uuid::parse_str(&result_string).unwrap();
505 let expected_parsed = Uuid::parse_str(uuid_string).unwrap();
506 assert_eq!(result_parsed, expected_parsed);
507 }
508
509 #[rstest]
510 fn test_from_string() {
511 let uuid = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757".to_string());
512 assert_eq!(uuid.as_str(), "2d89666b-1a1e-4a75-b193-4eb3b454c757");
513 }
514
515 #[rstest]
516 fn test_from_uuid() {
517 let original = uuid::Uuid::new_v4();
518 let uuid4 = UUID4::from(original);
519 assert_eq!(uuid4.to_string(), original.to_string());
520 }
521
522 #[rstest]
523 fn test_into_uuid_roundtrip() {
524 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
525 let uuid4 = UUID4::from(uuid_string);
526 let uuid = uuid::Uuid::from(uuid4);
527 assert_eq!(uuid.to_string(), uuid_string);
528 assert_eq!(UUID4::from(uuid).as_str(), uuid_string);
529 }
530
531 #[rstest]
532 fn test_equality() {
533 let uuid1 = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
534 let uuid2 = UUID4::from("46922ecb-4324-4e40-a56c-841e0d774cef");
535 assert_eq!(uuid1, uuid1);
536 assert_ne!(uuid1, uuid2);
537 }
538
539 #[rstest]
540 fn test_debug() {
541 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
542 let uuid = UUID4::from(uuid_string);
543 assert_eq!(format!("{uuid:?}"), format!("UUID4({uuid_string})"));
544 }
545
546 #[rstest]
547 fn test_display() {
548 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
549 let uuid = UUID4::from(uuid_string);
550 assert_eq!(format!("{uuid}"), uuid_string);
551 }
552
553 #[rstest]
554 fn test_to_cstr() {
555 let uuid = UUID4::new();
556 let cstr = uuid.to_cstr();
557
558 assert_eq!(cstr.to_str().unwrap(), uuid.to_string());
559 assert_eq!(cstr.to_bytes_with_nul()[36], 0);
560 }
561
562 #[rstest]
563 fn test_as_str() {
564 let uuid = UUID4::new();
565 let s = uuid.as_str();
566
567 assert_eq!(s, uuid.to_string());
568 assert_eq!(s.len(), 36);
569 }
570
571 #[rstest]
572 fn test_hash_consistency() {
573 let uuid = UUID4::new();
574
575 let mut hasher1 = DefaultHasher::new();
576 let mut hasher2 = DefaultHasher::new();
577
578 uuid.hash(&mut hasher1);
579 uuid.hash(&mut hasher2);
580
581 assert_eq!(hasher1.finish(), hasher2.finish());
582 }
583
584 #[rstest]
585 fn test_serialize_json() {
586 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
587 let uuid = UUID4::from(uuid_string);
588
589 let serialized = serde_json::to_string(&uuid).unwrap();
590 let expected_json = format!("\"{uuid_string}\"");
591 assert_eq!(serialized, expected_json);
592 }
593
594 #[rstest]
595 fn test_deserialize_json() {
596 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
597 let serialized = format!("\"{uuid_string}\"");
598
599 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
600 assert_eq!(deserialized.to_string(), uuid_string);
601 }
602
603 #[rstest]
604 fn test_deserialize_from_owned_value() {
605 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
606 let value = serde_json::Value::String(uuid_string.to_string());
607
608 let deserialized: UUID4 = serde_json::from_value(value).unwrap();
609 assert_eq!(deserialized.to_string(), uuid_string);
610 }
611
612 #[rstest]
613 fn test_serialize_deserialize_round_trip() {
614 let uuid = UUID4::new();
615
616 let serialized = serde_json::to_string(&uuid).unwrap();
617 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
618
619 assert_eq!(uuid, deserialized);
620 }
621
622 fn uuid_bytes_outcome(
623 convert: impl FnOnce() -> [u8; 16] + std::panic::UnwindSafe,
624 ) -> Result<[u8; 16], String> {
625 std::panic::catch_unwind(convert).map_err(|payload| {
626 if let Some(message) = payload.downcast_ref::<String>() {
627 message.clone()
628 } else {
629 payload.downcast_ref::<&str>().unwrap().to_string()
630 }
631 })
632 }
633
634 #[rstest]
635 #[case(36, b'x')]
636 #[case(9, 0)]
637 #[case(8, b'_')]
638 #[case(0, b'g')]
639 #[case(0, 0xff)]
640 fn test_as_bytes_preserves_raw_representation_errors(
641 #[case] position: usize,
642 #[case] replacement: u8,
643 ) {
644 let mut value = *b"2d89666b-1a1e-4a75-b193-4eb3b454c757\0";
646 value[position] = replacement;
647 let uuid = UUID4 { value };
648 let expected = uuid_bytes_outcome(|| {
649 let text = uuid.to_cstr().to_str().expect("Valid UTF-8");
650 *Uuid::parse_str(text).expect("Valid UUID4").as_bytes()
651 });
652 assert!(expected.is_err());
653 assert_eq!(uuid_bytes_outcome(|| uuid.as_bytes()), expected);
654 }
655
656 #[rstest]
657 fn test_as_bytes_preserves_raw_uuid_version_and_variant() {
658 let mut value = *b"2d89666b-1a1e-4a75-b193-4eb3b454c757\0";
659 value[14] = b'1';
660 value[19] = b'0';
661 let uuid = UUID4 { value };
662 let expected = *Uuid::parse_str(uuid.to_cstr().to_str().unwrap())
663 .unwrap()
664 .as_bytes();
665 assert_eq!(uuid.as_bytes(), expected);
666 }
667
668 #[rstest]
669 fn test_as_bytes() {
670 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
671 let uuid = UUID4::from(uuid_string);
672
673 let bytes = uuid.as_bytes();
674 assert_eq!(bytes.len(), 16);
675
676 let reconstructed = Uuid::from_bytes(bytes);
678 assert_eq!(reconstructed.to_string(), uuid_string);
679
680 assert_eq!(reconstructed.get_version().unwrap(), uuid::Version::Random);
682 }
683
684 #[rstest]
685 fn test_as_bytes_round_trip() {
686 let uuid1 = UUID4::new();
687 let bytes = uuid1.as_bytes();
688 let uuid2 = UUID4::from(Uuid::from_bytes(bytes));
689
690 assert_eq!(uuid1, uuid2);
691 }
692
693 #[rstest]
694 fn test_from_bytes_basic() {
695 let bytes = [
697 0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
698 0xc7, 0x57,
699 ];
700 let uuid = UUID4::from_bytes(bytes);
701 assert_eq!(uuid.to_string(), "2d89666b-1a1e-4a75-b193-4eb3b454c757");
702 assert_eq!(uuid.as_bytes(), bytes);
703 }
704
705 #[rstest]
706 fn test_from_bytes_normalizes_version() {
707 let mut bytes = [0u8; 16];
709 bytes[6] = 0x1a; bytes[8] = 0x80; let uuid = UUID4::from_bytes(bytes);
712 assert_eq!(&uuid.to_string()[14..15], "4");
713 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
714 assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
715 }
716
717 #[rstest]
718 fn test_from_bytes_normalizes_variant() {
719 let mut bytes = [0u8; 16];
721 bytes[6] = 0x40; bytes[8] = 0x00; let uuid = UUID4::from_bytes(bytes);
724 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
725 assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
726 }
727
728 #[rstest]
729 fn test_from_bytes_all_zero_is_valid_v4() {
730 let uuid = UUID4::from_bytes([0u8; 16]);
731 assert_eq!(uuid.to_string(), "00000000-0000-4000-8000-000000000000");
734 }
735
736 #[rstest]
737 fn test_from_bytes_all_ones_is_valid_v4() {
738 let uuid = UUID4::from_bytes([0xFFu8; 16]);
739 let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
740 assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
741 assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
742 }
743
744 #[rstest]
745 fn test_from_bytes_round_trip() {
746 let original = UUID4::new();
749 let bytes = original.as_bytes();
750 let reconstructed = UUID4::from_bytes(bytes);
751 assert_eq!(original, reconstructed);
752 }
753
754 #[rstest]
755 #[case("\"not-a-uuid\"")] #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] #[case("\"\"")] fn test_deserialize_invalid_uuid_returns_error(#[case] json: &str) {
759 let result: Result<UUID4, _> = serde_json::from_str(json);
760 assert!(result.is_err());
761 }
762
763 fn uuid4_strategy() -> impl Strategy<Value = UUID4> {
764 any::<[u8; 16]>().prop_map(UUID4::from_bytes)
767 }
768
769 proptest! {
770 #[rstest]
771 fn prop_uuid4_string_roundtrip(uuid in uuid4_strategy()) {
772 let s = uuid.to_string();
773 let parsed = UUID4::from_str(&s);
774 prop_assert!(parsed.is_ok(), "Failed to parse UUID string: {}", s);
775 prop_assert_eq!(parsed.unwrap(), uuid, "String round-trip failed");
776 }
777
778 #[rstest]
779 fn prop_uuid4_serde_roundtrip(uuid in uuid4_strategy()) {
780 let serialized = serde_json::to_string(&uuid).unwrap();
781 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
782 prop_assert_eq!(deserialized, uuid, "Serde JSON round-trip failed");
783 }
784
785 #[rstest]
786 fn prop_uuid4_rfc4122_compliance(uuid in uuid4_strategy()) {
787 let s = uuid.to_string();
788 let bytes = uuid.value;
789
790 prop_assert_eq!(s.len(), 36);
792 prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
793
794 prop_assert_eq!(bytes[8] as char, '-');
796 prop_assert_eq!(bytes[13] as char, '-');
797 prop_assert_eq!(bytes[18] as char, '-');
798 prop_assert_eq!(bytes[23] as char, '-');
799
800 prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
802
803 let variant_char = s.chars().nth(19).unwrap().to_ascii_lowercase();
806 prop_assert!(
807 matches!(variant_char, '8' | '9' | 'a' | 'b'),
808 "Invalid variant character: {}", variant_char
809 );
810 }
811
812 #[rstest]
813 fn prop_uuid4_as_bytes_consistency(uuid in uuid4_strategy()) {
814 let bytes = uuid.as_bytes();
815 let reconstructed = uuid::Uuid::from_bytes(bytes);
816 prop_assert_eq!(reconstructed.to_string(), uuid.to_string(), "Byte reconstruction mismatch");
817 }
818
819 #[rstest]
820 fn prop_uuid4_equality_and_hashing(uuid in uuid4_strategy()) {
821 let equivalent = uuid;
822 let mut first_hasher = DefaultHasher::new();
823 let mut second_hasher = DefaultHasher::new();
824 uuid.hash(&mut first_hasher);
825 equivalent.hash(&mut second_hasher);
826
827 prop_assert_eq!(uuid, equivalent);
828 prop_assert_eq!(first_hasher.finish(), second_hasher.finish());
829 }
830
831 #[rstest]
832 fn prop_uuid4_from_str_never_panics(s: String) {
833 let _ = UUID4::from_str(&s);
835 }
836
837 #[rstest]
838 fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
839 let uuid = UUID4::from_bytes(bytes);
842 let parsed = uuid::Uuid::parse_str(uuid.as_str()).unwrap();
843 prop_assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
844 prop_assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
845 }
846
847 #[rstest]
848 fn prop_from_bytes_as_bytes_roundtrip(bytes in any::<[u8; 16]>()) {
849 let mut expected = bytes;
852 expected[6] = (expected[6] & 0x0F) | 0x40;
853 expected[8] = (expected[8] & 0x3F) | 0x80;
854 let uuid = UUID4::from_bytes(bytes);
855 prop_assert_eq!(uuid.as_bytes(), expected);
856 }
857 }
858}