Skip to main content

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
18// pyo3's `from_py_object` generates `.clone()` on `Copy` fields that clippy flags from the
19// macro expansion; an item-level `allow` cannot reach the expansion
20#![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
37/// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator).
38pub(crate) const UUID4_LEN: usize = 37;
39
40/// Represents a Universally Unique Identifier (UUID)
41/// version 4 based on a 128-bit label as specified in RFC 4122.
42#[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    /// The UUID v4 value as a fixed-length C string byte array (includes null terminator).
54    pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array
55}
56
57impl UUID4 {
58    /// Creates a new [`UUID4`] instance.
59    ///
60    /// The UUID value is stored as a fixed-length C string byte array.
61    #[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    /// Creates raw `UUIDv4` bytes.
70    #[must_use]
71    pub fn new_bytes() -> [u8; 16] {
72        let mut bytes = [0u8; 16];
73        #[cfg(all(feature = "simulation", madsim))]
74        {
75            // Deterministic RNG when running inside a madsim runtime; otherwise
76            // (e.g. plain `#[rstest]` tests under `cfg(madsim)`) fall back to
77            // the host RNG. Production paths under simulation always run inside
78            // a runtime, so they continue to consume seeded bytes.
79            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); // dst-ok: tests outside a madsim runtime
83            }
84        }
85        #[cfg(not(all(feature = "simulation", madsim)))]
86        rand::rng().fill_bytes(&mut bytes);
87
88        bytes[6] = (bytes[6] & 0x0F) | 0x40; // Set the version to 4
89        bytes[8] = (bytes[8] & 0x3F) | 0x80; // Set the variant to RFC 4122
90
91        bytes
92    }
93
94    /// Creates a [`UUID4`] from raw 16-byte representation.
95    ///
96    /// Sets the version-4 nibble and the RFC 4122 variant bits before constructing,
97    /// so any 16 bytes produce a valid v4 UUID.
98    #[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    /// Converts the [`UUID4`] to a C string reference.
108    ///
109    /// # Panics
110    ///
111    /// Panics if the internal byte array is not a valid C string (does not end with a null terminator).
112    #[must_use]
113    pub fn to_cstr(&self) -> &CStr {
114        // We always store valid C strings
115        CStr::from_bytes_with_nul(&self.value)
116            .expect("UUID byte representation should be a valid C string")
117    }
118
119    /// Returns the UUID as a string slice.
120    ///
121    /// # Panics
122    ///
123    /// Never panics in practice: the stored byte representation is constructed
124    /// from valid ASCII UUID strings by [`UUID4::new`] or deserialization paths.
125    #[must_use]
126    pub fn as_str(&self) -> &str {
127        // We always store valid ASCII UUID strings
128        self.to_cstr().to_str().expect("UUID should be valid UTF-8")
129    }
130
131    /// Returns the raw UUID bytes (16 bytes).
132    ///
133    /// Parses the stored string representation on each call; cache the result
134    /// when the bytes are needed repeatedly in hot paths.
135    ///
136    /// # Panics
137    ///
138    /// Never panics in practice: the stored byte representation is a valid
139    /// UTF-8 UUID v4 string produced by [`UUID4::new`] or deserialization paths.
140    #[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        // Validate this is a v4 UUID
149        assert_eq!(
150            uuid.get_version(),
151            Some(uuid::Version::Random),
152            "UUID is not version 4"
153        );
154
155        // Validate RFC4122 variant
156        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    /// Attempts to create a [`UUID4`] from a string representation.
185    ///
186    /// The string should be a valid UUID in the standard format (e.g., "2d89666b-1a1e-4a75-b193-4eb3b454c757").
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if the `value` is not a valid UUID version 4 RFC 4122.
191    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    /// Creates a [`UUID4`] from a [`uuid::Uuid`].
212    ///
213    /// # Panics
214    ///
215    /// Panics if the `value` is not a valid UUID version 4 RFC 4122.
216    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    /// Creates a [`uuid::Uuid`] from a [`UUID4`].
224    fn from(value: UUID4) -> Self {
225        Self::from_bytes(value.as_bytes())
226    }
227}
228
229impl Default for UUID4 {
230    /// Creates a new default [`UUID4`] instance.
231    ///
232    /// The default UUID4 is simply a newly generated UUID version 4.
233    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; // Add the null terminator
284
285    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        // Version 4 requires bits: 0b0100xxxx
327        assert_eq!(&uuid_string[14..15], "4");
328        // RFC4122 variant requires bits: 0b10xxxxxx
329        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        // Check null termination
349        assert_eq!(bytes[36], 0);
350
351        // Verify dash positions
352        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"; // v1 UUID
382        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")] // v1 (time-based)
398    #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] // v2 (DCE Security)
399    #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] // v3 (MD5 hash)
400    #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] // v5 (SHA-1 hash)
401    #[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        // Valid v4 but wrong variant
410        let uuid = "550e8400-e29b-41d4-0000-446655440000";
411        let _ = UUID4::from(uuid);
412    }
413
414    #[rstest]
415    #[case("")] // Empty string
416    #[case("not-a-uuid-at-all")] // Invalid format
417    #[case("6ba7b810-9dad-11d1-80b4")] // Too short
418    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] // Too long
419    #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] // Wrong separator
420    #[case("6ba7b81019dad111d180b400c04fd430c8")] // No separators
421    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] // Truncated
422    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] // Invalid hex character
423    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        // Reconstruct UUID from bytes and verify it matches
552        let reconstructed = Uuid::from_bytes(bytes);
553        assert_eq!(reconstructed.to_string(), uuid_string);
554
555        // Verify version 4
556        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        // A well-formed v4 / RFC 4122 input should be preserved verbatim.
571        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        // Input has version bits indicating v1 (0x10..): `from_bytes` must coerce to v4.
583        let mut bytes = [0u8; 16];
584        bytes[6] = 0x1a; // High nibble is version; 1 means v1
585        bytes[8] = 0x80; // Already RFC 4122
586        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        // Input has variant bits indicating non-RFC-4122 (0x00..): `from_bytes` must coerce.
595        let mut bytes = [0u8; 16];
596        bytes[6] = 0x40; // Already v4
597        bytes[8] = 0x00; // Non-RFC-4122 variant
598        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        // After normalization, byte 6 is 0x40 and byte 8 is 0x80, so the canonical representation
607        // is "00000000-0000-4000-8000-000000000000", still a valid v4 UUID.
608        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        // For inputs whose bits 6 and 8 are already v4/RFC-4122, `as_bytes` ∘ `from_bytes` is the
622        // identity.
623        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\"")] // Invalid format
631    #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] // v1 UUID (wrong version)
632    #[case("\"\"")] // Empty string
633    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        // Build from proptest-generated bytes for deterministic
640        // reproduction and shrinking on failure
641        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            // Invariant: Total length is always 36 characters + null terminator
666            prop_assert_eq!(s.len(), 36);
667            prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
668
669            // Invariant: Dash positions per RFC 4122
670            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            // Invariant: Version digit must be '4' (index 14)
676            prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
677
678            // Invariant: Variant bits must be RFC 4122 (index 19)
679            // Binary: 10xx -> Hex: 8, 9, a, b
680            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            // Fuzzing the parser with arbitrary strings
709            let _ = UUID4::from_str(&s);
710        }
711
712        #[rstest]
713        fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
714            // Any 16-byte input must produce a UUID that passes both v4 and RFC 4122 checks,
715            // because `from_bytes` unconditionally normalizes the version and variant nibbles.
716            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            // `as_bytes` must reflect exactly the bits `from_bytes` produced: the input
725            // bytes after version/variant normalization.
726            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}