Skip to main content

nbt_rust/
core.rs

1use std::io::{Read, Write};
2
3use crate::config::{NbtReadConfig, ParseMode};
4use crate::encoding::Encoding;
5use crate::error::{Error, Result};
6use crate::limits::NbtLimits;
7use crate::tag::{CompoundTag, ListTag, Tag, TagType};
8
9fn attach_context<T>(
10    op: &'static str,
11    offset: usize,
12    field: Option<&'static str>,
13    result: Result<T>,
14) -> Result<T> {
15    result.map_err(|error| error.with_context(op, offset, field))
16}
17
18pub fn read_payload<E: Encoding, R: Read>(reader: &mut R, tag_type: TagType) -> Result<Tag> {
19    read_payload_with_limits::<E, _>(reader, tag_type, &NbtLimits::default())
20}
21
22pub fn read_payload_with_limits<E: Encoding, R: Read>(
23    reader: &mut R,
24    tag_type: TagType,
25    limits: &NbtLimits,
26) -> Result<Tag> {
27    read_payload_with_config::<E, _>(reader, tag_type, &NbtReadConfig::strict(*limits))
28}
29
30pub fn read_payload_with_config<E: Encoding, R: Read>(
31    reader: &mut R,
32    tag_type: TagType,
33    config: &NbtReadConfig,
34) -> Result<Tag> {
35    let mut limited = LimitedReader::new(reader, config.limits.max_read_bytes);
36    let result = read_payload_inner::<E, _>(&mut limited, tag_type, config, 1);
37    attach_context(
38        "read_payload_with_config",
39        limited.offset(),
40        Some("payload"),
41        result,
42    )
43}
44
45fn read_payload_inner<E: Encoding, R: Read>(
46    reader: &mut LimitedReader<R>,
47    tag_type: TagType,
48    config: &NbtReadConfig,
49    depth: usize,
50) -> Result<Tag> {
51    if depth > config.limits.max_depth {
52        return Err(Error::DepthExceeded {
53            depth,
54            max_depth: config.limits.max_depth,
55        }
56        .with_context("check_depth", reader.offset(), Some("max_depth")));
57    }
58
59    match tag_type {
60        TagType::End => Err(Error::UnexpectedEndTagPayload),
61        TagType::Byte => Ok(Tag::Byte(read_i8(reader)?)),
62        TagType::Short => {
63            let offset = reader.offset();
64            let value = E::read_i16(reader);
65            Ok(Tag::Short(attach_context(
66                "read_i16",
67                offset,
68                Some("short_payload"),
69                value,
70            )?))
71        }
72        TagType::Int => {
73            let offset = reader.offset();
74            let value = E::read_i32(reader);
75            Ok(Tag::Int(attach_context(
76                "read_i32",
77                offset,
78                Some("int_payload"),
79                value,
80            )?))
81        }
82        TagType::Long => {
83            let offset = reader.offset();
84            let value = E::read_i64(reader);
85            Ok(Tag::Long(attach_context(
86                "read_i64",
87                offset,
88                Some("long_payload"),
89                value,
90            )?))
91        }
92        TagType::Float => {
93            let offset = reader.offset();
94            let value = E::read_f32(reader);
95            Ok(Tag::Float(attach_context(
96                "read_f32",
97                offset,
98                Some("float_payload"),
99                value,
100            )?))
101        }
102        TagType::Double => {
103            let offset = reader.offset();
104            let value = E::read_f64(reader);
105            Ok(Tag::Double(attach_context(
106                "read_f64",
107                offset,
108                Some("double_payload"),
109                value,
110            )?))
111        }
112        TagType::ByteArray => {
113            let len = read_len_i32::<E, _>(reader, "byte_array_length")?;
114            let limit_offset = reader.offset();
115            let limit_res =
116                ensure_within_limit("byte_array_length", len, config.limits.max_array_len);
117            attach_context(
118                "validate_size",
119                limit_offset,
120                Some("byte_array_length"),
121                limit_res,
122            )?;
123            let offset = reader.offset();
124            let budget_res = reader.ensure_can_read("byte_array_bytes", len);
125            attach_context(
126                "ensure_can_read",
127                offset,
128                Some("byte_array_bytes"),
129                budget_res,
130            )?;
131            let mut bytes = vec![0u8; len];
132            let offset = reader.offset();
133            let read_res = reader.read_exact(&mut bytes).map_err(Error::from);
134            attach_context("read_exact", offset, Some("byte_array_bytes"), read_res)?;
135            Ok(Tag::ByteArray(bytes))
136        }
137        TagType::String => Ok(Tag::String(read_string::<E, _>(reader, &config.limits)?)),
138        TagType::List => {
139            let element_type = read_tag_type(reader)?;
140            let offset = reader.offset();
141            let len_res = E::read_list_len(reader);
142            let len = attach_context("read_list_len", offset, Some("list_length"), len_res)?;
143            let limit_offset = reader.offset();
144            let limit_res = ensure_within_limit("list_length", len, config.limits.max_list_len);
145            attach_context(
146                "validate_size",
147                limit_offset,
148                Some("list_length"),
149                limit_res,
150            )?;
151            let effective_len = if element_type == TagType::End && len > 0 {
152                match config.parse_mode {
153                    ParseMode::Strict => {
154                        return Err(Error::InvalidListHeader {
155                            element_type_id: element_type.id(),
156                            length: len,
157                        }
158                        .with_context(
159                            "validate_list_header",
160                            limit_offset,
161                            Some("list_length"),
162                        ));
163                    }
164                    ParseMode::Compatible => 0,
165                }
166            } else {
167                len
168            };
169
170            let mut elements = Vec::with_capacity(effective_len);
171            for _ in 0..effective_len {
172                elements.push(read_payload_inner::<E, _>(
173                    reader,
174                    element_type,
175                    config,
176                    depth + 1,
177                )?);
178            }
179            Ok(Tag::List(ListTag {
180                element_type,
181                elements,
182            }))
183        }
184        TagType::Compound => {
185            let mut map = CompoundTag::new();
186            let mut entry_count = 0usize;
187            loop {
188                let next_type = read_tag_type(reader)?;
189                if next_type == TagType::End {
190                    break;
191                }
192                entry_count += 1;
193                let limit_offset = reader.offset();
194                let limit_res = ensure_within_limit(
195                    "compound_entries",
196                    entry_count,
197                    config.limits.max_compound_entries,
198                );
199                attach_context(
200                    "validate_size",
201                    limit_offset,
202                    Some("compound_entries"),
203                    limit_res,
204                )?;
205
206                let name = read_string::<E, _>(reader, &config.limits)?;
207                let value = read_payload_inner::<E, _>(reader, next_type, config, depth + 1)?;
208                map.insert(name, value);
209            }
210            Ok(Tag::Compound(map))
211        }
212        TagType::IntArray => {
213            let len = read_len_i32::<E, _>(reader, "int_array_length")?;
214            let limit_offset = reader.offset();
215            let limit_res =
216                ensure_within_limit("int_array_length", len, config.limits.max_array_len);
217            attach_context(
218                "validate_size",
219                limit_offset,
220                Some("int_array_length"),
221                limit_res,
222            )?;
223            let byte_len =
224                checked_len_to_bytes(len, std::mem::size_of::<i32>(), "int_array_bytes")?;
225            let offset = reader.offset();
226            let budget_res = reader.ensure_can_read("int_array_bytes", byte_len);
227            attach_context(
228                "ensure_can_read",
229                offset,
230                Some("int_array_bytes"),
231                budget_res,
232            )?;
233            let mut values = Vec::with_capacity(len);
234            for _ in 0..len {
235                let offset = reader.offset();
236                let read_res = E::read_i32(reader);
237                values.push(attach_context(
238                    "read_i32",
239                    offset,
240                    Some("int_array_value"),
241                    read_res,
242                )?);
243            }
244            Ok(Tag::IntArray(values))
245        }
246        TagType::LongArray => {
247            let len = read_len_i32::<E, _>(reader, "long_array_length")?;
248            let limit_offset = reader.offset();
249            let limit_res =
250                ensure_within_limit("long_array_length", len, config.limits.max_array_len);
251            attach_context(
252                "validate_size",
253                limit_offset,
254                Some("long_array_length"),
255                limit_res,
256            )?;
257            let byte_len =
258                checked_len_to_bytes(len, std::mem::size_of::<i64>(), "long_array_bytes")?;
259            let offset = reader.offset();
260            let budget_res = reader.ensure_can_read("long_array_bytes", byte_len);
261            attach_context(
262                "ensure_can_read",
263                offset,
264                Some("long_array_bytes"),
265                budget_res,
266            )?;
267            let mut values = Vec::with_capacity(len);
268            for _ in 0..len {
269                let offset = reader.offset();
270                let read_res = E::read_i64(reader);
271                values.push(attach_context(
272                    "read_i64",
273                    offset,
274                    Some("long_array_value"),
275                    read_res,
276                )?);
277            }
278            Ok(Tag::LongArray(values))
279        }
280    }
281}
282
283pub fn write_payload<E: Encoding, W: Write>(writer: &mut W, tag: &Tag) -> Result<()> {
284    match tag {
285        Tag::End => return Err(Error::UnexpectedEndTagPayload),
286        Tag::Byte(value) => writer.write_all(&[*value as u8])?,
287        Tag::Short(value) => E::write_i16(writer, *value)?,
288        Tag::Int(value) => E::write_i32(writer, *value)?,
289        Tag::Long(value) => E::write_i64(writer, *value)?,
290        Tag::Float(value) => E::write_f32(writer, *value)?,
291        Tag::Double(value) => E::write_f64(writer, *value)?,
292        Tag::ByteArray(bytes) => {
293            write_len_i32::<E, _>(writer, "byte_array_length", bytes.len())?;
294            writer.write_all(bytes)?;
295        }
296        Tag::String(text) => write_string::<E, _>(writer, text)?,
297        Tag::List(list) => {
298            list.validate()?;
299            writer.write_all(&[list.element_type.id()])?;
300            E::write_list_len(writer, list.elements.len())?;
301            for element in &list.elements {
302                write_payload::<E, _>(writer, element)?;
303            }
304        }
305        Tag::Compound(map) => {
306            for (name, value) in map {
307                if matches!(value, Tag::End) {
308                    return Err(Error::UnexpectedEndTagPayload);
309                }
310                writer.write_all(&[value.tag_type().id()])?;
311                write_string::<E, _>(writer, name)?;
312                write_payload::<E, _>(writer, value)?;
313            }
314            writer.write_all(&[TagType::End.id()])?;
315        }
316        Tag::IntArray(values) => {
317            write_len_i32::<E, _>(writer, "int_array_length", values.len())?;
318            for value in values {
319                E::write_i32(writer, *value)?;
320            }
321        }
322        Tag::LongArray(values) => {
323            write_len_i32::<E, _>(writer, "long_array_length", values.len())?;
324            for value in values {
325                E::write_i64(writer, *value)?;
326            }
327        }
328    }
329    Ok(())
330}
331
332fn read_tag_type<R: Read>(reader: &mut LimitedReader<R>) -> Result<TagType> {
333    let mut id = [0u8; 1];
334    let offset = reader.offset();
335    let read_res = reader.read_exact(&mut id).map_err(Error::from);
336    attach_context("read_exact", offset, Some("tag_type_id"), read_res)?;
337    let tag_res = TagType::try_from(id[0]);
338    attach_context("decode_tag_type", offset, Some("tag_type_id"), tag_res)
339}
340
341fn read_i8<R: Read>(reader: &mut LimitedReader<R>) -> Result<i8> {
342    let mut byte = [0u8; 1];
343    let offset = reader.offset();
344    let read_res = reader.read_exact(&mut byte).map_err(Error::from);
345    attach_context("read_exact", offset, Some("i8_value"), read_res)?;
346    Ok(byte[0] as i8)
347}
348
349fn read_string<E: Encoding, R: Read>(
350    reader: &mut LimitedReader<R>,
351    limits: &NbtLimits,
352) -> Result<String> {
353    let offset = reader.offset();
354    let len_res = E::read_string_len(reader);
355    let len = attach_context("read_string_len", offset, Some("string_length"), len_res)?;
356    let limit_offset = reader.offset();
357    let limit_res = ensure_within_limit("string_length", len, limits.max_string_len);
358    attach_context(
359        "validate_size",
360        limit_offset,
361        Some("string_length"),
362        limit_res,
363    )?;
364    let budget_offset = reader.offset();
365    let budget_res = reader.ensure_can_read("string_bytes", len);
366    attach_context(
367        "ensure_can_read",
368        budget_offset,
369        Some("string_bytes"),
370        budget_res,
371    )?;
372    let mut bytes = vec![0u8; len];
373    let payload_offset = reader.offset();
374    let read_res = reader.read_exact(&mut bytes).map_err(Error::from);
375    attach_context("read_exact", payload_offset, Some("string_bytes"), read_res)?;
376    let decode_res = String::from_utf8(bytes).map_err(|_| Error::InvalidUtf8 {
377        field: "string_payload",
378    });
379    attach_context(
380        "decode_utf8",
381        payload_offset,
382        Some("string_payload"),
383        decode_res,
384    )
385}
386
387fn write_string<E: Encoding, W: Write>(writer: &mut W, value: &str) -> Result<()> {
388    E::write_string_len(writer, value.len())?;
389    writer.write_all(value.as_bytes())?;
390    Ok(())
391}
392
393fn read_len_i32<E: Encoding, R: Read>(
394    reader: &mut LimitedReader<R>,
395    field: &'static str,
396) -> Result<usize> {
397    let offset = reader.offset();
398    let len_res = E::read_i32(reader);
399    let len = attach_context("read_i32", offset, Some(field), len_res)?;
400    if len < 0 {
401        return Err(Error::NegativeLength { field, value: len }.with_context(
402            "validate_non_negative_length",
403            offset,
404            Some(field),
405        ));
406    }
407    usize::try_from(len).map_err(|_| Error::LengthOverflow {
408        field,
409        max: usize::MAX,
410        actual: len as usize,
411    })
412}
413
414fn write_len_i32<E: Encoding, W: Write>(
415    writer: &mut W,
416    field: &'static str,
417    len: usize,
418) -> Result<()> {
419    if len > i32::MAX as usize {
420        return Err(Error::LengthOverflow {
421            field,
422            max: i32::MAX as usize,
423            actual: len,
424        });
425    }
426    E::write_i32(writer, len as i32)
427}
428
429fn ensure_within_limit(field: &'static str, actual: usize, max: usize) -> Result<()> {
430    if actual > max {
431        return Err(Error::SizeExceeded { field, max, actual });
432    }
433    Ok(())
434}
435
436fn checked_len_to_bytes(count: usize, elem_size: usize, field: &'static str) -> Result<usize> {
437    count.checked_mul(elem_size).ok_or(Error::LengthOverflow {
438        field,
439        max: usize::MAX,
440        actual: count,
441    })
442}
443
444struct LimitedReader<R> {
445    inner: R,
446    remaining: usize,
447    consumed: usize,
448}
449
450impl<R> LimitedReader<R> {
451    fn new(inner: R, max_read_bytes: usize) -> Self {
452        Self {
453            inner,
454            remaining: max_read_bytes,
455            consumed: 0,
456        }
457    }
458
459    fn offset(&self) -> usize {
460        self.consumed
461    }
462
463    fn ensure_can_read(&self, field: &'static str, size: usize) -> Result<()> {
464        if size > self.remaining {
465            return Err(Error::SizeExceeded {
466                field,
467                max: self.remaining,
468                actual: size,
469            });
470        }
471        Ok(())
472    }
473}
474
475impl<R: Read> Read for LimitedReader<R> {
476    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
477        if buf.is_empty() {
478            return Ok(0);
479        }
480        if self.remaining == 0 {
481            return Ok(0);
482        }
483
484        let capped_len = buf.len().min(self.remaining);
485        let read_len = self.inner.read(&mut buf[..capped_len])?;
486        self.remaining -= read_len;
487        self.consumed += read_len;
488        Ok(read_len)
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use std::io::Cursor;
495
496    use indexmap::IndexMap;
497
498    use crate::config::NbtReadConfig;
499    use crate::encoding::{BigEndian, Encoding, LittleEndian, NetworkLittleEndian};
500    use crate::limits::NbtLimits;
501
502    use super::*;
503
504    fn sample_compound_tag() -> Tag {
505        let mut root = IndexMap::new();
506        root.insert("health".to_string(), Tag::Int(20));
507        root.insert("name".to_string(), Tag::String("Steve".to_string()));
508        root.insert(
509            "pos".to_string(),
510            Tag::List(
511                ListTag::new(
512                    TagType::Float,
513                    vec![Tag::Float(1.0), Tag::Float(64.0), Tag::Float(-3.5)],
514                )
515                .unwrap(),
516            ),
517        );
518        root.insert("flags".to_string(), Tag::ByteArray(vec![1, 0, 1]));
519        root.insert("scores".to_string(), Tag::IntArray(vec![1, 2, 3]));
520        root.insert("history".to_string(), Tag::LongArray(vec![9, -3, 27]));
521        Tag::Compound(root)
522    }
523
524    fn assert_array_roundtrip<E: Encoding>(tag: &Tag, tag_type: TagType) {
525        let mut out = Vec::new();
526        write_payload::<E, _>(&mut out, tag).unwrap();
527        let mut input = Cursor::new(out);
528        let decoded = read_payload::<E, _>(&mut input, tag_type).unwrap();
529        assert_eq!(decoded, *tag);
530        assert_eq!(decoded.tag_type(), tag_type);
531    }
532
533    #[test]
534    fn be_roundtrip_compound_payload() {
535        let tag = sample_compound_tag();
536        let mut out = Vec::new();
537        write_payload::<BigEndian, _>(&mut out, &tag).unwrap();
538        let mut input = Cursor::new(out);
539        let decoded = read_payload::<BigEndian, _>(&mut input, TagType::Compound).unwrap();
540        assert_eq!(decoded, tag);
541    }
542
543    #[test]
544    fn int_array_roundtrip_all_encodings_preserves_variant() {
545        let tag = Tag::IntArray(vec![-2, -1, 0, 1, 2, i32::MIN, i32::MAX]);
546        assert_array_roundtrip::<BigEndian>(&tag, TagType::IntArray);
547        assert_array_roundtrip::<LittleEndian>(&tag, TagType::IntArray);
548        assert_array_roundtrip::<NetworkLittleEndian>(&tag, TagType::IntArray);
549    }
550
551    #[test]
552    fn long_array_roundtrip_all_encodings_preserves_variant() {
553        let tag = Tag::LongArray(vec![-2, -1, 0, 1, 2, i64::MIN, i64::MAX]);
554        assert_array_roundtrip::<BigEndian>(&tag, TagType::LongArray);
555        assert_array_roundtrip::<LittleEndian>(&tag, TagType::LongArray);
556        assert_array_roundtrip::<NetworkLittleEndian>(&tag, TagType::LongArray);
557    }
558
559    #[test]
560    fn le_roundtrip_compound_payload() {
561        let tag = sample_compound_tag();
562        let mut out = Vec::new();
563        write_payload::<LittleEndian, _>(&mut out, &tag).unwrap();
564        let mut input = Cursor::new(out);
565        let decoded = read_payload::<LittleEndian, _>(&mut input, TagType::Compound).unwrap();
566        assert_eq!(decoded, tag);
567    }
568
569    #[test]
570    fn nle_roundtrip_compound_payload() {
571        let tag = sample_compound_tag();
572        let mut out = Vec::new();
573        write_payload::<NetworkLittleEndian, _>(&mut out, &tag).unwrap();
574        let mut input = Cursor::new(out);
575        let decoded =
576            read_payload::<NetworkLittleEndian, _>(&mut input, TagType::Compound).unwrap();
577        assert_eq!(decoded, tag);
578    }
579
580    #[test]
581    fn list_constructor_rejects_mixed_types() {
582        let err = ListTag::new(TagType::Int, vec![Tag::Int(1), Tag::String("bad".into())]);
583        assert!(matches!(err, Err(Error::UnexpectedType { .. })));
584    }
585
586    #[test]
587    fn list_decode_rejects_end_type_with_non_zero_length() {
588        let payload = vec![0x00, 0x00, 0x00, 0x00, 0x01];
589        let mut input = Cursor::new(payload);
590        let err = read_payload::<BigEndian, _>(&mut input, TagType::List);
591        let err = err.unwrap_err();
592        assert!(matches!(err.innermost(), Error::InvalidListHeader { .. }));
593    }
594
595    #[test]
596    fn list_decode_compatible_mode_accepts_end_type_with_non_zero_length() {
597        let payload = vec![0x00, 0x00, 0x00, 0x00, 0x01];
598        let mut input = Cursor::new(payload);
599        let config = NbtReadConfig::compatible(NbtLimits::default());
600        let decoded =
601            read_payload_with_config::<BigEndian, _>(&mut input, TagType::List, &config).unwrap();
602        assert_eq!(decoded, Tag::List(ListTag::empty(TagType::End)));
603    }
604
605    #[test]
606    fn empty_list_encode_be_writes_elem_type_and_zero_len() {
607        let int_empty = Tag::List(ListTag::empty(TagType::Int));
608        let end_empty = Tag::List(ListTag::empty(TagType::End));
609
610        let mut be_int = Vec::new();
611        write_payload::<BigEndian, _>(&mut be_int, &int_empty).unwrap();
612        assert_eq!(be_int, vec![TagType::Int.id(), 0x00, 0x00, 0x00, 0x00]);
613
614        let mut be_end = Vec::new();
615        write_payload::<BigEndian, _>(&mut be_end, &end_empty).unwrap();
616        assert_eq!(be_end, vec![TagType::End.id(), 0x00, 0x00, 0x00, 0x00]);
617
618        let mut input = Cursor::new(be_int);
619        let decoded = read_payload::<BigEndian, _>(&mut input, TagType::List).unwrap();
620        assert_eq!(decoded, int_empty);
621    }
622
623    #[test]
624    fn empty_list_encode_le_writes_elem_type_and_zero_len() {
625        let int_empty = Tag::List(ListTag::empty(TagType::Int));
626        let end_empty = Tag::List(ListTag::empty(TagType::End));
627
628        let mut le_int = Vec::new();
629        write_payload::<LittleEndian, _>(&mut le_int, &int_empty).unwrap();
630        assert_eq!(le_int, vec![TagType::Int.id(), 0x00, 0x00, 0x00, 0x00]);
631
632        let mut le_end = Vec::new();
633        write_payload::<LittleEndian, _>(&mut le_end, &end_empty).unwrap();
634        assert_eq!(le_end, vec![TagType::End.id(), 0x00, 0x00, 0x00, 0x00]);
635
636        let mut input = Cursor::new(le_int);
637        let decoded = read_payload::<LittleEndian, _>(&mut input, TagType::List).unwrap();
638        assert_eq!(decoded, int_empty);
639    }
640
641    #[test]
642    fn empty_list_encode_nle_writes_elem_type_and_zero_len() {
643        let int_empty = Tag::List(ListTag::empty(TagType::Int));
644        let end_empty = Tag::List(ListTag::empty(TagType::End));
645
646        let mut nle_int = Vec::new();
647        write_payload::<NetworkLittleEndian, _>(&mut nle_int, &int_empty).unwrap();
648        assert_eq!(nle_int, vec![TagType::Int.id(), 0x00]);
649
650        let mut nle_end = Vec::new();
651        write_payload::<NetworkLittleEndian, _>(&mut nle_end, &end_empty).unwrap();
652        assert_eq!(nle_end, vec![TagType::End.id(), 0x00]);
653
654        let mut input = Cursor::new(nle_int);
655        let decoded = read_payload::<NetworkLittleEndian, _>(&mut input, TagType::List).unwrap();
656        assert_eq!(decoded, int_empty);
657    }
658
659    #[test]
660    fn byte_array_negative_length_is_rejected() {
661        let payload = (-1i32).to_le_bytes().to_vec();
662        let mut input = Cursor::new(payload);
663        let err = read_payload::<LittleEndian, _>(&mut input, TagType::ByteArray);
664        let err = err.unwrap_err();
665        assert!(matches!(err.innermost(), Error::NegativeLength { .. }));
666    }
667
668    #[test]
669    fn compound_rejects_unknown_inner_tag_id() {
670        let payload = vec![
671            99, // unknown type id
672            0x00, 0x00, // empty name
673            0,    // end
674        ];
675        let mut input = Cursor::new(payload);
676        let err = read_payload::<BigEndian, _>(&mut input, TagType::Compound);
677        let err = err.unwrap_err();
678        assert!(matches!(err.innermost(), Error::UnknownTag { id: 99 }));
679    }
680
681    #[test]
682    fn string_limit_rejects_large_string() {
683        let payload = vec![0x00, 0x05, b'h', b'e', b'l', b'l', b'o'];
684        let mut input = Cursor::new(payload);
685        let limits = NbtLimits::default().with_max_string_len(4);
686        let err = read_payload_with_limits::<BigEndian, _>(&mut input, TagType::String, &limits);
687        let err = err.unwrap_err();
688        assert!(matches!(
689            err.innermost(),
690            Error::SizeExceeded {
691                field: "string_length",
692                ..
693            }
694        ));
695    }
696
697    #[test]
698    fn array_limit_rejects_large_byte_array() {
699        let payload = (5i32).to_be_bytes().to_vec();
700        let mut input = Cursor::new(payload);
701        let limits = NbtLimits::default().with_max_array_len(4);
702        let err = read_payload_with_limits::<BigEndian, _>(&mut input, TagType::ByteArray, &limits);
703        let err = err.unwrap_err();
704        assert!(matches!(
705            err.innermost(),
706            Error::SizeExceeded {
707                field: "byte_array_length",
708                ..
709            }
710        ));
711    }
712
713    #[test]
714    fn read_budget_rejects_over_budget_payload() {
715        let payload = vec![0x00, 0x04, b't', b'e', b's', b't'];
716        let mut input = Cursor::new(payload);
717        let limits = NbtLimits::default().with_max_read_bytes(3);
718        let err = read_payload_with_limits::<BigEndian, _>(&mut input, TagType::String, &limits);
719        let err = err.unwrap_err();
720        assert!(matches!(
721            err.innermost(),
722            Error::SizeExceeded {
723                field: "string_bytes",
724                ..
725            }
726        ));
727    }
728
729    #[test]
730    fn checked_len_to_bytes_overflow_is_rejected() {
731        let err = checked_len_to_bytes(usize::MAX, 2, "int_array_bytes").unwrap_err();
732        assert!(matches!(
733            err,
734            Error::LengthOverflow {
735                field: "int_array_bytes",
736                ..
737            }
738        ));
739    }
740
741    #[test]
742    fn int_array_budget_guard_rejects_before_value_reads() {
743        let payload = (4i32).to_be_bytes().to_vec();
744        let mut input = Cursor::new(payload);
745        let limits = NbtLimits::default().with_max_read_bytes(6);
746        let err = read_payload_with_limits::<BigEndian, _>(&mut input, TagType::IntArray, &limits)
747            .unwrap_err();
748        assert!(matches!(
749            err.innermost(),
750            Error::SizeExceeded {
751                field: "int_array_bytes",
752                ..
753            }
754        ));
755    }
756
757    #[test]
758    fn long_array_budget_guard_rejects_before_value_reads() {
759        let payload = (4i32).to_be_bytes().to_vec();
760        let mut input = Cursor::new(payload);
761        let limits = NbtLimits::default().with_max_read_bytes(6);
762        let err = read_payload_with_limits::<BigEndian, _>(&mut input, TagType::LongArray, &limits)
763            .unwrap_err();
764        assert!(matches!(
765            err.innermost(),
766            Error::SizeExceeded {
767                field: "long_array_bytes",
768                ..
769            }
770        ));
771    }
772
773    #[test]
774    fn depth_limit_rejects_nested_compound() {
775        let mut inner = IndexMap::new();
776        inner.insert("value".to_string(), Tag::Int(1));
777
778        let mut outer = IndexMap::new();
779        outer.insert("nested".to_string(), Tag::Compound(inner));
780
781        let tag = Tag::Compound(outer);
782        let mut bytes = Vec::new();
783        write_payload::<BigEndian, _>(&mut bytes, &tag).unwrap();
784
785        let mut input = Cursor::new(bytes);
786        let limits = NbtLimits::default().with_max_depth(1);
787        let err = read_payload_with_limits::<BigEndian, _>(&mut input, TagType::Compound, &limits);
788        let err = err.unwrap_err();
789        assert!(matches!(err.innermost(), Error::DepthExceeded { .. }));
790    }
791
792    #[test]
793    fn contextual_error_contains_op_offset_and_field() {
794        let payload = vec![0x00, 0x00, 0x00, 0x00, 0x01];
795        let mut input = Cursor::new(payload);
796        let err = read_payload::<BigEndian, _>(&mut input, TagType::List).unwrap_err();
797
798        assert!(err.has_context("validate_list_header", Some("list_length")));
799        assert!(err.has_context("read_payload_with_config", Some("payload")));
800    }
801
802    #[test]
803    fn nested_compound_end_only_closes_nested_scope() {
804        let payload = vec![
805            0x0A, // nested compound
806            0x00, 0x06, b'n', b'e', b's', b't', b'e', b'd', // name = "nested"
807            0x03, // int
808            0x00, 0x01, b'a', // name = "a"
809            0x00, 0x00, 0x00, 0x01, // a = 1
810            0x00, // end nested compound
811            0x03, // int
812            0x00, 0x01, b'b', // name = "b"
813            0x00, 0x00, 0x00, 0x02, // b = 2
814            0x00, // end root compound
815        ];
816
817        let mut input = Cursor::new(payload);
818        let decoded = read_payload::<BigEndian, _>(&mut input, TagType::Compound).unwrap();
819
820        let mut nested = IndexMap::new();
821        nested.insert("a".to_string(), Tag::Int(1));
822
823        let mut expected = IndexMap::new();
824        expected.insert("nested".to_string(), Tag::Compound(nested));
825        expected.insert("b".to_string(), Tag::Int(2));
826
827        assert_eq!(decoded, Tag::Compound(expected));
828    }
829
830    #[test]
831    fn missing_compound_end_reports_contextual_io_error() {
832        let payload = vec![
833            0x03, // int
834            0x00, 0x01, b'a', // name = "a"
835            0x00, 0x00, 0x00, 0x01, // value
836                  // missing TAG_End for compound
837        ];
838        let mut input = Cursor::new(payload);
839        let err = read_payload::<BigEndian, _>(&mut input, TagType::Compound).unwrap_err();
840
841        assert!(matches!(err.innermost(), Error::Io(_)));
842        assert!(err.has_context("read_exact", Some("tag_type_id")));
843    }
844
845    #[test]
846    fn write_payload_rejects_end_tag_value() {
847        let mut out = Vec::new();
848        let err = write_payload::<BigEndian, _>(&mut out, &Tag::End).unwrap_err();
849        assert!(matches!(err, Error::UnexpectedEndTagPayload));
850        assert!(out.is_empty());
851    }
852
853    #[test]
854    fn compound_write_rejects_end_tag_member_without_partial_write() {
855        let mut map = IndexMap::new();
856        map.insert("bad".to_string(), Tag::End);
857
858        let mut out = Vec::new();
859        let err = write_payload::<BigEndian, _>(&mut out, &Tag::Compound(map)).unwrap_err();
860        assert!(matches!(err, Error::UnexpectedEndTagPayload));
861        assert!(out.is_empty());
862    }
863}