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, 0x00, 0x00, 0, ];
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, 0x00, 0x06, b'n', b'e', b's', b't', b'e', b'd', 0x03, 0x00, 0x01, b'a', 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, b'b', 0x00, 0x00, 0x00, 0x02, 0x00, ];
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, 0x00, 0x01, b'a', 0x00, 0x00, 0x00, 0x01, ];
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}