Skip to main content

deepslate_protocol/
types.rs

1//! Common types used across the Minecraft protocol.
2
3use bytes::{Buf, BufMut};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::varint;
8
9/// Maximum length of a Minecraft protocol string (32767 UTF-16 code units).
10const MAX_STRING_LENGTH: usize = 32767;
11
12/// Maximum number of properties allowed in a game profile.
13///
14/// Vanilla Minecraft profiles only have a handful of properties (typically
15/// just `textures`). We cap at 16 to match the protocol's practical limit
16/// and prevent a malicious peer from triggering a huge allocation via a
17/// crafted `VarInt` count.
18const MAX_PROPERTIES: usize = 16;
19
20/// Errors that can occur during protocol operations.
21#[derive(Debug, thiserror::Error)]
22pub enum ProtocolError {
23    /// `VarInt` exceeded the maximum of 5 bytes.
24    #[error("VarInt is too long (exceeded 5 bytes)")]
25    VarIntTooLong,
26
27    /// Buffer ran out of data unexpectedly.
28    #[error("unexpected end of data")]
29    UnexpectedEof,
30
31    /// String exceeded the maximum allowed length.
32    #[error("string too long: {length} > {max}")]
33    StringTooLong {
34        /// Actual length of the string.
35        length: usize,
36        /// Maximum allowed length.
37        max: usize,
38    },
39
40    /// String contained invalid UTF-8.
41    #[error("invalid UTF-8 in string")]
42    InvalidUtf8,
43
44    /// Invalid packet ID for the current state.
45    #[error("unknown packet ID {id:#04x} in state {state}")]
46    UnknownPacket {
47        /// The packet ID that was not recognized.
48        id: i32,
49        /// The current protocol state.
50        state: String,
51    },
52
53    /// A packet field had an invalid value.
54    #[error("invalid packet data: {0}")]
55    InvalidData(String),
56
57    /// Zlib decompression failed.
58    #[error("zlib decompression failed: {0}")]
59    DecompressionFailed(#[from] libdeflater::DecompressionError),
60
61    /// Zlib compression failed.
62    #[error("zlib compression failed: {0}")]
63    CompressionFailed(#[from] libdeflater::CompressionError),
64
65    /// Declared uncompressed size exceeds the protocol maximum.
66    #[error("uncompressed size {size} exceeds maximum {max}")]
67    UncompressedSizeTooLarge {
68        /// Declared size.
69        size: usize,
70        /// Maximum allowed.
71        max: usize,
72    },
73
74    /// Frame too large.
75    #[error("frame too large: {size} bytes (max {max})")]
76    FrameTooLarge {
77        /// Actual frame size.
78        size: usize,
79        /// Maximum allowed size.
80        max: usize,
81    },
82
83    /// Read buffer exceeded the maximum allowed size.
84    #[error("read buffer overflow: {size} bytes (max {max})")]
85    ReadBufferOverflow {
86        /// Actual buffer size.
87        size: usize,
88        /// Maximum allowed size.
89        max: usize,
90    },
91
92    /// Profile property count exceeded the allowed maximum.
93    #[error("property count too large: {count} (max {max})")]
94    PropertyCountExceeded {
95        /// Declared property count.
96        count: i32,
97        /// Maximum allowed count.
98        max: usize,
99    },
100
101    /// Byte array exceeded the maximum allowed length.
102    #[error("byte array too long: {length} > {max}")]
103    ByteArrayTooLong {
104        /// Actual declared length.
105        length: usize,
106        /// Maximum allowed length.
107        max: usize,
108    },
109}
110
111/// A player's game profile, as returned by the Mojang session server.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct GameProfile {
114    /// The player's UUID.
115    pub id: Uuid,
116    /// The player's username.
117    pub name: String,
118    /// Profile properties (e.g., skin textures).
119    pub properties: Vec<ProfileProperty>,
120}
121
122/// A single property in a game profile.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct ProfileProperty {
125    /// Property name (e.g., "textures").
126    pub name: String,
127    /// Base64-encoded property value.
128    pub value: String,
129    /// Optional base64-encoded signature.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub signature: Option<String>,
132}
133
134/// Read a Minecraft protocol string (VarInt-prefixed UTF-8).
135///
136/// # Errors
137///
138/// Returns an error if the string length is invalid or UTF-8 decoding fails.
139pub fn read_string(buf: &mut impl Buf) -> Result<String, ProtocolError> {
140    read_string_max(buf, MAX_STRING_LENGTH)
141}
142
143/// Read a Minecraft protocol string with a custom maximum length.
144///
145/// # Errors
146///
147/// Returns an error if the string length exceeds `max_len` or UTF-8 decoding fails.
148#[allow(clippy::cast_sign_loss)]
149pub fn read_string_max(buf: &mut impl Buf, max_len: usize) -> Result<String, ProtocolError> {
150    let length = varint::read_var_int(buf)? as usize;
151    if length > max_len * 4 {
152        return Err(ProtocolError::StringTooLong {
153            length,
154            max: max_len * 4,
155        });
156    }
157    if buf.remaining() < length {
158        return Err(ProtocolError::UnexpectedEof);
159    }
160
161    // Fast path: if the buffer is contiguous, we can validate UTF-8 in-place
162    if buf.chunk().len() >= length {
163        let s = std::str::from_utf8(&buf.chunk()[..length])
164            .map_err(|_| ProtocolError::InvalidUtf8)?
165            .to_owned();
166        buf.advance(length);
167        return Ok(s);
168    }
169
170    // Slow path: non-contiguous buffer requires copying
171    let mut data = vec![0u8; length];
172    buf.copy_to_slice(&mut data);
173    String::from_utf8(data).map_err(|_| ProtocolError::InvalidUtf8)
174}
175
176/// Write a Minecraft protocol string (VarInt-prefixed UTF-8).
177#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
178pub fn write_string(buf: &mut impl BufMut, value: &str) {
179    let bytes = value.as_bytes();
180    varint::write_var_int(buf, bytes.len() as i32);
181    buf.put_slice(bytes);
182}
183
184/// Read a UUID as two big-endian i64 values (Minecraft format).
185///
186/// # Errors
187///
188/// Returns `ProtocolError::UnexpectedEof` if not enough data.
189pub fn read_uuid(buf: &mut impl Buf) -> Result<Uuid, ProtocolError> {
190    if buf.remaining() < 16 {
191        return Err(ProtocolError::UnexpectedEof);
192    }
193    let most = buf.get_u64();
194    let least = buf.get_u64();
195    Ok(Uuid::from_u64_pair(most, least))
196}
197
198/// Write a UUID as two big-endian i64 values (Minecraft format).
199pub fn write_uuid(buf: &mut impl BufMut, uuid: Uuid) {
200    let (most, least) = uuid.as_u64_pair();
201    buf.put_u64(most);
202    buf.put_u64(least);
203}
204
205/// Write a game profile's properties array in the Minecraft protocol format.
206#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
207pub fn write_properties(buf: &mut impl BufMut, properties: &[ProfileProperty]) {
208    varint::write_var_int(buf, properties.len() as i32);
209    for prop in properties {
210        write_string(buf, &prop.name);
211        write_string(buf, &prop.value);
212        if let Some(sig) = &prop.signature {
213            buf.put_u8(1); // has signature
214            write_string(buf, sig);
215        } else {
216            buf.put_u8(0); // no signature
217        }
218    }
219}
220
221/// Read a game profile's properties array from the Minecraft protocol format.
222///
223/// # Errors
224///
225/// Returns `PropertyCountExceeded` if the declared count is negative or
226/// exceeds [`MAX_PROPERTIES`]. Returns other protocol errors if the data
227/// is malformed.
228pub fn read_properties(buf: &mut impl Buf) -> Result<Vec<ProfileProperty>, ProtocolError> {
229    let raw_count = varint::read_var_int(buf)?;
230    let count = usize::try_from(raw_count)
231        .ok()
232        .filter(|&n| n <= MAX_PROPERTIES)
233        .ok_or(ProtocolError::PropertyCountExceeded {
234            count: raw_count,
235            max: MAX_PROPERTIES,
236        })?;
237    let mut properties = Vec::with_capacity(count);
238    for _ in 0..count {
239        let name = read_string(buf)?;
240        let value = read_string(buf)?;
241        let has_signature = if buf.remaining() < 1 {
242            return Err(ProtocolError::UnexpectedEof);
243        } else {
244            buf.get_u8() != 0
245        };
246        let signature = if has_signature {
247            Some(read_string(buf)?)
248        } else {
249            None
250        };
251        properties.push(ProfileProperty {
252            name,
253            value,
254            signature,
255        });
256    }
257    Ok(properties)
258}
259
260/// Write an NBT string tag entry (tag type `0x08`) with the given name and value.
261///
262/// Wire format: `0x08` (`TAG_String`) + name (u16-prefixed) + value (u16-prefixed).
263#[allow(clippy::cast_possible_truncation)]
264fn write_nbt_string_tag(buf: &mut impl BufMut, name: &str, value: &str) {
265    buf.put_u8(0x08); // TAG_String
266    buf.put_u16(name.len() as u16);
267    buf.put_slice(name.as_bytes());
268    buf.put_u16(value.len() as u16);
269    buf.put_slice(value.as_bytes());
270}
271
272/// Write a Minecraft text component encoded as network NBT.
273///
274/// Since 1.20.2 (protocol 764), text components in PLAY and CONFIG state
275/// packets use NBT encoding instead of JSON strings. The network NBT format
276/// omits the root tag name.
277///
278/// This writes a minimal compound tag: `{"text": "...", "color": "..."}`.
279/// The `color` field is only included if `color` is `Some`.
280pub fn write_nbt_text_component(buf: &mut impl BufMut, text: &str, color: Option<&str>) {
281    buf.put_u8(0x0A); // TAG_Compound (root, no name in network NBT)
282    write_nbt_string_tag(buf, "text", text);
283    if let Some(color) = color {
284        write_nbt_string_tag(buf, "color", color);
285    }
286    buf.put_u8(0x00); // TAG_End
287}
288
289/// Format a UUID without dashes (Minecraft's "undashed" format).
290#[must_use]
291pub fn undashed_uuid(uuid: Uuid) -> String {
292    uuid.as_simple().to_string()
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use proptest::prelude::*;
299
300    proptest! {
301        #[test]
302        fn roundtrip_any_string(s in ".{0,1024}") {
303            let mut buf = Vec::new();
304            write_string(&mut buf, &s);
305            let result = read_string(&mut &buf[..]).unwrap();
306            prop_assert_eq!(result, s);
307        }
308
309        #[test]
310        fn roundtrip_any_uuid(u in any::<u128>()) {
311            let uuid = Uuid::from_u128(u);
312            let mut buf = Vec::new();
313            write_uuid(&mut buf, uuid);
314            let result = read_uuid(&mut &buf[..]).unwrap();
315            prop_assert_eq!(result, uuid);
316        }
317
318        #[test]
319        fn roundtrip_any_properties(
320            props in prop::collection::vec(
321                (
322                    ".{0,32}",
323                    ".{0,1024}",
324                    prop::option::weighted(0.5, ".{0,1024}")
325                ).prop_map(|(name, value, signature)| ProfileProperty { name, value, signature }),
326                0..4
327            )
328        ) {
329            let mut buf = Vec::new();
330            write_properties(&mut buf, &props);
331            let result = read_properties(&mut &buf[..]).unwrap();
332            prop_assert_eq!(result, props);
333        }
334    }
335
336    #[test]
337    fn test_string_roundtrip() {
338        let mut buf = Vec::new();
339        write_string(&mut buf, "Hello, Minecraft!");
340        let result = read_string(&mut &buf[..]).unwrap();
341        assert_eq!(result, "Hello, Minecraft!");
342    }
343
344    #[test]
345    fn test_string_empty() {
346        let mut buf = Vec::new();
347        write_string(&mut buf, "");
348        let result = read_string(&mut &buf[..]).unwrap();
349        assert_eq!(result, "");
350    }
351
352    #[test]
353    fn test_uuid_roundtrip() {
354        let uuid = Uuid::parse_str("069a79f4-44e9-4726-a5be-fca90e38aaf5").unwrap();
355        let mut buf = Vec::new();
356        write_uuid(&mut buf, uuid);
357        assert_eq!(buf.len(), 16);
358        let result = read_uuid(&mut &buf[..]).unwrap();
359        assert_eq!(result, uuid);
360    }
361
362    #[test]
363    fn test_properties_roundtrip() {
364        let props = vec![
365            ProfileProperty {
366                name: "textures".to_string(),
367                value: "base64data".to_string(),
368                signature: Some("sig".to_string()),
369            },
370            ProfileProperty {
371                name: "other".to_string(),
372                value: "val".to_string(),
373                signature: None,
374            },
375        ];
376        let mut buf = Vec::new();
377        write_properties(&mut buf, &props);
378        let result = read_properties(&mut &buf[..]).unwrap();
379        assert_eq!(result.len(), 2);
380        assert_eq!(result[0].name, "textures");
381        assert_eq!(result[0].signature.as_deref(), Some("sig"));
382        assert_eq!(result[1].signature, None);
383    }
384
385    #[test]
386    fn test_properties_rejects_huge_count() {
387        let mut buf = Vec::new();
388        varint::write_var_int(&mut buf, i32::MAX);
389        let err = read_properties(&mut &buf[..]).unwrap_err();
390        assert!(
391            matches!(err, ProtocolError::PropertyCountExceeded { count, max } if count == i32::MAX && max == MAX_PROPERTIES),
392            "expected PropertyCountExceeded, got {err:?}"
393        );
394    }
395
396    #[test]
397    fn test_properties_rejects_negative_count() {
398        let mut buf = Vec::new();
399        varint::write_var_int(&mut buf, -1);
400        let err = read_properties(&mut &buf[..]).unwrap_err();
401        assert!(
402            matches!(err, ProtocolError::PropertyCountExceeded { count, max } if count == -1 && max == MAX_PROPERTIES),
403            "expected PropertyCountExceeded, got {err:?}"
404        );
405    }
406
407    #[test]
408    fn test_properties_rejects_count_above_max() {
409        let mut buf = Vec::new();
410        // One above the limit
411        varint::write_var_int(&mut buf, i32::try_from(MAX_PROPERTIES + 1).unwrap());
412        let err = read_properties(&mut &buf[..]).unwrap_err();
413        assert!(
414            matches!(err, ProtocolError::PropertyCountExceeded { .. }),
415            "expected PropertyCountExceeded, got {err:?}"
416        );
417    }
418
419    #[test]
420    fn test_properties_accepts_empty() {
421        let mut buf = Vec::new();
422        varint::write_var_int(&mut buf, 0);
423        let result = read_properties(&mut &buf[..]).unwrap();
424        assert!(result.is_empty());
425    }
426
427    #[test]
428    fn test_properties_accepts_count_at_max() {
429        // Build a buffer with exactly MAX_PROPERTIES valid entries.
430        let props: Vec<ProfileProperty> = (0..MAX_PROPERTIES)
431            .map(|i| ProfileProperty {
432                name: format!("prop{i}"),
433                value: format!("val{i}"),
434                signature: None,
435            })
436            .collect();
437        let mut buf = Vec::new();
438        write_properties(&mut buf, &props);
439        let result = read_properties(&mut &buf[..]).unwrap();
440        assert_eq!(result.len(), MAX_PROPERTIES);
441    }
442
443    #[test]
444    fn test_nbt_text_component_simple() {
445        let mut buf = Vec::new();
446        write_nbt_text_component(&mut buf, "hello", None);
447        // TAG_Compound (0x0A), TAG_String (0x08), name "text" (4 bytes), value "hello" (5 bytes), TAG_End (0x00)
448        assert_eq!(buf[0], 0x0A); // compound
449        assert_eq!(buf[1], 0x08); // string tag
450        assert_eq!(&buf[2..4], &[0x00, 0x04]); // name length = 4
451        assert_eq!(&buf[4..8], b"text");
452        assert_eq!(&buf[8..10], &[0x00, 0x05]); // value length = 5
453        assert_eq!(&buf[10..15], b"hello");
454        assert_eq!(buf[15], 0x00); // end tag
455        assert_eq!(buf.len(), 16);
456    }
457
458    #[test]
459    fn test_nbt_text_component_with_color() {
460        let mut buf = Vec::new();
461        write_nbt_text_component(&mut buf, "hi", Some("yellow"));
462        assert_eq!(buf[0], 0x0A); // compound
463        // First entry: "text" = "hi"
464        assert_eq!(buf[1], 0x08);
465        assert_eq!(&buf[2..4], &[0x00, 0x04]);
466        assert_eq!(&buf[4..8], b"text");
467        assert_eq!(&buf[8..10], &[0x00, 0x02]);
468        assert_eq!(&buf[10..12], b"hi");
469        // Second entry: "color" = "yellow"
470        assert_eq!(buf[12], 0x08);
471        assert_eq!(&buf[13..15], &[0x00, 0x05]);
472        assert_eq!(&buf[15..20], b"color");
473        assert_eq!(&buf[20..22], &[0x00, 0x06]);
474        assert_eq!(&buf[22..28], b"yellow");
475        // End tag
476        assert_eq!(buf[28], 0x00);
477        assert_eq!(buf.len(), 29);
478    }
479}