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    /// Compression error.
50    #[error("compression error: {0}")]
51    CompressionError(String),
52
53    /// Frame too large.
54    #[error("frame too large: {size} bytes (max {max})")]
55    FrameTooLarge {
56        /// Actual frame size.
57        size: usize,
58        /// Maximum allowed size.
59        max: usize,
60    },
61}
62
63/// A player's game profile, as returned by the Mojang session server.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct GameProfile {
66    /// The player's UUID.
67    pub id: Uuid,
68    /// The player's username.
69    pub name: String,
70    /// Profile properties (e.g., skin textures).
71    pub properties: Vec<ProfileProperty>,
72}
73
74/// A single property in a game profile.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ProfileProperty {
77    /// Property name (e.g., "textures").
78    pub name: String,
79    /// Base64-encoded property value.
80    pub value: String,
81    /// Optional base64-encoded signature.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub signature: Option<String>,
84}
85
86/// Read a Minecraft protocol string (VarInt-prefixed UTF-8).
87///
88/// # Errors
89///
90/// Returns an error if the string length is invalid or UTF-8 decoding fails.
91pub fn read_string(buf: &mut impl Buf) -> Result<String, ProtocolError> {
92    read_string_max(buf, MAX_STRING_LENGTH)
93}
94
95/// Read a Minecraft protocol string with a custom maximum length.
96///
97/// # Errors
98///
99/// Returns an error if the string length exceeds `max_len` or UTF-8 decoding fails.
100#[allow(clippy::cast_sign_loss)]
101pub fn read_string_max(buf: &mut impl Buf, max_len: usize) -> Result<String, ProtocolError> {
102    let length = varint::read_var_int(buf)? as usize;
103    if length > max_len * 4 {
104        return Err(ProtocolError::StringTooLong {
105            length,
106            max: max_len * 4,
107        });
108    }
109    if buf.remaining() < length {
110        return Err(ProtocolError::UnexpectedEof);
111    }
112    let mut data = vec![0u8; length];
113    buf.copy_to_slice(&mut data);
114    String::from_utf8(data).map_err(|_| ProtocolError::InvalidUtf8)
115}
116
117/// Write a Minecraft protocol string (VarInt-prefixed UTF-8).
118#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
119pub fn write_string(buf: &mut impl BufMut, value: &str) {
120    let bytes = value.as_bytes();
121    varint::write_var_int(buf, bytes.len() as i32);
122    buf.put_slice(bytes);
123}
124
125/// Read a UUID as two big-endian i64 values (Minecraft format).
126///
127/// # Errors
128///
129/// Returns `ProtocolError::UnexpectedEof` if not enough data.
130pub fn read_uuid(buf: &mut impl Buf) -> Result<Uuid, ProtocolError> {
131    if buf.remaining() < 16 {
132        return Err(ProtocolError::UnexpectedEof);
133    }
134    let most = buf.get_u64();
135    let least = buf.get_u64();
136    Ok(Uuid::from_u64_pair(most, least))
137}
138
139/// Write a UUID as two big-endian i64 values (Minecraft format).
140pub fn write_uuid(buf: &mut impl BufMut, uuid: Uuid) {
141    let (most, least) = uuid.as_u64_pair();
142    buf.put_u64(most);
143    buf.put_u64(least);
144}
145
146/// Write a game profile's properties array in the Minecraft protocol format.
147#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
148pub fn write_properties(buf: &mut impl BufMut, properties: &[ProfileProperty]) {
149    varint::write_var_int(buf, properties.len() as i32);
150    for prop in properties {
151        write_string(buf, &prop.name);
152        write_string(buf, &prop.value);
153        if let Some(sig) = &prop.signature {
154            buf.put_u8(1); // has signature
155            write_string(buf, sig);
156        } else {
157            buf.put_u8(0); // no signature
158        }
159    }
160}
161
162/// Read a game profile's properties array from the Minecraft protocol format.
163///
164/// # Errors
165///
166/// Returns an error if the data is malformed.
167#[allow(clippy::cast_sign_loss)]
168pub fn read_properties(buf: &mut impl Buf) -> Result<Vec<ProfileProperty>, ProtocolError> {
169    let count = varint::read_var_int(buf)? as usize;
170    let mut properties = Vec::with_capacity(count);
171    for _ in 0..count {
172        let name = read_string(buf)?;
173        let value = read_string(buf)?;
174        let has_signature = if buf.remaining() < 1 {
175            return Err(ProtocolError::UnexpectedEof);
176        } else {
177            buf.get_u8() != 0
178        };
179        let signature = if has_signature {
180            Some(read_string(buf)?)
181        } else {
182            None
183        };
184        properties.push(ProfileProperty {
185            name,
186            value,
187            signature,
188        });
189    }
190    Ok(properties)
191}
192
193/// Write an NBT string tag entry (tag type `0x08`) with the given name and value.
194///
195/// Wire format: `0x08` (`TAG_String`) + name (u16-prefixed) + value (u16-prefixed).
196#[allow(clippy::cast_possible_truncation)]
197fn write_nbt_string_tag(buf: &mut impl BufMut, name: &str, value: &str) {
198    buf.put_u8(0x08); // TAG_String
199    buf.put_u16(name.len() as u16);
200    buf.put_slice(name.as_bytes());
201    buf.put_u16(value.len() as u16);
202    buf.put_slice(value.as_bytes());
203}
204
205/// Write a Minecraft text component encoded as network NBT.
206///
207/// Since 1.20.2 (protocol 764), text components in PLAY and CONFIG state
208/// packets use NBT encoding instead of JSON strings. The network NBT format
209/// omits the root tag name.
210///
211/// This writes a minimal compound tag: `{"text": "...", "color": "..."}`.
212/// The `color` field is only included if `color` is `Some`.
213pub fn write_nbt_text_component(buf: &mut impl BufMut, text: &str, color: Option<&str>) {
214    buf.put_u8(0x0A); // TAG_Compound (root, no name in network NBT)
215    write_nbt_string_tag(buf, "text", text);
216    if let Some(color) = color {
217        write_nbt_string_tag(buf, "color", color);
218    }
219    buf.put_u8(0x00); // TAG_End
220}
221
222/// Format a UUID without dashes (Minecraft's "undashed" format).
223#[must_use]
224pub fn undashed_uuid(uuid: Uuid) -> String {
225    uuid.as_simple().to_string()
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_string_roundtrip() {
234        let mut buf = Vec::new();
235        write_string(&mut buf, "Hello, Minecraft!");
236        let result = read_string(&mut &buf[..]).unwrap();
237        assert_eq!(result, "Hello, Minecraft!");
238    }
239
240    #[test]
241    fn test_string_empty() {
242        let mut buf = Vec::new();
243        write_string(&mut buf, "");
244        let result = read_string(&mut &buf[..]).unwrap();
245        assert_eq!(result, "");
246    }
247
248    #[test]
249    fn test_uuid_roundtrip() {
250        let uuid = Uuid::parse_str("069a79f4-44e9-4726-a5be-fca90e38aaf5").unwrap();
251        let mut buf = Vec::new();
252        write_uuid(&mut buf, uuid);
253        assert_eq!(buf.len(), 16);
254        let result = read_uuid(&mut &buf[..]).unwrap();
255        assert_eq!(result, uuid);
256    }
257
258    #[test]
259    fn test_properties_roundtrip() {
260        let props = vec![
261            ProfileProperty {
262                name: "textures".to_string(),
263                value: "base64data".to_string(),
264                signature: Some("sig".to_string()),
265            },
266            ProfileProperty {
267                name: "other".to_string(),
268                value: "val".to_string(),
269                signature: None,
270            },
271        ];
272        let mut buf = Vec::new();
273        write_properties(&mut buf, &props);
274        let result = read_properties(&mut &buf[..]).unwrap();
275        assert_eq!(result.len(), 2);
276        assert_eq!(result[0].name, "textures");
277        assert_eq!(result[0].signature.as_deref(), Some("sig"));
278        assert_eq!(result[1].signature, None);
279    }
280
281    #[test]
282    fn test_nbt_text_component_simple() {
283        let mut buf = Vec::new();
284        write_nbt_text_component(&mut buf, "hello", None);
285        // TAG_Compound (0x0A), TAG_String (0x08), name "text" (4 bytes), value "hello" (5 bytes), TAG_End (0x00)
286        assert_eq!(buf[0], 0x0A); // compound
287        assert_eq!(buf[1], 0x08); // string tag
288        assert_eq!(&buf[2..4], &[0x00, 0x04]); // name length = 4
289        assert_eq!(&buf[4..8], b"text");
290        assert_eq!(&buf[8..10], &[0x00, 0x05]); // value length = 5
291        assert_eq!(&buf[10..15], b"hello");
292        assert_eq!(buf[15], 0x00); // end tag
293        assert_eq!(buf.len(), 16);
294    }
295
296    #[test]
297    fn test_nbt_text_component_with_color() {
298        let mut buf = Vec::new();
299        write_nbt_text_component(&mut buf, "hi", Some("yellow"));
300        assert_eq!(buf[0], 0x0A); // compound
301        // First entry: "text" = "hi"
302        assert_eq!(buf[1], 0x08);
303        assert_eq!(&buf[2..4], &[0x00, 0x04]);
304        assert_eq!(&buf[4..8], b"text");
305        assert_eq!(&buf[8..10], &[0x00, 0x02]);
306        assert_eq!(&buf[10..12], b"hi");
307        // Second entry: "color" = "yellow"
308        assert_eq!(buf[12], 0x08);
309        assert_eq!(&buf[13..15], &[0x00, 0x05]);
310        assert_eq!(&buf[15..20], b"color");
311        assert_eq!(&buf[20..22], &[0x00, 0x06]);
312        assert_eq!(&buf[22..28], b"yellow");
313        // End tag
314        assert_eq!(buf[28], 0x00);
315        assert_eq!(buf.len(), 29);
316    }
317}