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::{DECODE_NIBBLE, 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    /// The stored byte representation is constructed from valid ASCII UUID strings by
122    /// [`UUID4::new`] or deserialization paths, so conversion to a string cannot panic.
123    #[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        // We always store valid ASCII UUID strings
130        self.to_cstr().to_str().expect("UUID should be valid UTF-8")
131    }
132
133    /// Returns the raw UUID bytes (16 bytes).
134    ///
135    /// Decodes the canonical stored hexadecimal representation on each call;
136    /// cache the result when the bytes are needed repeatedly in hot paths.
137    ///
138    /// # Panics
139    ///
140    /// Panics if the internal representation is not a valid C string, UTF-8 or UUID.
141    #[must_use]
142    pub fn as_bytes(&self) -> [u8; 16] {
143        // Constructors normalize the text to lowercase, hyphenated ASCII.
144        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        // Values crossing the C ABI retain the original validation and panic priority.
172        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        // Validate this is a v4 UUID
178        assert_eq!(
179            uuid.get_version(),
180            Some(uuid::Version::Random),
181            "UUID is not version 4"
182        );
183
184        // Validate RFC4122 variant
185        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    /// Attempts to create a [`UUID4`] from a string representation.
214    ///
215    /// The string should be a valid UUID in the standard format (e.g., "2d89666b-1a1e-4a75-b193-4eb3b454c757").
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the `value` is not a valid UUID version 4 RFC 4122.
220    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    /// Creates a [`UUID4`] from a [`uuid::Uuid`].
241    ///
242    /// # Panics
243    ///
244    /// Panics if the `value` is not a valid UUID version 4 RFC 4122.
245    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    /// Creates a [`uuid::Uuid`] from a [`UUID4`].
253    fn from(value: UUID4) -> Self {
254        Self::from_bytes(value.as_bytes())
255    }
256}
257
258impl Default for UUID4 {
259    /// Creates a new default [`UUID4`] instance.
260    ///
261    /// The default UUID4 is simply a newly generated UUID version 4.
262    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; // Add the null terminator
313
314    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        // Version 4 requires bits: 0b0100xxxx
374        assert_eq!(&uuid_string[14..15], "4");
375        // RFC4122 variant requires bits: 0b10xxxxxx
376        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        // Check null termination
413        assert_eq!(bytes[36], 0);
414
415        // Verify dash positions
416        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"; // v1 UUID
446        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")] // v1 (time-based)
462    #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] // v2 (DCE Security)
463    #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] // v3 (MD5 hash)
464    #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] // v5 (SHA-1 hash)
465    #[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        // Valid v4 but wrong variant
474        let uuid = "550e8400-e29b-41d4-0000-446655440000";
475        let _ = UUID4::from(uuid);
476    }
477
478    #[rstest]
479    #[case("")] // Empty string
480    #[case("not-a-uuid-at-all")] // Invalid format
481    #[case("6ba7b810-9dad-11d1-80b4")] // Too short
482    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] // Too long
483    #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] // Wrong separator
484    #[case("6ba7b81019dad111d180b400c04fd430c8")] // No separators
485    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] // Truncated
486    #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] // Invalid hex character
487    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        // UUID4 also crosses the C ABI by value, without a constructor-enforced representation.
645        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        // Reconstruct UUID from bytes and verify it matches
677        let reconstructed = Uuid::from_bytes(bytes);
678        assert_eq!(reconstructed.to_string(), uuid_string);
679
680        // Verify version 4
681        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        // A well-formed v4 / RFC 4122 input should be preserved verbatim.
696        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        // Input has version bits indicating v1 (0x10..): `from_bytes` must coerce to v4.
708        let mut bytes = [0u8; 16];
709        bytes[6] = 0x1a; // High nibble is version; 1 means v1
710        bytes[8] = 0x80; // Already RFC 4122
711        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        // Input has variant bits indicating non-RFC-4122 (0x00..): `from_bytes` must coerce.
720        let mut bytes = [0u8; 16];
721        bytes[6] = 0x40; // Already v4
722        bytes[8] = 0x00; // Non-RFC-4122 variant
723        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        // After normalization, byte 6 is 0x40 and byte 8 is 0x80, so the canonical representation
732        // is "00000000-0000-4000-8000-000000000000", still a valid v4 UUID.
733        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        // For inputs whose bits 6 and 8 are already v4/RFC-4122, `as_bytes` ∘ `from_bytes` is the
747        // identity.
748        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\"")] // Invalid format
756    #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] // v1 UUID (wrong version)
757    #[case("\"\"")] // Empty string
758    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        // Build from proptest-generated bytes for deterministic
765        // reproduction and shrinking on failure
766        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            // Invariant: Total length is always 36 characters + null terminator
791            prop_assert_eq!(s.len(), 36);
792            prop_assert_eq!(bytes[36], 0, "Missing null terminator at index 36");
793
794            // Invariant: Dash positions per RFC 4122
795            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            // Invariant: Version digit must be '4' (index 14)
801            prop_assert_eq!(&s[14..15], "4", "Version digit must be 4");
802
803            // Invariant: Variant bits must be RFC 4122 (index 19)
804            // Binary: 10xx -> Hex: 8, 9, a, b
805            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            // Fuzzing the parser with arbitrary strings
834            let _ = UUID4::from_str(&s);
835        }
836
837        #[rstest]
838        fn prop_from_bytes_always_yields_v4(bytes in any::<[u8; 16]>()) {
839            // Any 16-byte input must produce a UUID that passes both v4 and RFC 4122 checks,
840            // because `from_bytes` unconditionally normalizes the version and variant nibbles.
841            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            // `as_bytes` must reflect exactly the bits `from_bytes` produced: the input
850            // bytes after version/variant normalization.
851            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}