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