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
18use 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
32/// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator).
33pub(crate) const UUID4_LEN: usize = 37;
34
35/// Represents a Universally Unique Identifier (UUID)
36/// version 4 based on a 128-bit label as specified in RFC 4122.
37#[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    /// The UUID v4 value as a fixed-length C string byte array (includes null terminator).
49    pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array
50}
51
52impl UUID4 {
53    /// Creates a new [`UUID4`] instance.
54    ///
55    /// The UUID value is stored as a fixed-length C string byte array.
56    #[must_use]
57    pub fn new() -> Self {
58        let mut bytes = [0u8; 16];
59        #[cfg(all(feature = "simulation", madsim))]
60        {
61            // Deterministic RNG when running inside a madsim runtime; otherwise
62            // (e.g. plain `#[rstest]` tests under `cfg(madsim)`) fall back to
63            // the host RNG. Production paths under simulation always run inside
64            // a runtime, so they continue to consume seeded bytes.
65            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); // dst-ok: tests outside a madsim runtime
69            }
70        }
71        #[cfg(not(all(feature = "simulation", madsim)))]
72        rand::rng().fill_bytes(&mut bytes);
73
74        bytes[6] = (bytes[6] & 0x0F) | 0x40; // Set the version to 4
75        bytes[8] = (bytes[8] & 0x3F) | 0x80; // Set the variant to RFC 4122
76
77        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; // Add the null terminator
94
95        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    /// Creates a [`UUID4`] from raw 16-byte representation.
114    ///
115    /// Sets the version-4 nibble and the RFC 4122 variant bits before constructing,
116    /// so any 16 bytes produce a valid v4 UUID.
117    #[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    /// Converts the [`UUID4`] to a C string reference.
125    ///
126    /// # Panics
127    ///
128    /// Panics if the internal byte array is not a valid C string (does not end with a null terminator).
129    #[must_use]
130    pub fn to_cstr(&self) -> &CStr {
131        // We always store valid C strings
132        CStr::from_bytes_with_nul(&self.value)
133            .expect("UUID byte representation should be a valid C string")
134    }
135
136    /// Returns the UUID as a string slice.
137    ///
138    /// # Panics
139    ///
140    /// Never panics in practice: the stored byte representation is constructed
141    /// from valid ASCII UUID strings by [`UUID4::new`] or deserialization paths.
142    #[must_use]
143    pub fn as_str(&self) -> &str {
144        // We always store valid ASCII UUID strings
145        self.to_cstr().to_str().expect("UUID should be valid UTF-8")
146    }
147
148    /// Returns the raw UUID bytes (16 bytes).
149    ///
150    /// This method is optimized for serialization where the UUID bytes
151    /// are needed directly without string conversion overhead.
152    ///
153    /// # Panics
154    ///
155    /// Never panics in practice: the stored byte representation is a valid
156    /// UTF-8 UUID v4 string produced by [`UUID4::new`] or deserialization paths.
157    #[must_use]
158    pub fn as_bytes(&self) -> [u8; 16] {
159        // Parse the string representation to extract the raw bytes
160        // This is done once at read time to avoid repeated parsing
161        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        // Validate this is a v4 UUID
168        assert_eq!(
169            uuid.get_version(),
170            Some(uuid::Version::Random),
171            "UUID is not version 4"
172        );
173
174        // Validate RFC4122 variant
175        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; // Add null terminator
198        Self { value }
199    }
200}
201
202impl FromStr for UUID4 {
203    type Err = String;
204
205    /// Attempts to create a [`UUID4`] from a string representation.
206    ///
207    /// The string should be a valid UUID in the standard format (e.g., "2d89666b-1a1e-4a75-b193-4eb3b454c757").
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the `value` is not a valid UUID version 4 RFC 4122.
212    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    /// Creates a [`UUID4`] from a [`uuid::Uuid`].
233    ///
234    /// # Panics
235    ///
236    /// Panics if the `value` is not a valid UUID version 4 RFC 4122.
237    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    /// Creates a [`uuid::Uuid`] from a [`UUID4`].
245    fn from(value: UUID4) -> Self {
246        Self::from_bytes(value.as_bytes())
247    }
248}
249
250impl Default for UUID4 {
251    /// Creates a new default [`UUID4`] instance.
252    ///
253    /// The default UUID4 is simply a newly generated UUID version 4.
254    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        // Version 4 requires bits: 0b0100xxxx
312        assert_eq!(&uuid_string[14..15], "4");
313        // RFC4122 variant requires bits: 0b10xxxxxx
314        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        // Check null termination
324        assert_eq!(bytes[36], 0);
325
326        // Verify dash positions
327        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"; // v1 UUID
340        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")] // v1 (time-based)
356    #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] // v2 (DCE Security)
357    #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] // v3 (MD5 hash)
358    #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] // v5 (SHA-1 hash)
359    #[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        // Valid v4 but wrong variant
368        let uuid = "550e8400-e29b-41d4-0000-446655440000";
369        let _ = UUID4::from(uuid);
370    }
371
372    #[rstest]
373    #[case("")] // Empty string
374    #[case("not-a-uuid-at-all")] // Invalid format
375    #[case("6ba7b810-9dad-11d1-80b4")] // Too short
376    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] // Too long
377    #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] // Wrong separator
378    #[case("6ba7b81019dad111d180b400c04fd430c8")] // No separators
379    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] // Truncated
380    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] // Invalid hex character
381    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        // Reconstruct UUID from bytes and verify it matches
501        let reconstructed = Uuid::from_bytes(bytes);
502        assert_eq!(reconstructed.to_string(), uuid_string);
503
504        // Verify version 4
505        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        // A well-formed v4 / RFC 4122 input should be preserved verbatim.
520        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        // Input has version bits indicating v1 (0x10..): `from_bytes` must coerce to v4.
532        let mut bytes = [0u8; 16];
533        bytes[6] = 0x1a; // High nibble is version; 1 means v1
534        bytes[8] = 0x80; // Already RFC 4122
535        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        // Input has variant bits indicating non-RFC-4122 (0x00..): `from_bytes` must coerce.
544        let mut bytes = [0u8; 16];
545        bytes[6] = 0x40; // Already v4
546        bytes[8] = 0x00; // Non-RFC-4122 variant
547        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        // After normalization, byte 6 is 0x40 and byte 8 is 0x80, so the canonical representation
556        // is "00000000-0000-4000-8000-000000000000", still a valid v4 UUID.
557        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        // For inputs whose bits 6 and 8 are already v4/RFC-4122, `as_bytes` ∘ `from_bytes` is the
571        // identity.
572        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\"")] // Invalid format
580    #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] // v1 UUID (wrong version)
581    #[case("\"\"")] // Empty string
582    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        // Build from proptest-generated bytes for deterministic
589        // reproduction and shrinking on failure
590        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            // Invariant: Total length is always 36 characters + null terminator
615            prop_assert_eq!(s.len(), 36);
616            prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
617
618            // Invariant: Dash positions per RFC 4122
619            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            // Invariant: Version digit must be '4' (index 14)
625            prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
626
627            // Invariant: Variant bits must be RFC 4122 (index 19)
628            // Binary: 10xx -> Hex: 8, 9, a, b
629            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            // Identity
646            prop_assert_eq!(uuid1, uuid1);
647
648            // Equality implies hash equality
649            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            // Fuzzing the parser with arbitrary strings
661            let _ = UUID4::from_str(&s);
662        }
663
664        #[rstest]
665        fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
666            // Any 16-byte input must produce a UUID that passes both v4 and RFC 4122 checks,
667            // because `from_bytes` unconditionally normalizes the version and variant nibbles.
668            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            // `as_bytes` must reflect exactly the bits `from_bytes` produced: the input
677            // bytes after version/variant normalization.
678            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}