Skip to main content

ferroui_remote_protocol/
guid.rs

1//! A globally unique identifier, with the text forms and the byte layout of
2//! the `System.Guid` the protocol is written against.
3//!
4//! The byte layout is part of the wire format: the header of every message
5//! carries the sixteen bytes of [`Guid::to_byte_array`], in which the first
6//! three groups of the text form are little-endian and the last two are in
7//! the order of the text.
8
9use std::fmt;
10use std::str::FromStr;
11
12use crate::error::Error;
13
14/// A 128-bit identifier.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub struct Guid {
17    a: u32,
18    b: u16,
19    c: u16,
20    d: [u8; 8],
21}
22
23const fn hex_digit(b: u8) -> Option<u8> {
24    match b {
25        b'0'..=b'9' => Some(b - b'0'),
26        b'a'..=b'f' => Some(b - b'a' + 10),
27        b'A'..=b'F' => Some(b - b'A' + 10),
28        _ => None,
29    }
30}
31
32impl Guid {
33    /// The identifier whose bits are all zero.
34    pub const EMPTY: Guid = Guid { a: 0, b: 0, c: 0, d: [0; 8] };
35
36    /// The identifier with the given groups.
37    pub const fn new(a: u32, b: u16, c: u16, d: [u8; 8]) -> Guid {
38        Guid { a, b, c, d }
39    }
40
41    /// Parses the forms `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` and the same
42    /// thirty-two digits without hyphens, either of them optionally in braces
43    /// or parentheses. The digits may be of either case.
44    const fn try_parse_bytes(s: &[u8]) -> Option<Guid> {
45        let mut start = 0;
46        let mut end = s.len();
47        if end >= 2 && ((s[0] == b'{' && s[end - 1] == b'}') || (s[0] == b'(' && s[end - 1] == b')')) {
48            start = 1;
49            end -= 1;
50        }
51        let hyphens = match end - start {
52            36 => true,
53            32 => false,
54            _ => return None,
55        };
56        let mut digits = [0u8; 32];
57        let mut count = 0;
58        let mut i = start;
59        while i < end {
60            let position = i - start;
61            if hyphens && (position == 8 || position == 13 || position == 18 || position == 23) {
62                if s[i] != b'-' {
63                    return None;
64                }
65            } else {
66                match hex_digit(s[i]) {
67                    Some(digit) => {
68                        if count == 32 {
69                            return None;
70                        }
71                        digits[count] = digit;
72                        count += 1;
73                    }
74                    None => return None,
75                }
76            }
77            i += 1;
78        }
79        if count != 32 {
80            return None;
81        }
82        let mut bytes = [0u8; 16];
83        let mut k = 0;
84        while k < 16 {
85            bytes[k] = (digits[2 * k] << 4) | digits[2 * k + 1];
86            k += 1;
87        }
88        Some(Guid {
89            a: u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
90            b: u16::from_be_bytes([bytes[4], bytes[5]]),
91            c: u16::from_be_bytes([bytes[6], bytes[7]]),
92            d: [bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]],
93        })
94    }
95
96    /// `Guid.Parse` for a constant: a text that is not an identifier fails
97    /// the build.
98    pub const fn parse_const(text: &str) -> Guid {
99        match Guid::try_parse_bytes(text.as_bytes()) {
100            Some(guid) => guid,
101            None => panic!("Unrecognized Guid format."),
102        }
103    }
104
105    /// `Guid.Parse`: a `FormatException` is [`Error::Format`].
106    pub fn parse(text: &str) -> Result<Guid, Error> {
107        Guid::try_parse_bytes(text.trim().as_bytes())
108            .ok_or_else(|| Error::Format("Unrecognized Guid format.".to_string()))
109    }
110
111    /// `new Guid(byte[])`: the layout of [`Guid::to_byte_array`]. Anything
112    /// but sixteen bytes is an `ArgumentException`.
113    pub fn from_byte_array(bytes: &[u8]) -> Result<Guid, Error> {
114        if bytes.len() != 16 {
115            return Err(Error::Argument("Byte array for Guid must be exactly 16 bytes long.".to_string()));
116        }
117        let mut d = [0u8; 8];
118        d.copy_from_slice(&bytes[8..16]);
119        Ok(Guid {
120            a: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
121            b: u16::from_le_bytes([bytes[4], bytes[5]]),
122            c: u16::from_le_bytes([bytes[6], bytes[7]]),
123            d,
124        })
125    }
126
127    /// `Guid.ToByteArray`: the first three groups little-endian, then the
128    /// last eight bytes in the order of the text.
129    pub fn to_byte_array(&self) -> [u8; 16] {
130        let mut bytes = [0u8; 16];
131        bytes[0..4].copy_from_slice(&self.a.to_le_bytes());
132        bytes[4..6].copy_from_slice(&self.b.to_le_bytes());
133        bytes[6..8].copy_from_slice(&self.c.to_le_bytes());
134        bytes[8..16].copy_from_slice(&self.d);
135        bytes
136    }
137}
138
139/// `Guid.ToString()`: the hyphenated form in lower case.
140impl fmt::Display for Guid {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(
143            f,
144            "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
145            self.a,
146            self.b,
147            self.c,
148            self.d[0],
149            self.d[1],
150            self.d[2],
151            self.d[3],
152            self.d[4],
153            self.d[5],
154            self.d[6],
155            self.d[7]
156        )
157    }
158}
159
160impl FromStr for Guid {
161    type Err = Error;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        Guid::parse(s)
165    }
166}
167
168// Tests of the port: the upstream type is the one of the runtime library.
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn parses_and_formats_the_hyphenated_form() {
175        let guid = Guid::parse("6E3C5310-E2B1-4C3D-8688-01183AA48C5B").unwrap();
176        assert_eq!(guid.to_string(), "6e3c5310-e2b1-4c3d-8688-01183aa48c5b");
177        assert_eq!(guid, Guid::parse_const("6e3c5310-e2b1-4c3d-8688-01183aa48c5b"));
178        assert_eq!(guid, Guid::parse("{6E3C5310-E2B1-4C3D-8688-01183AA48C5B}").unwrap());
179        assert_eq!(guid, Guid::parse("6E3C5310E2B14C3D868801183AA48C5B").unwrap());
180        assert_eq!(guid, "6E3C5310-E2B1-4C3D-8688-01183AA48C5B".parse::<Guid>().unwrap());
181    }
182
183    #[test]
184    fn byte_array_has_the_first_three_groups_little_endian() {
185        let guid = Guid::parse_const("6E3C5310-E2B1-4C3D-8688-01183AA48C5B");
186        let bytes = guid.to_byte_array();
187        assert_eq!(
188            bytes,
189            [0x10, 0x53, 0x3C, 0x6E, 0xB1, 0xE2, 0x3D, 0x4C, 0x86, 0x88, 0x01, 0x18, 0x3A, 0xA4, 0x8C, 0x5B]
190        );
191        assert_eq!(Guid::from_byte_array(&bytes).unwrap(), guid);
192    }
193
194    #[test]
195    fn rejects_what_is_not_an_identifier() {
196        assert!(matches!(Guid::parse(""), Err(Error::Format(_))));
197        assert!(matches!(Guid::parse("6E3C5310-E2B1-4C3D-8688-01183AA48C5"), Err(Error::Format(_))));
198        assert!(matches!(Guid::parse("6E3C5310+E2B1-4C3D-8688-01183AA48C5B"), Err(Error::Format(_))));
199        assert!(matches!(Guid::parse("6E3C5310-E2B1-4C3D-8688-01183AA48C5G"), Err(Error::Format(_))));
200        assert!(matches!(Guid::from_byte_array(&[0; 15]), Err(Error::Argument(_))));
201    }
202}