Skip to main content

basalt_types/nbt/
decode.rs

1use crate::nbt::tag::{NbtCompound, NbtList, NbtTag, tag_id};
2use crate::{Decode, Error, Result};
3
4/// Reads an NBT string in the NBT wire format (u16-prefixed modified UTF-8).
5///
6/// NBT strings use a big-endian u16 length prefix, unlike protocol strings
7/// which use VarInt. The maximum length is 65535 bytes (u16::MAX).
8fn decode_nbt_string(buf: &mut &[u8]) -> Result<String> {
9    let len = u16::decode(buf)? as usize;
10    if buf.len() < len {
11        return Err(Error::BufferUnderflow {
12            needed: len,
13            available: buf.len(),
14        });
15    }
16    let (bytes, rest) = buf.split_at(len);
17    let value = String::from_utf8(bytes.to_vec())?;
18    *buf = rest;
19    Ok(value)
20}
21
22/// Decodes the payload of an NBT tag given its type ID.
23///
24/// The type ID has already been read from the stream. This function reads
25/// the remaining bytes for the tag's value based on its type. Used for
26/// both compound entries (after reading type + name) and list elements
27/// (after the list header declares the element type).
28fn decode_tag_payload(tag_type: u8, buf: &mut &[u8]) -> Result<NbtTag> {
29    match tag_type {
30        tag_id::BYTE => Ok(NbtTag::Byte(i8::decode(buf)?)),
31        tag_id::SHORT => Ok(NbtTag::Short(i16::decode(buf)?)),
32        tag_id::INT => Ok(NbtTag::Int(i32::decode(buf)?)),
33        tag_id::LONG => Ok(NbtTag::Long(i64::decode(buf)?)),
34        tag_id::FLOAT => Ok(NbtTag::Float(f32::decode(buf)?)),
35        tag_id::DOUBLE => Ok(NbtTag::Double(f64::decode(buf)?)),
36        tag_id::BYTE_ARRAY => {
37            let len = i32::decode(buf)?;
38            if len < 0 {
39                return Err(Error::Nbt(format!("negative byte array length: {len}")));
40            }
41            let len = len as usize;
42            if buf.len() < len {
43                return Err(Error::BufferUnderflow {
44                    needed: len,
45                    available: buf.len(),
46                });
47            }
48            let mut data = Vec::with_capacity(len);
49            for _ in 0..len {
50                data.push(i8::decode(buf)?);
51            }
52            Ok(NbtTag::ByteArray(data))
53        }
54        tag_id::STRING => Ok(NbtTag::String(decode_nbt_string(buf)?)),
55        tag_id::LIST => {
56            let element_type = u8::decode(buf)?;
57            let len = i32::decode(buf)?;
58            if len < 0 {
59                return Err(Error::Nbt(format!("negative list length: {len}")));
60            }
61            let len = len as usize;
62            // Cap allocation to remaining buffer size to prevent OOM from
63            // malicious length fields (each element is at least 1 byte)
64            let mut elements = Vec::with_capacity(len.min(buf.len()));
65            for _ in 0..len {
66                elements.push(decode_tag_payload(element_type, buf)?);
67            }
68            Ok(NbtTag::List(NbtList {
69                element_type,
70                elements,
71            }))
72        }
73        tag_id::COMPOUND => {
74            let compound = decode_compound_payload(buf)?;
75            Ok(NbtTag::Compound(compound))
76        }
77        tag_id::INT_ARRAY => {
78            let len = i32::decode(buf)?;
79            if len < 0 {
80                return Err(Error::Nbt(format!("negative int array length: {len}")));
81            }
82            let len = len as usize;
83            // Each i32 is 4 bytes — cap allocation to prevent OOM
84            let mut data = Vec::with_capacity(len.min(buf.len() / 4));
85            for _ in 0..len {
86                data.push(i32::decode(buf)?);
87            }
88            Ok(NbtTag::IntArray(data))
89        }
90        tag_id::LONG_ARRAY => {
91            let len = i32::decode(buf)?;
92            if len < 0 {
93                return Err(Error::Nbt(format!("negative long array length: {len}")));
94            }
95            let len = len as usize;
96            // Each i64 is 8 bytes — cap allocation to prevent OOM
97            let mut data = Vec::with_capacity(len.min(buf.len() / 8));
98            for _ in 0..len {
99                data.push(i64::decode(buf)?);
100            }
101            Ok(NbtTag::LongArray(data))
102        }
103        _ => Err(Error::Nbt(format!("unknown NBT tag type: {tag_type}"))),
104    }
105}
106
107/// Decodes a compound payload: reads named entries until an End tag is found.
108///
109/// Each entry is: tag type byte + u16-prefixed name + payload.
110/// The compound ends when a tag type of 0 (End) is encountered.
111fn decode_compound_payload(buf: &mut &[u8]) -> Result<NbtCompound> {
112    let mut compound = NbtCompound::new();
113    loop {
114        let tag_type = u8::decode(buf)?;
115        if tag_type == tag_id::END {
116            return Ok(compound);
117        }
118        let name = decode_nbt_string(buf)?;
119        let tag = decode_tag_payload(tag_type, buf)?;
120        compound.insert(name, tag);
121    }
122}
123
124/// Decodes an NbtCompound from network NBT format.
125///
126/// Since Minecraft 1.20.3, network NBT uses a simplified root format:
127/// a compound tag type byte (0x0A) followed directly by the compound
128/// payload (no root tag name). This differs from the traditional NBT
129/// format which includes a root tag name.
130///
131/// Fails if the root tag type is not Compound (0x0A).
132impl Decode for NbtCompound {
133    /// Reads the compound type byte, then decodes the compound payload.
134    ///
135    /// Fails with `Error::Nbt` if the root type is not Compound,
136    /// or with other errors if the payload is malformed.
137    fn decode(buf: &mut &[u8]) -> Result<Self> {
138        let tag_type = u8::decode(buf)?;
139        if tag_type != tag_id::COMPOUND {
140            return Err(Error::Nbt(format!(
141                "expected compound root (type 10), got type {tag_type}"
142            )));
143        }
144        decode_compound_payload(buf)
145    }
146}
147
148/// Decodes an NbtTag from network NBT format.
149///
150/// The root is always expected to be a Compound tag. This delegates to
151/// `NbtCompound::decode` and wraps the result in `NbtTag::Compound`.
152impl Decode for NbtTag {
153    /// Reads a network NBT root compound and wraps it as `NbtTag::Compound`.
154    fn decode(buf: &mut &[u8]) -> Result<Self> {
155        let compound = NbtCompound::decode(buf)?;
156        Ok(NbtTag::Compound(compound))
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::Encode;
164
165    /// Helper: encode then decode an NbtCompound and verify roundtrip.
166    fn roundtrip_compound(compound: &NbtCompound) {
167        let mut buf = Vec::new();
168        compound.encode(&mut buf).unwrap();
169
170        let mut cursor = buf.as_slice();
171        let decoded = NbtCompound::decode(&mut cursor).unwrap();
172        assert!(cursor.is_empty(), "cursor not fully consumed");
173        assert_eq!(decoded, *compound);
174    }
175
176    // -- Empty compound --
177
178    #[test]
179    fn empty_compound() {
180        roundtrip_compound(&NbtCompound::new());
181    }
182
183    // -- Primitive types --
184
185    #[test]
186    fn compound_with_byte() {
187        let mut c = NbtCompound::new();
188        c.insert("value", NbtTag::Byte(42));
189        roundtrip_compound(&c);
190    }
191
192    #[test]
193    fn compound_with_short() {
194        let mut c = NbtCompound::new();
195        c.insert("value", NbtTag::Short(1234));
196        roundtrip_compound(&c);
197    }
198
199    #[test]
200    fn compound_with_int() {
201        let mut c = NbtCompound::new();
202        c.insert("value", NbtTag::Int(100000));
203        roundtrip_compound(&c);
204    }
205
206    #[test]
207    fn compound_with_long() {
208        let mut c = NbtCompound::new();
209        c.insert("value", NbtTag::Long(i64::MAX));
210        roundtrip_compound(&c);
211    }
212
213    #[test]
214    fn compound_with_float() {
215        let mut c = NbtCompound::new();
216        c.insert("value", NbtTag::Float(1.5));
217        roundtrip_compound(&c);
218    }
219
220    #[test]
221    fn compound_with_double() {
222        let mut c = NbtCompound::new();
223        c.insert("value", NbtTag::Double(1.23456789));
224        roundtrip_compound(&c);
225    }
226
227    #[test]
228    fn compound_with_string() {
229        let mut c = NbtCompound::new();
230        c.insert("name", NbtTag::String("hello world".into()));
231        roundtrip_compound(&c);
232    }
233
234    // -- Array types --
235
236    #[test]
237    fn compound_with_byte_array() {
238        let mut c = NbtCompound::new();
239        c.insert("data", NbtTag::ByteArray(vec![1, 2, 3, -1, -128, 127]));
240        roundtrip_compound(&c);
241    }
242
243    #[test]
244    fn compound_with_int_array() {
245        let mut c = NbtCompound::new();
246        c.insert("heights", NbtTag::IntArray(vec![100, 200, -300]));
247        roundtrip_compound(&c);
248    }
249
250    #[test]
251    fn compound_with_long_array() {
252        let mut c = NbtCompound::new();
253        c.insert("states", NbtTag::LongArray(vec![i64::MIN, 0, i64::MAX]));
254        roundtrip_compound(&c);
255    }
256
257    // -- List --
258
259    #[test]
260    fn compound_with_empty_list() {
261        let mut c = NbtCompound::new();
262        c.insert("items", NbtTag::List(NbtList::new()));
263        roundtrip_compound(&c);
264    }
265
266    #[test]
267    fn compound_with_int_list() {
268        let mut c = NbtCompound::new();
269        let list =
270            NbtList::from_tags(vec![NbtTag::Int(1), NbtTag::Int(2), NbtTag::Int(3)]).unwrap();
271        c.insert("scores", NbtTag::List(list));
272        roundtrip_compound(&c);
273    }
274
275    #[test]
276    fn compound_with_string_list() {
277        let mut c = NbtCompound::new();
278        let list = NbtList::from_tags(vec![
279            NbtTag::String("alpha".into()),
280            NbtTag::String("beta".into()),
281        ])
282        .unwrap();
283        c.insert("names", NbtTag::List(list));
284        roundtrip_compound(&c);
285    }
286
287    // -- Nested compounds --
288
289    #[test]
290    fn nested_compound() {
291        let mut inner = NbtCompound::new();
292        inner.insert("x", NbtTag::Int(10));
293        inner.insert("y", NbtTag::Int(64));
294        inner.insert("z", NbtTag::Int(-20));
295
296        let mut outer = NbtCompound::new();
297        outer.insert("pos", NbtTag::Compound(inner));
298        outer.insert("name", NbtTag::String("marker".into()));
299        roundtrip_compound(&outer);
300    }
301
302    #[test]
303    fn deeply_nested() {
304        let mut level3 = NbtCompound::new();
305        level3.insert("deep", NbtTag::Byte(1));
306
307        let mut level2 = NbtCompound::new();
308        level2.insert("mid", NbtTag::Compound(level3));
309
310        let mut level1 = NbtCompound::new();
311        level1.insert("top", NbtTag::Compound(level2));
312        roundtrip_compound(&level1);
313    }
314
315    // -- List of compounds --
316
317    #[test]
318    fn list_of_compounds() {
319        let mut item1 = NbtCompound::new();
320        item1.insert("id", NbtTag::String("minecraft:stone".into()));
321        item1.insert("count", NbtTag::Byte(64));
322
323        let mut item2 = NbtCompound::new();
324        item2.insert("id", NbtTag::String("minecraft:dirt".into()));
325        item2.insert("count", NbtTag::Byte(32));
326
327        let list =
328            NbtList::from_tags(vec![NbtTag::Compound(item1), NbtTag::Compound(item2)]).unwrap();
329
330        let mut c = NbtCompound::new();
331        c.insert("inventory", NbtTag::List(list));
332        roundtrip_compound(&c);
333    }
334
335    // -- Multiple entries --
336
337    #[test]
338    fn compound_with_all_types() {
339        let mut c = NbtCompound::new();
340        c.insert("byte", NbtTag::Byte(i8::MAX));
341        c.insert("short", NbtTag::Short(i16::MIN));
342        c.insert("int", NbtTag::Int(42));
343        c.insert("long", NbtTag::Long(i64::MAX));
344        c.insert("float", NbtTag::Float(1.5));
345        c.insert("double", NbtTag::Double(2.5));
346        c.insert("string", NbtTag::String("test".into()));
347        c.insert("byte_array", NbtTag::ByteArray(vec![1, 2]));
348        c.insert("int_array", NbtTag::IntArray(vec![10, 20]));
349        c.insert("long_array", NbtTag::LongArray(vec![100, 200]));
350        c.insert("list", NbtTag::List(NbtList::new()));
351        c.insert("compound", NbtTag::Compound(NbtCompound::new()));
352        roundtrip_compound(&c);
353    }
354
355    // -- NbtTag root --
356
357    #[test]
358    fn nbt_tag_compound_roundtrip() {
359        let mut c = NbtCompound::new();
360        c.insert("value", NbtTag::Int(42));
361        let tag = NbtTag::Compound(c.clone());
362
363        let mut buf = Vec::new();
364        tag.encode(&mut buf).unwrap();
365
366        let mut cursor = buf.as_slice();
367        let decoded = NbtTag::decode(&mut cursor).unwrap();
368        assert!(cursor.is_empty());
369        assert_eq!(decoded, NbtTag::Compound(c));
370    }
371
372    // -- Error cases --
373
374    #[test]
375    fn invalid_root_type() {
376        // Root type byte is BYTE (1) instead of COMPOUND (10)
377        let buf = [tag_id::BYTE, 42];
378        let mut cursor = buf.as_slice();
379        assert!(matches!(
380            NbtCompound::decode(&mut cursor),
381            Err(Error::Nbt(_))
382        ));
383    }
384
385    #[test]
386    fn empty_buffer() {
387        let mut cursor: &[u8] = &[];
388        assert!(NbtCompound::decode(&mut cursor).is_err());
389    }
390
391    #[test]
392    fn truncated_compound() {
393        // Compound type byte but no payload
394        let buf = [tag_id::COMPOUND];
395        let mut cursor = buf.as_slice();
396        assert!(NbtCompound::decode(&mut cursor).is_err());
397    }
398
399    // -- NbtTag non-compound root encoding --
400
401    #[test]
402    fn nbt_tag_non_compound_wraps_in_compound() {
403        use crate::EncodedSize;
404
405        // Encoding a non-compound NbtTag wraps it in a compound
406        let tag = NbtTag::Int(42);
407        let mut buf = Vec::new();
408        tag.encode(&mut buf).unwrap();
409
410        // Should decode as a compound with one entry
411        let mut cursor = buf.as_slice();
412        let decoded = NbtCompound::decode(&mut cursor).unwrap();
413        assert!(cursor.is_empty());
414        assert_eq!(decoded.get(""), Some(&NbtTag::Int(42)));
415
416        // EncodedSize should match actual encoded length
417        assert_eq!(tag.encoded_size(), buf.len());
418    }
419
420    #[test]
421    fn nbt_tag_string_wraps_in_compound() {
422        use crate::EncodedSize;
423
424        let tag = NbtTag::String("hello".into());
425        let mut buf = Vec::new();
426        tag.encode(&mut buf).unwrap();
427        assert_eq!(tag.encoded_size(), buf.len());
428
429        let mut cursor = buf.as_slice();
430        let decoded = NbtCompound::decode(&mut cursor).unwrap();
431        assert_eq!(decoded.get(""), Some(&NbtTag::String("hello".into())));
432    }
433
434    #[test]
435    fn nbt_tag_list_wraps_in_compound() {
436        use crate::EncodedSize;
437
438        let list = NbtList::from_tags(vec![NbtTag::Int(1), NbtTag::Int(2)]).unwrap();
439        let tag = NbtTag::List(list);
440        let mut buf = Vec::new();
441        tag.encode(&mut buf).unwrap();
442        assert_eq!(tag.encoded_size(), buf.len());
443    }
444
445    #[test]
446    fn nbt_tag_byte_array_wraps_in_compound() {
447        use crate::EncodedSize;
448
449        let tag = NbtTag::ByteArray(vec![1, 2, 3]);
450        let mut buf = Vec::new();
451        tag.encode(&mut buf).unwrap();
452        assert_eq!(tag.encoded_size(), buf.len());
453    }
454
455    #[test]
456    fn nbt_tag_int_array_wraps_in_compound() {
457        use crate::EncodedSize;
458
459        let tag = NbtTag::IntArray(vec![100, 200]);
460        let mut buf = Vec::new();
461        tag.encode(&mut buf).unwrap();
462        assert_eq!(tag.encoded_size(), buf.len());
463    }
464
465    #[test]
466    fn nbt_tag_long_array_wraps_in_compound() {
467        use crate::EncodedSize;
468
469        let tag = NbtTag::LongArray(vec![i64::MIN, i64::MAX]);
470        let mut buf = Vec::new();
471        tag.encode(&mut buf).unwrap();
472        assert_eq!(tag.encoded_size(), buf.len());
473    }
474}