Skip to main content

keramics_types/
uuid.rs

1/* Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may
5 * obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software
8 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 * License for the specific language governing permissions and limitations
11 * under the License.
12 */
13
14use std::fmt;
15
16use super::errors::ParseError;
17use super::{bytes_to_u16_be, bytes_to_u16_le, bytes_to_u32_be, bytes_to_u32_le};
18
19/// Universally unique identifier (UUID).
20#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
21pub struct Uuid {
22    pub part1: u32,
23    pub part2: u16,
24    pub part3: u16,
25    pub part4: u16,
26    pub part5: u64,
27}
28
29impl Uuid {
30    /// Creates a new UUID.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Reads a big-endian UUID from a byte sequence.
36    pub fn from_be_bytes(data: &[u8]) -> Self {
37        let part5_upper: u16 = bytes_to_u16_be!(data, 10);
38        let part5_lower: u32 = bytes_to_u32_be!(data, 12);
39        Self {
40            part1: bytes_to_u32_be!(data, 0),
41            part2: bytes_to_u16_be!(data, 4),
42            part3: bytes_to_u16_be!(data, 6),
43            part4: bytes_to_u16_be!(data, 8),
44            part5: ((part5_upper as u64) << 32) | (part5_lower as u64),
45        }
46    }
47
48    /// Reads a little-endian UUID from a byte sequence.
49    pub fn from_le_bytes(data: &[u8]) -> Self {
50        let part5_upper: u16 = bytes_to_u16_be!(data, 10);
51        let part5_lower: u32 = bytes_to_u32_be!(data, 12);
52        Self {
53            part1: bytes_to_u32_le!(data, 0),
54            part2: bytes_to_u16_le!(data, 4),
55            part3: bytes_to_u16_le!(data, 6),
56            part4: bytes_to_u16_be!(data, 8),
57            part5: ((part5_upper as u64) << 32) | (part5_lower as u64),
58        }
59    }
60
61    /// Determines if the UUID is the Max (or Omni) UUID (ffffffff-ffff-ffff-ffff-ffffffffffff)
62    pub fn is_max(&self) -> bool {
63        self.part1 == 0xffffffff
64            && self.part2 == 0xffff
65            && self.part3 == 0xffff
66            && self.part4 == 0xffff
67            && self.part5 == 0xffffffffffff
68    }
69
70    /// Determines if the UUID is the Nil UUID (00000000-0000-0000-0000-000000000000)
71    pub fn is_nil(&self) -> bool {
72        self.part1 == 0 && self.part2 == 0 && self.part3 == 0 && self.part4 == 0 && self.part5 == 0
73    }
74
75    /// Reads an UUID from a string.
76    pub fn from_string(&mut self, mut string: &str) -> Result<(), ParseError> {
77        let mut string_length: usize = string.len();
78
79        if string.starts_with("{") && string.ends_with("}") {
80            string = &string[1..string_length - 1];
81            string_length -= 2;
82        }
83        if string_length != 36 {
84            return Err(ParseError::new(format!("Unsupported string length")));
85        }
86        if &string[8..9] != "-"
87            || &string[13..14] != "-"
88            || &string[18..19] != "-"
89            || &string[23..24] != "-"
90        {
91            return Err(ParseError::new(format!("Unsupported string")));
92        }
93        self.part1 = match u32::from_str_radix(&string[0..8], 16) {
94            Ok(value) => value,
95            Err(_) => {
96                return Err(ParseError::new(format!(
97                    "Unable to parse part1: {}",
98                    &string[0..8]
99                )));
100            }
101        };
102        self.part2 = match u16::from_str_radix(&string[9..13], 16) {
103            Ok(value) => value,
104            Err(_) => {
105                return Err(ParseError::new(format!(
106                    "Unable to parse part2: {}",
107                    &string[9..13]
108                )));
109            }
110        };
111        self.part3 = match u16::from_str_radix(&string[14..18], 16) {
112            Ok(value) => value,
113            Err(_) => {
114                return Err(ParseError::new(format!(
115                    "Unable to parse part3: {}",
116                    &string[14..18]
117                )));
118            }
119        };
120        self.part4 = match u16::from_str_radix(&string[19..23], 16) {
121            Ok(value) => value,
122            Err(_) => {
123                return Err(ParseError::new(format!(
124                    "Unable to parse part4: {}",
125                    &string[19..24]
126                )));
127            }
128        };
129        self.part5 = match u64::from_str_radix(&string[24..36], 16) {
130            Ok(value) => value,
131            Err(_) => {
132                return Err(ParseError::new(format!(
133                    "Unable to parse part5: {}",
134                    &string[24..36]
135                )));
136            }
137        };
138        Ok(())
139    }
140
141    /// Retrieves the string representation of an UUID.
142    pub fn to_string(&self) -> String {
143        format!(
144            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
145            self.part1, self.part2, self.part3, self.part4, self.part5,
146        )
147    }
148}
149
150impl fmt::Display for Uuid {
151    /// Formats the UUID for display.
152    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
153        write!(formatter, "{}", self.to_string())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_from_be_bytes() {
163        let test_data: [u8; 16] = [
164            0xb6, 0x1f, 0x53, 0xca, 0xa7, 0x86, 0x45, 0x28, 0x90, 0xe2, 0x55, 0xba, 0x79, 0x1a,
165            0x1c, 0x4c,
166        ];
167
168        let uuid: Uuid = Uuid::from_be_bytes(&test_data);
169        assert_eq!(uuid.part1, 0xb61f53ca);
170        assert_eq!(uuid.part2, 0xa786);
171        assert_eq!(uuid.part3, 0x4528);
172        assert_eq!(uuid.part4, 0x90e2);
173        assert_eq!(uuid.part5, 0x55ba791a1c4c);
174    }
175
176    #[test]
177    fn test_from_le_bytes() {
178        let test_data: [u8; 16] = [
179            0xca, 0x53, 0x1f, 0xb6, 0x86, 0xa7, 0x28, 0x45, 0x90, 0xe2, 0x55, 0xba, 0x79, 0x1a,
180            0x1c, 0x4c,
181        ];
182
183        let uuid: Uuid = Uuid::from_le_bytes(&test_data);
184        assert_eq!(uuid.part1, 0xb61f53ca);
185        assert_eq!(uuid.part2, 0xa786);
186        assert_eq!(uuid.part3, 0x4528);
187        assert_eq!(uuid.part4, 0x90e2);
188        assert_eq!(uuid.part5, 0x55ba791a1c4c);
189    }
190
191    #[test]
192    fn test_is_max() {
193        let test_data: [u8; 16] = [
194            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
195            0xff, 0xff,
196        ];
197
198        let uuid: Uuid = Uuid::from_le_bytes(&test_data);
199        assert_eq!(uuid.is_max(), true);
200    }
201
202    #[test]
203    fn test_is_not_max() {
204        let test_data: [u8; 16] = [
205            0xca, 0x53, 0x1f, 0xb6, 0x86, 0xa7, 0x28, 0x45, 0x90, 0xe2, 0x55, 0xba, 0x79, 0x1a,
206            0x1c, 0x4c,
207        ];
208
209        let uuid: Uuid = Uuid::from_le_bytes(&test_data);
210        assert_eq!(uuid.is_max(), false);
211    }
212
213    #[test]
214    fn test_is_nil() {
215        let uuid: Uuid = Uuid::new();
216        assert_eq!(uuid.is_nil(), true);
217    }
218
219    #[test]
220    fn test_is_not_nil() {
221        let test_data: [u8; 16] = [
222            0xca, 0x53, 0x1f, 0xb6, 0x86, 0xa7, 0x28, 0x45, 0x90, 0xe2, 0x55, 0xba, 0x79, 0x1a,
223            0x1c, 0x4c,
224        ];
225
226        let uuid: Uuid = Uuid::from_le_bytes(&test_data);
227        assert_eq!(uuid.is_nil(), false);
228    }
229
230    #[test]
231    fn test_from_string() -> Result<(), ParseError> {
232        let mut uuid: Uuid = Uuid::new();
233        uuid.from_string("{b61f53ca-a786-4528-90e2-55ba791a1c4c}")?;
234
235        assert_eq!(uuid.part1, 0xb61f53ca);
236        assert_eq!(uuid.part2, 0xa786);
237        assert_eq!(uuid.part3, 0x4528);
238        assert_eq!(uuid.part4, 0x90e2);
239        assert_eq!(uuid.part5, 0x55ba791a1c4c);
240
241        Ok(())
242    }
243
244    #[test]
245    fn test_to_string() {
246        let test_data: [u8; 16] = [
247            0xca, 0x53, 0x1f, 0xb6, 0x86, 0xa7, 0x28, 0x45, 0x90, 0xe2, 0x55, 0xba, 0x79, 0x1a,
248            0x1c, 0x4c,
249        ];
250
251        let uuid: Uuid = Uuid::from_le_bytes(&test_data);
252        let uuid_string: String = uuid.to_string();
253        assert_eq!(uuid_string, "b61f53ca-a786-4528-90e2-55ba791a1c4c");
254    }
255}