Skip to main content

dolang_winterop/
guid.rs

1//! Globally unique identifiers (GUIDs).
2
3use std::{error, fmt, str::FromStr};
4
5use serde::{Deserialize, Serialize};
6
7/// A Windows globally unique identifier (GUID).
8///
9/// Mirrors the fields of the native `GUID` struct (`Data1`/`Data2`/`Data3`/
10/// `Data4`) rather than an opaque byte packet.
11///
12/// Parse and format it using the canonical hyphenated form:
13///
14/// ```
15/// use dolang_winterop::guid::Guid;
16///
17/// let guid: Guid = "00112233-4455-6677-8899-aabbccddeeff".parse()?;
18/// assert_eq!(guid.to_string(), "00112233-4455-6677-8899-aabbccddeeff");
19/// # Ok::<(), dolang_winterop::guid::Error>(())
20/// ```
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct Guid {
23    pub data1: u32,
24    pub data2: u16,
25    pub data3: u16,
26    pub data4: [u8; 8],
27}
28
29impl Guid {
30    /// Generates a random version 4 GUID.
31    pub fn new_v4() -> Self {
32        let mut bytes = [0u8; 16];
33        rand::fill(&mut bytes);
34        bytes[7] = (bytes[7] & 0x0f) | 0x40;
35        bytes[8] = (bytes[8] & 0x3f) | 0x80;
36        Self::from_bytes(&bytes).unwrap()
37    }
38
39    /// Parses the 16-byte in-memory layout used by Windows APIs.
40    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
41        let bytes: [u8; 16] = bytes.try_into().map_err(|_| Error::PacketLength)?;
42        Ok(Self {
43            data1: u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
44            data2: u16::from_le_bytes(bytes[4..6].try_into().unwrap()),
45            data3: u16::from_le_bytes(bytes[6..8].try_into().unwrap()),
46            data4: bytes[8..16].try_into().unwrap(),
47        })
48    }
49
50    /// Returns the native 16-byte Windows GUID representation.
51    pub const fn to_bytes(&self) -> [u8; 16] {
52        let mut bytes = [0u8; 16];
53        let d1 = self.data1.to_le_bytes();
54        let d2 = self.data2.to_le_bytes();
55        let d3 = self.data3.to_le_bytes();
56        bytes[0] = d1[0];
57        bytes[1] = d1[1];
58        bytes[2] = d1[2];
59        bytes[3] = d1[3];
60        bytes[4] = d2[0];
61        bytes[5] = d2[1];
62        bytes[6] = d3[0];
63        bytes[7] = d3[1];
64        let mut i = 0;
65        while i < 8 {
66            bytes[8 + i] = self.data4[i];
67            i += 1;
68        }
69        bytes
70    }
71}
72
73impl TryFrom<&[u8]> for Guid {
74    type Error = Error;
75
76    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
77        Self::from_bytes(value)
78    }
79}
80
81impl fmt::Display for Guid {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(
84            f,
85            "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-",
86            self.data1, self.data2, self.data3, self.data4[0], self.data4[1]
87        )?;
88        for byte in &self.data4[2..] {
89            write!(f, "{byte:02x}")?;
90        }
91        Ok(())
92    }
93}
94
95impl FromStr for Guid {
96    type Err = Error;
97
98    fn from_str(value: &str) -> Result<Self, Self::Err> {
99        if value.len() != 36
100            || value.as_bytes()[8] != b'-'
101            || value.as_bytes()[13] != b'-'
102            || value.as_bytes()[18] != b'-'
103            || value.as_bytes()[23] != b'-'
104        {
105            return Err(Error::StringSyntax);
106        }
107        let parse = |start, end| {
108            u64::from_str_radix(&value[start..end], 16).map_err(|_| Error::StringSyntax)
109        };
110        let data1 = u32::try_from(parse(0, 8)?).unwrap();
111        let data2 = u16::try_from(parse(9, 13)?).unwrap();
112        let data3 = u16::try_from(parse(14, 18)?).unwrap();
113        let data4a = u16::try_from(parse(19, 23)?).unwrap();
114        let data4b = parse(24, 36)?;
115        let mut data4 = [0u8; 8];
116        data4[0..2].copy_from_slice(&data4a.to_be_bytes());
117        data4[2..8].copy_from_slice(&data4b.to_be_bytes()[2..]);
118        Ok(Self {
119            data1,
120            data2,
121            data3,
122            data4,
123        })
124    }
125}
126
127/// Error returned when parsing a GUID.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum Error {
130    /// A binary packet was not exactly 16 bytes long.
131    PacketLength,
132    /// Text was not a canonical hyphenated GUID.
133    StringSyntax,
134}
135
136impl fmt::Display for Error {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match self {
139            Self::PacketLength => f.write_str("GUID packet must contain exactly 16 bytes"),
140            Self::StringSyntax => f.write_str("invalid canonical GUID string"),
141        }
142    }
143}
144
145impl error::Error for Error {}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn string_and_native_packet_round_trip() {
153        let guid: Guid = "00112233-4455-6677-8899-aabbccddeeff".parse().unwrap();
154        assert_eq!(
155            guid.to_bytes(),
156            [
157                0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
158                0xee, 0xff,
159            ]
160        );
161        assert_eq!(guid.to_string(), "00112233-4455-6677-8899-aabbccddeeff");
162        assert_eq!(
163            "00112233-4455-6677-8899-AABBCCDDEEFF"
164                .parse::<Guid>()
165                .unwrap(),
166            guid
167        );
168        assert_eq!(Guid::from_bytes(&guid.to_bytes()).unwrap(), guid);
169    }
170
171    #[test]
172    fn rejects_noncanonical_text_and_packet_lengths() {
173        assert!(
174            "{00112233-4455-6677-8899-aabbccddeeff}"
175                .parse::<Guid>()
176                .is_err()
177        );
178        assert!("00112233445566778899aabbccddeeff".parse::<Guid>().is_err());
179        assert!(Guid::from_bytes(&[0; 15]).is_err());
180    }
181
182    #[test]
183    fn serde_round_trip() {
184        let guid: Guid = "00112233-4455-6677-8899-aabbccddeeff".parse().unwrap();
185        let encoded = postcard::to_stdvec(&guid).unwrap();
186        assert_eq!(postcard::from_bytes::<Guid>(&encoded).unwrap(), guid);
187    }
188
189    #[test]
190    fn generated_guid_is_version_4_with_rfc_variant() {
191        let guid = Guid::new_v4();
192        assert_eq!(guid.data3 >> 12, 4);
193        assert_eq!(guid.data4[0] >> 6, 2);
194    }
195}