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    str::FromStr,
23};
24
25#[cfg(all(feature = "simulation", madsim))]
26use madsim::rand::RngCore as MadsimRngCore;
27use rand::Rng;
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29use uuid::Uuid;
30
31use crate::hex::ENCODE_PAIR;
32
33/// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator).
34pub(crate) const UUID4_LEN: usize = 37;
35
36/// Represents a Universally Unique Identifier (UUID)
37/// version 4 based on a 128-bit label as specified in RFC 4122.
38#[repr(C)]
39#[derive(Copy, Clone, Hash, PartialEq, Eq)]
40#[cfg_attr(
41    feature = "python",
42    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core", from_py_object)
43)]
44#[cfg_attr(
45    feature = "python",
46    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.core")
47)]
48pub struct UUID4 {
49    /// The UUID v4 value as a fixed-length C string byte array (includes null terminator).
50    pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array
51}
52
53impl UUID4 {
54    /// Creates a new [`UUID4`] instance.
55    ///
56    /// The UUID value is stored as a fixed-length C string byte array.
57    #[must_use]
58    pub fn new() -> Self {
59        let bytes = Self::new_bytes();
60        Self {
61            value: format_uuid4_bytes(bytes),
62        }
63    }
64
65    /// Creates raw `UUIDv4` bytes.
66    #[must_use]
67    pub fn new_bytes() -> [u8; 16] {
68        let mut bytes = [0u8; 16];
69        #[cfg(all(feature = "simulation", madsim))]
70        {
71            // Deterministic RNG when running inside a madsim runtime; otherwise
72            // (e.g. plain `#[rstest]` tests under `cfg(madsim)`) fall back to
73            // the host RNG. Production paths under simulation always run inside
74            // a runtime, so they continue to consume seeded bytes.
75            if madsim::runtime::Handle::try_current().is_ok() {
76                MadsimRngCore::fill_bytes(&mut madsim::rand::thread_rng(), &mut bytes);
77            } else {
78                rand::rng().fill_bytes(&mut bytes); // dst-ok: tests outside a madsim runtime
79            }
80        }
81        #[cfg(not(all(feature = "simulation", madsim)))]
82        rand::rng().fill_bytes(&mut bytes);
83
84        bytes[6] = (bytes[6] & 0x0F) | 0x40; // Set the version to 4
85        bytes[8] = (bytes[8] & 0x3F) | 0x80; // Set the variant to RFC 4122
86
87        bytes
88    }
89
90    /// Creates a [`UUID4`] from raw 16-byte representation.
91    ///
92    /// Sets the version-4 nibble and the RFC 4122 variant bits before constructing,
93    /// so any 16 bytes produce a valid v4 UUID.
94    #[must_use]
95    pub fn from_bytes(mut bytes: [u8; 16]) -> Self {
96        bytes[6] = (bytes[6] & 0x0F) | 0x40;
97        bytes[8] = (bytes[8] & 0x3F) | 0x80;
98        Self::from_validated_uuid(&Uuid::from_bytes(bytes))
99    }
100
101    /// Converts the [`UUID4`] to a C string reference.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the internal byte array is not a valid C string (does not end with a null terminator).
106    #[must_use]
107    pub fn to_cstr(&self) -> &CStr {
108        // We always store valid C strings
109        CStr::from_bytes_with_nul(&self.value)
110            .expect("UUID byte representation should be a valid C string")
111    }
112
113    /// Returns the UUID as a string slice.
114    ///
115    /// # Panics
116    ///
117    /// Never panics in practice: the stored byte representation is constructed
118    /// from valid ASCII UUID strings by [`UUID4::new`] or deserialization paths.
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        // We always store valid ASCII UUID strings
122        self.to_cstr().to_str().expect("UUID should be valid UTF-8")
123    }
124
125    /// Returns the raw UUID bytes (16 bytes).
126    ///
127    /// Parses the stored string representation on each call; cache the result
128    /// when the bytes are needed repeatedly in hot paths.
129    ///
130    /// # Panics
131    ///
132    /// Never panics in practice: the stored byte representation is a valid
133    /// UTF-8 UUID v4 string produced by [`UUID4::new`] or deserialization paths.
134    #[must_use]
135    pub fn as_bytes(&self) -> [u8; 16] {
136        let uuid_str = self.to_cstr().to_str().expect("Valid UTF-8");
137        let uuid = Uuid::parse_str(uuid_str).expect("Valid UUID4");
138        *uuid.as_bytes()
139    }
140
141    fn validate_v4(uuid: &Uuid) {
142        // Validate this is a v4 UUID
143        assert_eq!(
144            uuid.get_version(),
145            Some(uuid::Version::Random),
146            "UUID is not version 4"
147        );
148
149        // Validate RFC4122 variant
150        assert_eq!(
151            uuid.get_variant(),
152            uuid::Variant::RFC4122,
153            "UUID is not RFC 4122 variant"
154        );
155    }
156
157    fn try_validate_v4(uuid: &Uuid) -> Result<(), String> {
158        if uuid.get_version() != Some(uuid::Version::Random) {
159            return Err("UUID is not version 4".to_string());
160        }
161
162        if uuid.get_variant() != uuid::Variant::RFC4122 {
163            return Err("UUID is not RFC 4122 variant".to_string());
164        }
165        Ok(())
166    }
167
168    fn from_validated_uuid(uuid: &Uuid) -> Self {
169        let mut value = [0; UUID4_LEN];
170        let uuid_str = uuid.to_string();
171        value[..uuid_str.len()].copy_from_slice(uuid_str.as_bytes());
172        value[uuid_str.len()] = 0; // Add null terminator
173        Self { value }
174    }
175}
176
177impl FromStr for UUID4 {
178    type Err = String;
179
180    /// Attempts to create a [`UUID4`] from a string representation.
181    ///
182    /// The string should be a valid UUID in the standard format (e.g., "2d89666b-1a1e-4a75-b193-4eb3b454c757").
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the `value` is not a valid UUID version 4 RFC 4122.
187    fn from_str(value: &str) -> Result<Self, Self::Err> {
188        let uuid = Uuid::try_parse(value).map_err(|e| e.to_string())?;
189        Self::try_validate_v4(&uuid)?;
190        Ok(Self::from_validated_uuid(&uuid))
191    }
192}
193
194impl From<&str> for UUID4 {
195    fn from(value: &str) -> Self {
196        Self::from_str(value).expect("Invalid UUID4 string")
197    }
198}
199
200impl From<String> for UUID4 {
201    fn from(value: String) -> Self {
202        Self::from_str(&value).expect("Invalid UUID4 string")
203    }
204}
205
206impl From<uuid::Uuid> for UUID4 {
207    /// Creates a [`UUID4`] from a [`uuid::Uuid`].
208    ///
209    /// # Panics
210    ///
211    /// Panics if the `value` is not a valid UUID version 4 RFC 4122.
212    fn from(value: uuid::Uuid) -> Self {
213        Self::validate_v4(&value);
214        Self::from_validated_uuid(&value)
215    }
216}
217
218impl From<UUID4> for uuid::Uuid {
219    /// Creates a [`uuid::Uuid`] from a [`UUID4`].
220    fn from(value: UUID4) -> Self {
221        Self::from_bytes(value.as_bytes())
222    }
223}
224
225impl Default for UUID4 {
226    /// Creates a new default [`UUID4`] instance.
227    ///
228    /// The default UUID4 is simply a newly generated UUID version 4.
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234impl Debug for UUID4 {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        write!(f, "{}({})", stringify!(UUID4), self)
237    }
238}
239
240impl Display for UUID4 {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        write!(f, "{}", self.to_cstr().to_string_lossy())
243    }
244}
245
246impl Serialize for UUID4 {
247    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248    where
249        S: Serializer,
250    {
251        self.to_string().serialize(serializer)
252    }
253}
254
255impl<'de> Deserialize<'de> for UUID4 {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: Deserializer<'de>,
259    {
260        let uuid4_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
261        uuid4_str.as_ref().parse().map_err(serde::de::Error::custom)
262    }
263}
264
265fn format_uuid4_bytes(bytes: [u8; 16]) -> [u8; UUID4_LEN] {
266    let mut value = [0u8; UUID4_LEN];
267    let mut pos = 0;
268
269    for (idx, byte) in bytes.into_iter().enumerate() {
270        if matches!(idx, 4 | 6 | 8 | 10) {
271            value[pos] = b'-';
272            pos += 1;
273        }
274
275        value[pos..pos + 2].copy_from_slice(&ENCODE_PAIR[byte as usize]);
276        pos += 2;
277    }
278
279    value[36] = 0; // Add the null terminator
280
281    debug_assert_eq!(pos, 36, "Invariant: UUID text must be 36 bytes");
282    debug_assert!(
283        value[14] == b'4',
284        "Invariant: UUID version digit must be '4' (was {})",
285        value[14] as char
286    );
287    debug_assert!(
288        matches!(value[19], b'8' | b'9' | b'a' | b'b'),
289        "Invariant: UUID variant byte must be RFC 4122 (was {})",
290        value[19] as char
291    );
292    debug_assert!(
293        value[36] == 0,
294        "Invariant: UUID null terminator must be at index 36"
295    );
296
297    value
298}
299
300#[cfg(test)]
301mod tests {
302    use std::{
303        collections::hash_map::DefaultHasher,
304        ffi::CStr,
305        hash::{Hash, Hasher},
306    };
307
308    use proptest::prelude::*;
309    use rstest::*;
310    use uuid;
311
312    use super::*;
313
314    #[rstest]
315    fn test_new() {
316        let uuid = UUID4::new();
317        let uuid_string = uuid.to_string();
318        let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
319        assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
320        assert_eq!(uuid_parsed.to_string().len(), 36);
321
322        // Version 4 requires bits: 0b0100xxxx
323        assert_eq!(&uuid_string[14..15], "4");
324        // RFC4122 variant requires bits: 0b10xxxxxx
325        let variant_char = &uuid_string[19..20];
326        assert!(matches!(variant_char, "8" | "9" | "a" | "b" | "A" | "B"));
327    }
328
329    #[rstest]
330    fn test_new_bytes() {
331        let bytes = UUID4::new_bytes();
332        let uuid = UUID4::from_bytes(bytes);
333
334        assert_eq!(bytes[6] >> 4, 4);
335        assert!(matches!(bytes[8] >> 6, 0b10));
336        assert_eq!(uuid.as_bytes(), bytes);
337    }
338
339    #[rstest]
340    fn test_uuid_format() {
341        let uuid = UUID4::new();
342        let bytes = uuid.value;
343
344        // Check null termination
345        assert_eq!(bytes[36], 0);
346
347        // Verify dash positions
348        assert_eq!(bytes[8] as char, '-');
349        assert_eq!(bytes[13] as char, '-');
350        assert_eq!(bytes[18] as char, '-');
351        assert_eq!(bytes[23] as char, '-');
352
353        let s = uuid.to_string();
354        assert_eq!(s.chars().nth(14).unwrap(), '4');
355    }
356
357    #[rstest]
358    fn test_format_uuid4_bytes_golden() {
359        let bytes = [
360            0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
361            0xc7, 0x57,
362        ];
363
364        let formatted = format_uuid4_bytes(bytes);
365        let text = CStr::from_bytes_with_nul(&formatted)
366            .unwrap()
367            .to_str()
368            .unwrap();
369
370        assert_eq!(text, "2d89666b-1a1e-4a75-b193-4eb3b454c757");
371        assert_eq!(formatted[36], 0);
372    }
373
374    #[rstest]
375    #[should_panic(expected = "UUID is not version 4")]
376    fn test_from_str_with_non_version_4_uuid_panics() {
377        let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; // v1 UUID
378        let _ = UUID4::from(uuid_string);
379    }
380
381    #[rstest]
382    fn test_case_insensitive_parsing() {
383        let upper = "2D89666B-1A1E-4A75-B193-4EB3B454C757";
384        let lower = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
385        let uuid_upper = UUID4::from(upper);
386        let uuid_lower = UUID4::from(lower);
387
388        assert_eq!(uuid_upper, uuid_lower);
389        assert_eq!(uuid_upper.to_string(), lower);
390    }
391
392    #[rstest]
393    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8")] // v1 (time-based)
394    #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] // v2 (DCE Security)
395    #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] // v3 (MD5 hash)
396    #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] // v5 (SHA-1 hash)
397    #[should_panic(expected = "UUID is not version 4")]
398    fn test_invalid_version(#[case] uuid_string: &str) {
399        let _ = UUID4::from(uuid_string);
400    }
401
402    #[rstest]
403    #[should_panic(expected = "UUID is not RFC 4122 variant")]
404    fn test_non_rfc4122_variant() {
405        // Valid v4 but wrong variant
406        let uuid = "550e8400-e29b-41d4-0000-446655440000";
407        let _ = UUID4::from(uuid);
408    }
409
410    #[rstest]
411    #[case("")] // Empty string
412    #[case("not-a-uuid-at-all")] // Invalid format
413    #[case("6ba7b810-9dad-11d1-80b4")] // Too short
414    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] // Too long
415    #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] // Wrong separator
416    #[case("6ba7b81019dad111d180b400c04fd430c8")] // No separators
417    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] // Truncated
418    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] // Invalid hex character
419    fn test_invalid_uuid_cases(#[case] invalid_uuid: &str) {
420        assert!(UUID4::from_str(invalid_uuid).is_err());
421    }
422
423    #[rstest]
424    fn test_default() {
425        let uuid: UUID4 = UUID4::default();
426        let uuid_string = uuid.to_string();
427        let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
428        assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
429    }
430
431    #[rstest]
432    fn test_from_str() {
433        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
434        let uuid = UUID4::from(uuid_string);
435        let result_string = uuid.to_string();
436        let result_parsed = Uuid::parse_str(&result_string).unwrap();
437        let expected_parsed = Uuid::parse_str(uuid_string).unwrap();
438        assert_eq!(result_parsed, expected_parsed);
439    }
440
441    #[rstest]
442    fn test_from_uuid() {
443        let original = uuid::Uuid::new_v4();
444        let uuid4 = UUID4::from(original);
445        assert_eq!(uuid4.to_string(), original.to_string());
446    }
447
448    #[rstest]
449    fn test_equality() {
450        let uuid1 = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
451        let uuid2 = UUID4::from("46922ecb-4324-4e40-a56c-841e0d774cef");
452        assert_eq!(uuid1, uuid1);
453        assert_ne!(uuid1, uuid2);
454    }
455
456    #[rstest]
457    fn test_debug() {
458        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
459        let uuid = UUID4::from(uuid_string);
460        assert_eq!(format!("{uuid:?}"), format!("UUID4({uuid_string})"));
461    }
462
463    #[rstest]
464    fn test_display() {
465        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
466        let uuid = UUID4::from(uuid_string);
467        assert_eq!(format!("{uuid}"), uuid_string);
468    }
469
470    #[rstest]
471    fn test_to_cstr() {
472        let uuid = UUID4::new();
473        let cstr = uuid.to_cstr();
474
475        assert_eq!(cstr.to_str().unwrap(), uuid.to_string());
476        assert_eq!(cstr.to_bytes_with_nul()[36], 0);
477    }
478
479    #[rstest]
480    fn test_as_str() {
481        let uuid = UUID4::new();
482        let s = uuid.as_str();
483
484        assert_eq!(s, uuid.to_string());
485        assert_eq!(s.len(), 36);
486    }
487
488    #[rstest]
489    fn test_hash_consistency() {
490        let uuid = UUID4::new();
491
492        let mut hasher1 = DefaultHasher::new();
493        let mut hasher2 = DefaultHasher::new();
494
495        uuid.hash(&mut hasher1);
496        uuid.hash(&mut hasher2);
497
498        assert_eq!(hasher1.finish(), hasher2.finish());
499    }
500
501    #[rstest]
502    fn test_serialize_json() {
503        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
504        let uuid = UUID4::from(uuid_string);
505
506        let serialized = serde_json::to_string(&uuid).unwrap();
507        let expected_json = format!("\"{uuid_string}\"");
508        assert_eq!(serialized, expected_json);
509    }
510
511    #[rstest]
512    fn test_deserialize_json() {
513        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
514        let serialized = format!("\"{uuid_string}\"");
515
516        let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
517        assert_eq!(deserialized.to_string(), uuid_string);
518    }
519
520    #[rstest]
521    fn test_deserialize_from_owned_value() {
522        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
523        let value = serde_json::Value::String(uuid_string.to_string());
524
525        let deserialized: UUID4 = serde_json::from_value(value).unwrap();
526        assert_eq!(deserialized.to_string(), uuid_string);
527    }
528
529    #[rstest]
530    fn test_serialize_deserialize_round_trip() {
531        let uuid = UUID4::new();
532
533        let serialized = serde_json::to_string(&uuid).unwrap();
534        let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
535
536        assert_eq!(uuid, deserialized);
537    }
538
539    #[rstest]
540    fn test_as_bytes() {
541        let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
542        let uuid = UUID4::from(uuid_string);
543
544        let bytes = uuid.as_bytes();
545        assert_eq!(bytes.len(), 16);
546
547        // Reconstruct UUID from bytes and verify it matches
548        let reconstructed = Uuid::from_bytes(bytes);
549        assert_eq!(reconstructed.to_string(), uuid_string);
550
551        // Verify version 4
552        assert_eq!(reconstructed.get_version().unwrap(), uuid::Version::Random);
553    }
554
555    #[rstest]
556    fn test_as_bytes_round_trip() {
557        let uuid1 = UUID4::new();
558        let bytes = uuid1.as_bytes();
559        let uuid2 = UUID4::from(Uuid::from_bytes(bytes));
560
561        assert_eq!(uuid1, uuid2);
562    }
563
564    #[rstest]
565    fn test_from_bytes_basic() {
566        // A well-formed v4 / RFC 4122 input should be preserved verbatim.
567        let bytes = [
568            0x2d, 0x89, 0x66, 0x6b, 0x1a, 0x1e, 0x4a, 0x75, 0xb1, 0x93, 0x4e, 0xb3, 0xb4, 0x54,
569            0xc7, 0x57,
570        ];
571        let uuid = UUID4::from_bytes(bytes);
572        assert_eq!(uuid.to_string(), "2d89666b-1a1e-4a75-b193-4eb3b454c757");
573        assert_eq!(uuid.as_bytes(), bytes);
574    }
575
576    #[rstest]
577    fn test_from_bytes_normalizes_version() {
578        // Input has version bits indicating v1 (0x10..): `from_bytes` must coerce to v4.
579        let mut bytes = [0u8; 16];
580        bytes[6] = 0x1a; // High nibble is version; 1 means v1
581        bytes[8] = 0x80; // Already RFC 4122
582        let uuid = UUID4::from_bytes(bytes);
583        assert_eq!(&uuid.to_string()[14..15], "4");
584        let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
585        assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
586    }
587
588    #[rstest]
589    fn test_from_bytes_normalizes_variant() {
590        // Input has variant bits indicating non-RFC-4122 (0x00..): `from_bytes` must coerce.
591        let mut bytes = [0u8; 16];
592        bytes[6] = 0x40; // Already v4
593        bytes[8] = 0x00; // Non-RFC-4122 variant
594        let uuid = UUID4::from_bytes(bytes);
595        let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
596        assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
597    }
598
599    #[rstest]
600    fn test_from_bytes_all_zero_is_valid_v4() {
601        let uuid = UUID4::from_bytes([0u8; 16]);
602        // After normalization, byte 6 is 0x40 and byte 8 is 0x80, so the canonical representation
603        // is "00000000-0000-4000-8000-000000000000", still a valid v4 UUID.
604        assert_eq!(uuid.to_string(), "00000000-0000-4000-8000-000000000000");
605    }
606
607    #[rstest]
608    fn test_from_bytes_all_ones_is_valid_v4() {
609        let uuid = UUID4::from_bytes([0xFFu8; 16]);
610        let parsed = Uuid::parse_str(uuid.as_str()).unwrap();
611        assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
612        assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
613    }
614
615    #[rstest]
616    fn test_from_bytes_round_trip() {
617        // For inputs whose bits 6 and 8 are already v4/RFC-4122, `as_bytes` ∘ `from_bytes` is the
618        // identity.
619        let original = UUID4::new();
620        let bytes = original.as_bytes();
621        let reconstructed = UUID4::from_bytes(bytes);
622        assert_eq!(original, reconstructed);
623    }
624
625    #[rstest]
626    #[case("\"not-a-uuid\"")] // Invalid format
627    #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] // v1 UUID (wrong version)
628    #[case("\"\"")] // Empty string
629    fn test_deserialize_invalid_uuid_returns_error(#[case] json: &str) {
630        let result: Result<UUID4, _> = serde_json::from_str(json);
631        assert!(result.is_err());
632    }
633
634    fn uuid4_strategy() -> impl Strategy<Value = UUID4> {
635        // Build from proptest-generated bytes for deterministic
636        // reproduction and shrinking on failure
637        any::<[u8; 16]>().prop_map(UUID4::from_bytes)
638    }
639
640    proptest! {
641        #[rstest]
642        fn prop_uuid4_string_roundtrip(uuid in uuid4_strategy()) {
643            let s = uuid.to_string();
644            let parsed = UUID4::from_str(&s);
645            prop_assert!(parsed.is_ok(), "Failed to parse UUID string: {}", s);
646            prop_assert_eq!(parsed.unwrap(), uuid, "String round-trip failed");
647        }
648
649        #[rstest]
650        fn prop_uuid4_serde_roundtrip(uuid in uuid4_strategy()) {
651            let serialized = serde_json::to_string(&uuid).unwrap();
652            let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
653            prop_assert_eq!(deserialized, uuid, "Serde JSON round-trip failed");
654        }
655
656        #[rstest]
657        fn prop_uuid4_rfc4122_compliance(uuid in uuid4_strategy()) {
658            let s = uuid.to_string();
659            let bytes = uuid.value;
660
661            // Invariant: Total length is always 36 characters + null terminator
662            prop_assert_eq!(s.len(), 36);
663            prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
664
665            // Invariant: Dash positions per RFC 4122
666            prop_assert_eq!(bytes[8] as char, '-');
667            prop_assert_eq!(bytes[13] as char, '-');
668            prop_assert_eq!(bytes[18] as char, '-');
669            prop_assert_eq!(bytes[23] as char, '-');
670
671            // Invariant: Version digit must be '4' (index 14)
672            prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
673
674            // Invariant: Variant bits must be RFC 4122 (index 19)
675            // Binary: 10xx -> Hex: 8, 9, a, b
676            let variant_char = s.chars().nth(19).unwrap().to_ascii_lowercase();
677            prop_assert!(
678                matches!(variant_char, '8' | '9' | 'a' | 'b'),
679                "Invalid variant character: {}", variant_char
680            );
681        }
682
683        #[rstest]
684        fn prop_uuid4_as_bytes_consistency(uuid in uuid4_strategy()) {
685            let bytes = uuid.as_bytes();
686            let reconstructed = uuid::Uuid::from_bytes(bytes);
687            prop_assert_eq!(reconstructed.to_string(), uuid.to_string(), "Byte reconstruction mismatch");
688        }
689
690        #[rstest]
691        fn prop_uuid4_equality_and_hashing(uuid1 in uuid4_strategy(), uuid2 in uuid4_strategy()) {
692            // Identity
693            prop_assert_eq!(uuid1, uuid1);
694
695            // Equality implies hash equality
696            if uuid1 == uuid2 {
697                let mut h1 = DefaultHasher::new();
698                let mut h2 = DefaultHasher::new();
699                uuid1.hash(&mut h1);
700                uuid2.hash(&mut h2);
701                prop_assert_eq!(h1.finish(), h2.finish());
702            }
703        }
704
705        #[rstest]
706        fn prop_uuid4_from_str_never_panics(s: String) {
707            // Fuzzing the parser with arbitrary strings
708            let _ = UUID4::from_str(&s);
709        }
710
711        #[rstest]
712        fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
713            // Any 16-byte input must produce a UUID that passes both v4 and RFC 4122 checks,
714            // because `from_bytes` unconditionally normalizes the version and variant nibbles.
715            let uuid = UUID4::from_bytes(bytes);
716            let parsed = uuid::Uuid::parse_str(uuid.as_str()).unwrap();
717            prop_assert_eq!(parsed.get_version(), Some(uuid::Version::Random));
718            prop_assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
719        }
720
721        #[rstest]
722        fn prop_from_bytes_as_bytes_roundtrip(bytes in any::<[u8; 16]>()) {
723            // `as_bytes` must reflect exactly the bits `from_bytes` produced: the input
724            // bytes after version/variant normalization.
725            let mut expected = bytes;
726            expected[6] = (expected[6] & 0x0F) | 0x40;
727            expected[8] = (expected[8] & 0x3F) | 0x80;
728            let uuid = UUID4::from_bytes(bytes);
729            prop_assert_eq!(uuid.as_bytes(), expected);
730        }
731    }
732}