Skip to main content

basalt_types/
uuid.rs

1use std::fmt;
2
3use crate::{Decode, Encode, EncodedSize, Result};
4
5/// A 128-bit universally unique identifier used throughout the Minecraft protocol.
6///
7/// UUIDs identify players, entities, and various protocol objects. They are
8/// encoded as two consecutive big-endian 64-bit integers (most significant
9/// bits first, then least significant bits), occupying exactly 16 bytes on
10/// the wire. The Minecraft protocol uses UUIDs in login packets (player UUID),
11/// entity spawn packets, player info, and boss bar management.
12///
13/// The standard display format is `8-4-4-4-12` lowercase hex with dashes
14/// (e.g., `550e8400-e29b-41d4-a716-446655440000`).
15#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
16pub struct Uuid {
17    /// Most significant 64 bits of the UUID.
18    pub most: u64,
19    /// Least significant 64 bits of the UUID.
20    pub least: u64,
21}
22
23impl Uuid {
24    /// Creates a new UUID from its most and least significant 64-bit halves.
25    pub fn new(most: u64, least: u64) -> Self {
26        Self { most, least }
27    }
28
29    /// Creates a UUID from a 16-byte array in big-endian order.
30    ///
31    /// The first 8 bytes form the most significant bits, the last 8 form
32    /// the least significant bits.
33    pub fn from_bytes(bytes: [u8; 16]) -> Self {
34        let most = u64::from_be_bytes(bytes[..8].try_into().unwrap());
35        let least = u64::from_be_bytes(bytes[8..].try_into().unwrap());
36        Self { most, least }
37    }
38
39    /// Converts the UUID to a 16-byte array in big-endian order.
40    pub fn to_bytes(self) -> [u8; 16] {
41        let mut bytes = [0u8; 16];
42        bytes[..8].copy_from_slice(&self.most.to_be_bytes());
43        bytes[8..].copy_from_slice(&self.least.to_be_bytes());
44        bytes
45    }
46}
47
48/// Encodes a UUID as two consecutive big-endian u64 values (16 bytes total).
49///
50/// The most significant 64 bits are written first, followed by the least
51/// significant 64 bits. This matches the Minecraft protocol wire format
52/// for all UUID fields.
53impl Encode for Uuid {
54    /// Writes the UUID as 16 big-endian bytes (most significant first).
55    fn encode(&self, buf: &mut Vec<u8>) -> Result<()> {
56        self.most.encode(buf)?;
57        self.least.encode(buf)
58    }
59}
60
61/// Decodes a UUID from two consecutive big-endian u64 values (16 bytes).
62///
63/// Reads the most significant 64 bits first, then the least significant
64/// 64 bits. Fails if fewer than 16 bytes remain in the buffer.
65impl Decode for Uuid {
66    /// Reads 16 big-endian bytes and reconstructs the UUID.
67    ///
68    /// Fails with `Error::BufferUnderflow` if fewer than 16 bytes remain.
69    fn decode(buf: &mut &[u8]) -> Result<Self> {
70        let most = u64::decode(buf)?;
71        let least = u64::decode(buf)?;
72        Ok(Self { most, least })
73    }
74}
75
76/// A UUID always occupies exactly 16 bytes on the wire.
77impl EncodedSize for Uuid {
78    fn encoded_size(&self) -> usize {
79        16
80    }
81}
82
83/// Displays the UUID in the standard `8-4-4-4-12` lowercase hex format.
84///
85/// This matches the format used by Mojang's API and the Minecraft client
86/// (e.g., `550e8400-e29b-41d4-a716-446655440000`).
87impl fmt::Display for Uuid {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        let bytes = self.to_bytes();
90        write!(
91            f,
92            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
93            bytes[0],
94            bytes[1],
95            bytes[2],
96            bytes[3],
97            bytes[4],
98            bytes[5],
99            bytes[6],
100            bytes[7],
101            bytes[8],
102            bytes[9],
103            bytes[10],
104            bytes[11],
105            bytes[12],
106            bytes[13],
107            bytes[14],
108            bytes[15],
109        )
110    }
111}
112
113/// Debug output uses the same `8-4-4-4-12` hex format as Display for readability.
114impl fmt::Debug for Uuid {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "Uuid({})", self)
117    }
118}
119
120/// Converts a 16-byte array into a UUID.
121impl From<[u8; 16]> for Uuid {
122    fn from(bytes: [u8; 16]) -> Self {
123        Self::from_bytes(bytes)
124    }
125}
126
127/// Converts a UUID into a 16-byte array in big-endian order.
128impl From<Uuid> for [u8; 16] {
129    fn from(uuid: Uuid) -> Self {
130        uuid.to_bytes()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    fn roundtrip(most: u64, least: u64) {
139        let uuid = Uuid::new(most, least);
140        let mut buf = Vec::with_capacity(uuid.encoded_size());
141        uuid.encode(&mut buf).unwrap();
142        assert_eq!(buf.len(), 16);
143
144        let mut cursor = buf.as_slice();
145        let decoded = Uuid::decode(&mut cursor).unwrap();
146        assert!(cursor.is_empty());
147        assert_eq!(decoded, uuid);
148    }
149
150    #[test]
151    fn zero_uuid() {
152        roundtrip(0, 0);
153    }
154
155    #[test]
156    fn max_uuid() {
157        roundtrip(u64::MAX, u64::MAX);
158    }
159
160    #[test]
161    fn known_uuid() {
162        // Notch's UUID: 069a79f4-44e9-4726-a5be-fca90e38aaf5
163        let uuid = Uuid::new(0x069a79f444e94726, 0xa5befca90e38aaf5);
164        roundtrip(uuid.most, uuid.least);
165    }
166
167    #[test]
168    fn display_format() {
169        let uuid = Uuid::new(0x550e8400e29b41d4, 0xa716446655440000);
170        assert_eq!(uuid.to_string(), "550e8400-e29b-41d4-a716-446655440000");
171    }
172
173    #[test]
174    fn debug_format() {
175        let uuid = Uuid::new(0x550e8400e29b41d4, 0xa716446655440000);
176        assert_eq!(
177            format!("{:?}", uuid),
178            "Uuid(550e8400-e29b-41d4-a716-446655440000)"
179        );
180    }
181
182    #[test]
183    fn from_bytes() {
184        let bytes = [
185            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
186            0x00, 0x00,
187        ];
188        let uuid = Uuid::from_bytes(bytes);
189        assert_eq!(uuid.most, 0x550e8400e29b41d4);
190        assert_eq!(uuid.least, 0xa716446655440000);
191    }
192
193    #[test]
194    fn to_bytes() {
195        let uuid = Uuid::new(0x550e8400e29b41d4, 0xa716446655440000);
196        let bytes = uuid.to_bytes();
197        assert_eq!(
198            bytes,
199            [
200                0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
201                0x00, 0x00
202            ]
203        );
204    }
205
206    #[test]
207    fn bytes_roundtrip() {
208        let original = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
209        let uuid = Uuid::from(original);
210        let back: [u8; 16] = uuid.into();
211        assert_eq!(back, original);
212    }
213
214    #[test]
215    fn encoded_size_is_16() {
216        assert_eq!(Uuid::new(0, 0).encoded_size(), 16);
217    }
218
219    #[test]
220    fn underflow() {
221        let mut cursor: &[u8] = &[0x01; 15];
222        assert!(Uuid::decode(&mut cursor).is_err());
223    }
224
225    mod proptests {
226        use super::*;
227        use proptest::prelude::*;
228
229        proptest! {
230            #[test]
231            fn uuid_roundtrip(most: u64, least: u64) {
232                roundtrip(most, least);
233            }
234        }
235    }
236}