use crate::error::{BedrockWorldError, Result};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::io::{Cursor, Read, Write};
const MAX_NBT_DEPTH: usize = 128;
const MAX_NBT_CONTAINER_LENGTH: usize = 1_000_000;
const MAX_NBT_BYTE_LENGTH: usize = 32 * 1024 * 1024;
const MAX_NBT_STRING_BYTES: usize = u16::MAX as usize;
const TAG_END: u8 = 0x00;
const TAG_BYTE: u8 = 0x01;
const TAG_SHORT: u8 = 0x02;
const TAG_INT: u8 = 0x03;
const TAG_LONG: u8 = 0x04;
const TAG_FLOAT: u8 = 0x05;
const TAG_DOUBLE: u8 = 0x06;
const TAG_BYTE_ARRAY: u8 = 0x07;
const TAG_STRING: u8 = 0x08;
const TAG_LIST: u8 = 0x09;
const TAG_COMPOUND: u8 = 0x0a;
const TAG_INT_ARRAY: u8 = 0x0b;
const TAG_LONG_ARRAY: u8 = 0x0c;
const TAG_SHORT_ARRAY: u8 = 0x64;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum NbtTag {
End,
Byte(i8),
Short(i16),
Int(i32),
Long(i64),
Float(f32),
Double(f64),
ByteArray(Vec<i8>),
String(String),
List(Vec<NbtTag>),
Compound(IndexMap<String, NbtTag>),
IntArray(Vec<i32>),
LongArray(Vec<i64>),
ShortArray(Vec<i16>),
}
pub type NbtValue = NbtTag;
#[derive(Debug, Clone, PartialEq)]
pub enum NbtRef<'a> {
End,
Byte(i8),
Short(i16),
Int(i32),
Long(i64),
Float(f32),
Double(f64),
ByteArray(Cow<'a, [i8]>),
String(Cow<'a, str>),
List(Vec<NbtRef<'a>>),
Compound(Vec<(Cow<'a, str>, NbtRef<'a>)>),
IntArray(Cow<'a, [i32]>),
LongArray(Cow<'a, [i64]>),
ShortArray(Cow<'a, [i16]>),
}
impl NbtRef<'_> {
#[must_use]
pub fn to_owned_tag(&self) -> NbtTag {
match self {
Self::End => NbtTag::End,
Self::Byte(value) => NbtTag::Byte(*value),
Self::Short(value) => NbtTag::Short(*value),
Self::Int(value) => NbtTag::Int(*value),
Self::Long(value) => NbtTag::Long(*value),
Self::Float(value) => NbtTag::Float(*value),
Self::Double(value) => NbtTag::Double(*value),
Self::ByteArray(values) => NbtTag::ByteArray(values.to_vec()),
Self::String(value) => NbtTag::String(value.to_string()),
Self::List(values) => NbtTag::List(values.iter().map(Self::to_owned_tag).collect()),
Self::Compound(values) => NbtTag::Compound(
values
.iter()
.map(|(key, value)| (key.to_string(), value.to_owned_tag()))
.collect(),
),
Self::IntArray(values) => NbtTag::IntArray(values.to_vec()),
Self::LongArray(values) => NbtTag::LongArray(values.to_vec()),
Self::ShortArray(values) => NbtTag::ShortArray(values.to_vec()),
}
}
}
pub struct NbtReader<'a> {
data: &'a [u8],
}
impl<'a> NbtReader<'a> {
#[must_use]
pub const fn new(data: &'a [u8]) -> Self {
Self { data }
}
pub fn parse_root(&self) -> Result<NbtTag> {
parse_root_nbt(self.data)
}
pub fn parse_root_with_consumed(&self) -> Result<(NbtTag, usize)> {
parse_root_nbt_with_consumed(self.data)
}
pub fn parse_root_ref(&self) -> Result<NbtRef<'a>> {
self.parse_root().map(NbtRef::from_owned)
}
pub fn view(&self) -> NbtView<'a> {
NbtView::new(self.data)
}
}
#[derive(Debug, Clone, Copy)]
pub struct NbtView<'a> {
data: &'a [u8],
}
impl<'a> NbtView<'a> {
#[must_use]
pub const fn new(data: &'a [u8]) -> Self {
Self { data }
}
pub fn events(&self) -> Result<Vec<NbtEvent<'a>>> {
parse_nbt_events(self.data)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum NbtEvent<'a> {
BeginCompound {
name: Option<&'a str>,
},
EndCompound,
BeginList {
name: Option<&'a str>,
element_type: u8,
len: usize,
},
EndList,
Byte {
name: Option<&'a str>,
value: i8,
},
Short {
name: Option<&'a str>,
value: i16,
},
Int {
name: Option<&'a str>,
value: i32,
},
Long {
name: Option<&'a str>,
value: i64,
},
Float {
name: Option<&'a str>,
value: f32,
},
Double {
name: Option<&'a str>,
value: f64,
},
String {
name: Option<&'a str>,
value: &'a str,
},
ByteArray {
name: Option<&'a str>,
bytes: &'a [u8],
},
IntArray {
name: Option<&'a str>,
bytes: &'a [u8],
len: usize,
},
LongArray {
name: Option<&'a str>,
bytes: &'a [u8],
len: usize,
},
ShortArray {
name: Option<&'a str>,
bytes: &'a [u8],
len: usize,
},
}
impl NbtRef<'_> {
fn from_owned(tag: NbtTag) -> Self {
match tag {
NbtTag::End => Self::End,
NbtTag::Byte(value) => Self::Byte(value),
NbtTag::Short(value) => Self::Short(value),
NbtTag::Int(value) => Self::Int(value),
NbtTag::Long(value) => Self::Long(value),
NbtTag::Float(value) => Self::Float(value),
NbtTag::Double(value) => Self::Double(value),
NbtTag::ByteArray(values) => Self::ByteArray(Cow::Owned(values)),
NbtTag::String(value) => Self::String(Cow::Owned(value)),
NbtTag::List(values) => Self::List(values.into_iter().map(Self::from_owned).collect()),
NbtTag::Compound(values) => Self::Compound(
values
.into_iter()
.map(|(key, value)| (Cow::Owned(key), Self::from_owned(value)))
.collect(),
),
NbtTag::IntArray(values) => Self::IntArray(Cow::Owned(values)),
NbtTag::LongArray(values) => Self::LongArray(Cow::Owned(values)),
NbtTag::ShortArray(values) => Self::ShortArray(Cow::Owned(values)),
}
}
}
pub struct NbtWriter;
impl NbtWriter {
pub fn write_root(tag: &NbtTag) -> Result<Vec<u8>> {
serialize_root_nbt(tag)
}
}
pub fn parse_root_nbt(data: &[u8]) -> Result<NbtTag> {
parse_root_nbt_with_consumed(data).map(|(tag, _)| tag)
}
pub fn parse_root_nbt_with_consumed(data: &[u8]) -> Result<(NbtTag, usize)> {
let mut cursor = Cursor::new(data);
let (_, tag) = parse_named_tag(&mut cursor, 0)?;
if !matches!(tag, NbtTag::Compound(_)) {
return Err(BedrockWorldError::Nbt(
"root NBT tag must be Compound".to_string(),
));
}
let consumed = usize::try_from(cursor.position())
.map_err(|_| BedrockWorldError::Nbt("NBT cursor position overflowed".to_string()))?;
Ok((tag, consumed))
}
pub fn parse_consecutive_root_nbt(mut data: &[u8]) -> Result<Vec<NbtTag>> {
let mut tags = Vec::new();
while !data.is_empty() {
let (tag, consumed) = parse_root_nbt_with_consumed(data)?;
if consumed == 0 || consumed > data.len() {
return Err(BedrockWorldError::Nbt(
"consecutive NBT parser did not advance".to_string(),
));
}
tags.push(tag);
data = &data[consumed..];
}
Ok(tags)
}
pub fn serialize_root_nbt(tag: &NbtTag) -> Result<Vec<u8>> {
validate_root_nbt_for_write(tag)?;
let mut buf = Vec::new();
serialize_named_tag(&mut buf, "", tag)?;
Ok(buf)
}
pub fn validate_root_nbt_for_write(tag: &NbtTag) -> Result<()> {
match tag {
NbtTag::Compound(_) => validate_nbt_tag_for_write(tag, 0, "<root>"),
_ => Err(BedrockWorldError::Validation(
"level.dat root must be Compound".to_string(),
)),
}
}
fn parse_named_tag(reader: &mut impl Read, depth: usize) -> Result<(String, NbtTag)> {
let tag_type = read_u8(reader)?;
if tag_type == TAG_END {
return Ok((String::new(), NbtTag::End));
}
ensure_depth(depth, tag_type)?;
let name = read_string(reader)?;
let value = parse_tag_payload(reader, tag_type, depth)?;
Ok((name, value))
}
fn parse_tag_payload(reader: &mut impl Read, tag_type: u8, depth: usize) -> Result<NbtTag> {
ensure_depth(depth, tag_type)?;
match tag_type {
TAG_BYTE => Ok(NbtTag::Byte(read_i8(reader)?)),
TAG_SHORT => Ok(NbtTag::Short(read_i16(reader)?)),
TAG_INT => Ok(NbtTag::Int(read_i32(reader)?)),
TAG_LONG => Ok(NbtTag::Long(read_i64(reader)?)),
TAG_FLOAT => Ok(NbtTag::Float(read_f32(reader)?)),
TAG_DOUBLE => Ok(NbtTag::Double(read_f64(reader)?)),
TAG_BYTE_ARRAY => {
let len = read_byte_length(reader, "ByteArray")?;
let mut values = Vec::with_capacity(len);
for _ in 0..len {
values.push(read_i8(reader)?);
}
Ok(NbtTag::ByteArray(values))
}
TAG_STRING => Ok(NbtTag::String(read_string(reader)?)),
TAG_LIST => {
let element_type = read_u8(reader)?;
let len = read_container_length(reader, "List")?;
let mut values = Vec::with_capacity(len);
for _ in 0..len {
values.push(parse_tag_payload(reader, element_type, depth + 1)?);
}
Ok(NbtTag::List(values))
}
TAG_COMPOUND => {
let mut map = IndexMap::new();
loop {
let tag_type = read_u8(reader)?;
if tag_type == TAG_END {
break;
}
let name = read_string(reader)?;
let value = parse_tag_payload(reader, tag_type, depth + 1)?;
map.insert(name, value);
if map.len() > MAX_NBT_CONTAINER_LENGTH {
return Err(BedrockWorldError::Nbt("Compound is too large".to_string()));
}
}
Ok(NbtTag::Compound(map))
}
TAG_INT_ARRAY => {
let len = read_container_length(reader, "IntArray")?;
let mut values = Vec::with_capacity(len);
for _ in 0..len {
values.push(read_i32(reader)?);
}
Ok(NbtTag::IntArray(values))
}
TAG_LONG_ARRAY => {
let len = read_container_length(reader, "LongArray")?;
let mut values = Vec::with_capacity(len);
for _ in 0..len {
values.push(read_i64(reader)?);
}
Ok(NbtTag::LongArray(values))
}
TAG_SHORT_ARRAY => {
let len = read_container_length(reader, "ShortArray")?;
let mut values = Vec::with_capacity(len);
for _ in 0..len {
values.push(read_i16(reader)?);
}
Ok(NbtTag::ShortArray(values))
}
_ => Err(BedrockWorldError::Nbt(format!(
"unknown NBT tag type: {tag_type}"
))),
}
}
fn serialize_named_tag(writer: &mut impl Write, name: &str, tag: &NbtTag) -> Result<()> {
let tag_type = tag_discriminant(tag);
writer.write_all(&[tag_type])?;
if tag_type != TAG_END {
write_string(writer, name)?;
serialize_tag_payload(writer, tag)?;
}
Ok(())
}
fn serialize_tag_payload(writer: &mut impl Write, tag: &NbtTag) -> Result<()> {
match tag {
NbtTag::End => Ok(()),
NbtTag::Byte(value) => writer.write_all(&[*value as u8]).map_err(Into::into),
NbtTag::Short(value) => write_i16(writer, *value),
NbtTag::Int(value) => write_i32(writer, *value),
NbtTag::Long(value) => write_i64(writer, *value),
NbtTag::Float(value) => writer.write_all(&value.to_le_bytes()).map_err(Into::into),
NbtTag::Double(value) => writer.write_all(&value.to_le_bytes()).map_err(Into::into),
NbtTag::ByteArray(values) => {
write_i32_len(writer, values.len())?;
for value in values {
writer.write_all(&[*value as u8])?;
}
Ok(())
}
NbtTag::String(value) => write_string(writer, value),
NbtTag::List(values) => {
let element_type = values.first().map_or(TAG_END, tag_discriminant);
writer.write_all(&[element_type])?;
write_i32_len(writer, values.len())?;
for value in values {
serialize_tag_payload(writer, value)?;
}
Ok(())
}
NbtTag::Compound(values) => {
for (name, value) in values {
serialize_named_tag(writer, name, value)?;
}
writer.write_all(&[TAG_END])?;
Ok(())
}
NbtTag::IntArray(values) => {
write_i32_len(writer, values.len())?;
for value in values {
write_i32(writer, *value)?;
}
Ok(())
}
NbtTag::LongArray(values) => {
write_i32_len(writer, values.len())?;
for value in values {
write_i64(writer, *value)?;
}
Ok(())
}
NbtTag::ShortArray(values) => {
write_i32_len(writer, values.len())?;
for value in values {
write_i16(writer, *value)?;
}
Ok(())
}
}
}
fn validate_nbt_tag_for_write(tag: &NbtTag, depth: usize, path: &str) -> Result<()> {
if depth > MAX_NBT_DEPTH {
return Err(BedrockWorldError::Validation(format!(
"NBT nesting is too deep: {path}"
)));
}
match tag {
NbtTag::End
| NbtTag::Byte(_)
| NbtTag::Short(_)
| NbtTag::Int(_)
| NbtTag::Long(_)
| NbtTag::Float(_)
| NbtTag::Double(_) => Ok(()),
NbtTag::String(value) => validate_string(value, path),
NbtTag::ByteArray(values) => validate_array_len(values.len(), path),
NbtTag::IntArray(values) => validate_array_len(values.len(), path),
NbtTag::LongArray(values) => validate_array_len(values.len(), path),
NbtTag::ShortArray(values) => validate_array_len(values.len(), path),
NbtTag::List(values) => {
validate_array_len(values.len(), path)?;
let Some(first) = values.first() else {
return Ok(());
};
let first_type = tag_discriminant(first);
if first_type == TAG_END {
return Err(BedrockWorldError::Validation(format!(
"NBT List cannot contain End: {path}"
)));
}
for (index, value) in values.iter().enumerate() {
if tag_discriminant(value) != first_type {
return Err(BedrockWorldError::Validation(format!(
"NBT List element type mismatch at {path}[{index}]"
)));
}
validate_nbt_tag_for_write(value, depth + 1, &format!("{path}[{index}]"))?;
}
Ok(())
}
NbtTag::Compound(values) => {
validate_array_len(values.len(), path)?;
for (key, value) in values {
validate_string(key, path)?;
validate_nbt_tag_for_write(value, depth + 1, &format!("{path}.{key}"))?;
}
Ok(())
}
}
}
pub fn nbt_tags_equal_for_write(left: &NbtTag, right: &NbtTag) -> bool {
match (left, right) {
(NbtTag::End, NbtTag::End) => true,
(NbtTag::Byte(left), NbtTag::Byte(right)) => left == right,
(NbtTag::Short(left), NbtTag::Short(right)) => left == right,
(NbtTag::Int(left), NbtTag::Int(right)) => left == right,
(NbtTag::Long(left), NbtTag::Long(right)) => left == right,
(NbtTag::Float(left), NbtTag::Float(right)) => left.to_bits() == right.to_bits(),
(NbtTag::Double(left), NbtTag::Double(right)) => left.to_bits() == right.to_bits(),
(NbtTag::ByteArray(left), NbtTag::ByteArray(right)) => left == right,
(NbtTag::String(left), NbtTag::String(right)) => left == right,
(NbtTag::List(left), NbtTag::List(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right)
.all(|(left, right)| nbt_tags_equal_for_write(left, right))
}
(NbtTag::Compound(left), NbtTag::Compound(right)) => {
left.len() == right.len()
&& left.iter().all(|(key, value)| {
right
.get(key)
.is_some_and(|right_value| nbt_tags_equal_for_write(value, right_value))
})
}
(NbtTag::IntArray(left), NbtTag::IntArray(right)) => left == right,
(NbtTag::LongArray(left), NbtTag::LongArray(right)) => left == right,
(NbtTag::ShortArray(left), NbtTag::ShortArray(right)) => left == right,
_ => false,
}
}
fn validate_string(value: &str, path: &str) -> Result<()> {
if value.len() > MAX_NBT_STRING_BYTES {
return Err(BedrockWorldError::Validation(format!(
"NBT string is too long at {path}"
)));
}
Ok(())
}
fn validate_array_len(len: usize, path: &str) -> Result<()> {
if len > MAX_NBT_CONTAINER_LENGTH {
return Err(BedrockWorldError::Validation(format!(
"NBT container is too large at {path}"
)));
}
if len > i32::MAX as usize {
return Err(BedrockWorldError::Validation(format!(
"NBT container length exceeds i32 at {path}"
)));
}
Ok(())
}
fn ensure_depth(depth: usize, tag_type: u8) -> Result<()> {
if depth > MAX_NBT_DEPTH {
return Err(BedrockWorldError::Nbt(format!(
"NBT nesting is too deep at tag {tag_type}"
)));
}
Ok(())
}
fn tag_discriminant(tag: &NbtTag) -> u8 {
match tag {
NbtTag::End => TAG_END,
NbtTag::Byte(_) => TAG_BYTE,
NbtTag::Short(_) => TAG_SHORT,
NbtTag::Int(_) => TAG_INT,
NbtTag::Long(_) => TAG_LONG,
NbtTag::Float(_) => TAG_FLOAT,
NbtTag::Double(_) => TAG_DOUBLE,
NbtTag::ByteArray(_) => TAG_BYTE_ARRAY,
NbtTag::String(_) => TAG_STRING,
NbtTag::List(_) => TAG_LIST,
NbtTag::Compound(_) => TAG_COMPOUND,
NbtTag::IntArray(_) => TAG_INT_ARRAY,
NbtTag::LongArray(_) => TAG_LONG_ARRAY,
NbtTag::ShortArray(_) => TAG_SHORT_ARRAY,
}
}
fn read_container_length(reader: &mut impl Read, field_name: &str) -> Result<usize> {
let len = read_i32(reader)?;
if len < 0 {
return Err(BedrockWorldError::Nbt(format!(
"{field_name} length cannot be negative"
)));
}
let len = len as usize;
if len > MAX_NBT_CONTAINER_LENGTH {
return Err(BedrockWorldError::Nbt(format!(
"{field_name} length is too large: {len}"
)));
}
Ok(len)
}
fn read_byte_length(reader: &mut impl Read, field_name: &str) -> Result<usize> {
let len = read_container_length(reader, field_name)?;
if len > MAX_NBT_BYTE_LENGTH {
return Err(BedrockWorldError::Nbt(format!(
"{field_name} byte length is too large: {len}"
)));
}
Ok(len)
}
fn read_u8(reader: &mut impl Read) -> Result<u8> {
let mut buf = [0; 1];
reader.read_exact(&mut buf)?;
Ok(buf[0])
}
fn read_i8(reader: &mut impl Read) -> Result<i8> {
Ok(read_u8(reader)? as i8)
}
fn read_i16(reader: &mut impl Read) -> Result<i16> {
let mut buf = [0; 2];
reader.read_exact(&mut buf)?;
Ok(i16::from_le_bytes(buf))
}
fn read_i32(reader: &mut impl Read) -> Result<i32> {
let mut buf = [0; 4];
reader.read_exact(&mut buf)?;
Ok(i32::from_le_bytes(buf))
}
fn read_i64(reader: &mut impl Read) -> Result<i64> {
let mut buf = [0; 8];
reader.read_exact(&mut buf)?;
Ok(i64::from_le_bytes(buf))
}
fn read_f32(reader: &mut impl Read) -> Result<f32> {
let mut buf = [0; 4];
reader.read_exact(&mut buf)?;
Ok(f32::from_le_bytes(buf))
}
fn read_f64(reader: &mut impl Read) -> Result<f64> {
let mut buf = [0; 8];
reader.read_exact(&mut buf)?;
Ok(f64::from_le_bytes(buf))
}
fn read_string(reader: &mut impl Read) -> Result<String> {
let mut len = [0; 2];
reader.read_exact(&mut len)?;
let len = u16::from_le_bytes(len) as usize;
let mut bytes = vec![0; len];
reader.read_exact(&mut bytes)?;
Ok(String::from_utf8(bytes)?)
}
fn write_string(writer: &mut impl Write, value: &str) -> Result<()> {
validate_string(value, "<string>")?;
writer.write_all(&(value.len() as u16).to_le_bytes())?;
writer.write_all(value.as_bytes())?;
Ok(())
}
fn write_i16(writer: &mut impl Write, value: i16) -> Result<()> {
writer.write_all(&value.to_le_bytes())?;
Ok(())
}
fn write_i32(writer: &mut impl Write, value: i32) -> Result<()> {
writer.write_all(&value.to_le_bytes())?;
Ok(())
}
fn write_i64(writer: &mut impl Write, value: i64) -> Result<()> {
writer.write_all(&value.to_le_bytes())?;
Ok(())
}
fn write_i32_len(writer: &mut impl Write, len: usize) -> Result<()> {
validate_array_len(len, "<len>")?;
write_i32(writer, len as i32)
}
fn parse_nbt_events(data: &[u8]) -> Result<Vec<NbtEvent<'_>>> {
let mut reader = SliceNbtReader::new(data);
let tag_type = reader.read_u8()?;
if tag_type != TAG_COMPOUND {
return Err(BedrockWorldError::Nbt(
"root NBT tag must be Compound".to_string(),
));
}
let name = reader.read_string_ref()?;
let mut events = Vec::new();
parse_event_payload(&mut reader, tag_type, Some(name), 0, &mut events)?;
Ok(events)
}
fn parse_event_payload<'a>(
reader: &mut SliceNbtReader<'a>,
tag_type: u8,
name: Option<&'a str>,
depth: usize,
events: &mut Vec<NbtEvent<'a>>,
) -> Result<()> {
ensure_depth(depth, tag_type)?;
match tag_type {
TAG_BYTE => events.push(NbtEvent::Byte {
name,
value: reader.read_i8()?,
}),
TAG_SHORT => events.push(NbtEvent::Short {
name,
value: reader.read_i16()?,
}),
TAG_INT => events.push(NbtEvent::Int {
name,
value: reader.read_i32()?,
}),
TAG_LONG => events.push(NbtEvent::Long {
name,
value: reader.read_i64()?,
}),
TAG_FLOAT => events.push(NbtEvent::Float {
name,
value: reader.read_f32()?,
}),
TAG_DOUBLE => events.push(NbtEvent::Double {
name,
value: reader.read_f64()?,
}),
TAG_BYTE_ARRAY => {
let len = reader.read_byte_length("ByteArray")?;
events.push(NbtEvent::ByteArray {
name,
bytes: reader.take(len)?,
});
}
TAG_STRING => events.push(NbtEvent::String {
name,
value: reader.read_string_ref()?,
}),
TAG_LIST => {
let element_type = reader.read_u8()?;
let len = reader.read_container_length("List")?;
events.push(NbtEvent::BeginList {
name,
element_type,
len,
});
for _ in 0..len {
parse_event_payload(reader, element_type, None, depth + 1, events)?;
}
events.push(NbtEvent::EndList);
}
TAG_COMPOUND => {
events.push(NbtEvent::BeginCompound { name });
loop {
let child_type = reader.read_u8()?;
if child_type == TAG_END {
break;
}
let child_name = reader.read_string_ref()?;
parse_event_payload(reader, child_type, Some(child_name), depth + 1, events)?;
}
events.push(NbtEvent::EndCompound);
}
TAG_INT_ARRAY => {
let len = reader.read_container_length("IntArray")?;
events.push(NbtEvent::IntArray {
name,
bytes: reader.take_array_bytes(len, 4, "IntArray")?,
len,
});
}
TAG_LONG_ARRAY => {
let len = reader.read_container_length("LongArray")?;
events.push(NbtEvent::LongArray {
name,
bytes: reader.take_array_bytes(len, 8, "LongArray")?,
len,
});
}
TAG_SHORT_ARRAY => {
let len = reader.read_container_length("ShortArray")?;
events.push(NbtEvent::ShortArray {
name,
bytes: reader.take_array_bytes(len, 2, "ShortArray")?,
len,
});
}
TAG_END => {}
_ => {
return Err(BedrockWorldError::Nbt(format!(
"unknown NBT tag type: {tag_type}"
)));
}
}
Ok(())
}
struct SliceNbtReader<'a> {
data: &'a [u8],
offset: usize,
}
impl<'a> SliceNbtReader<'a> {
const fn new(data: &'a [u8]) -> Self {
Self { data, offset: 0 }
}
fn take(&mut self, len: usize) -> Result<&'a [u8]> {
let end = self
.offset
.checked_add(len)
.ok_or_else(|| BedrockWorldError::Nbt("NBT slice range overflowed".to_string()))?;
let bytes = self
.data
.get(self.offset..end)
.ok_or_else(|| BedrockWorldError::Nbt("NBT payload is truncated".to_string()))?;
self.offset = end;
Ok(bytes)
}
fn read_u8(&mut self) -> Result<u8> {
Ok(*self
.take(1)?
.first()
.ok_or_else(|| BedrockWorldError::Nbt("NBT byte is truncated".to_string()))?)
}
fn read_i8(&mut self) -> Result<i8> {
Ok(self.read_u8()? as i8)
}
fn read_i16(&mut self) -> Result<i16> {
let bytes: [u8; 2] = self
.take(2)?
.try_into()
.map_err(|_| BedrockWorldError::Nbt("NBT i16 is truncated".to_string()))?;
Ok(i16::from_le_bytes(bytes))
}
fn read_i32(&mut self) -> Result<i32> {
let bytes: [u8; 4] = self
.take(4)?
.try_into()
.map_err(|_| BedrockWorldError::Nbt("NBT i32 is truncated".to_string()))?;
Ok(i32::from_le_bytes(bytes))
}
fn read_i64(&mut self) -> Result<i64> {
let bytes: [u8; 8] = self
.take(8)?
.try_into()
.map_err(|_| BedrockWorldError::Nbt("NBT i64 is truncated".to_string()))?;
Ok(i64::from_le_bytes(bytes))
}
fn read_f32(&mut self) -> Result<f32> {
Ok(f32::from_bits(self.read_i32()? as u32))
}
fn read_f64(&mut self) -> Result<f64> {
Ok(f64::from_bits(self.read_i64()? as u64))
}
fn read_string_ref(&mut self) -> Result<&'a str> {
let len_bytes: [u8; 2] = self
.take(2)?
.try_into()
.map_err(|_| BedrockWorldError::Nbt("NBT string length is truncated".to_string()))?;
let len = u16::from_le_bytes(len_bytes) as usize;
let bytes = self.take(len)?;
Ok(std::str::from_utf8(bytes)?)
}
fn read_container_length(&mut self, name: &str) -> Result<usize> {
let len = self.read_i32()?;
if len < 0 {
return Err(BedrockWorldError::Nbt(format!(
"{name} has negative length"
)));
}
let len = usize::try_from(len)
.map_err(|_| BedrockWorldError::Nbt(format!("{name} length overflow")))?;
if len > MAX_NBT_CONTAINER_LENGTH {
return Err(BedrockWorldError::Nbt(format!("{name} is too large")));
}
Ok(len)
}
fn read_byte_length(&mut self, name: &str) -> Result<usize> {
let len = self.read_container_length(name)?;
if len > MAX_NBT_BYTE_LENGTH {
return Err(BedrockWorldError::Nbt(format!("{name} is too large")));
}
Ok(len)
}
fn take_array_bytes(&mut self, len: usize, width: usize, name: &str) -> Result<&'a [u8]> {
let byte_len = len
.checked_mul(width)
.ok_or_else(|| BedrockWorldError::Nbt(format!("{name} byte length overflow")))?;
self.take(byte_len)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_nbt_roundtrips_little_endian_tags() {
let mut root = IndexMap::new();
root.insert("Name".to_string(), NbtTag::String("World".to_string()));
root.insert("Seed".to_string(), NbtTag::Long(-42));
root.insert(
"List".to_string(),
NbtTag::List(vec![NbtTag::Int(1), NbtTag::Int(2)]),
);
let tag = NbtTag::Compound(root);
let bytes = serialize_root_nbt(&tag).expect("serialize");
let parsed = parse_root_nbt(&bytes).expect("parse");
assert!(nbt_tags_equal_for_write(&tag, &parsed));
}
#[test]
fn mixed_list_is_rejected_before_write() {
let mut root = IndexMap::new();
root.insert(
"Bad".to_string(),
NbtTag::List(vec![NbtTag::Int(1), NbtTag::String("bad".to_string())]),
);
assert!(validate_root_nbt_for_write(&NbtTag::Compound(root)).is_err());
}
#[test]
fn root_parser_reports_consumed_bytes() {
let mut root = IndexMap::new();
root.insert("Name".to_string(), NbtTag::String("First".to_string()));
let bytes = serialize_root_nbt(&NbtTag::Compound(root)).expect("serialize");
let mut combined = bytes.clone();
combined.extend_from_slice(&bytes);
let (_, consumed) = parse_root_nbt_with_consumed(&combined).expect("parse");
assert_eq!(consumed, bytes.len());
}
#[test]
fn nbt_view_emits_borrowed_events_without_owned_dom() {
let mut root = IndexMap::new();
root.insert("Name".to_string(), NbtTag::String("Borrowed".to_string()));
root.insert("Seed".to_string(), NbtTag::Long(42));
root.insert("Bytes".to_string(), NbtTag::ByteArray(vec![1, 2, 3]));
let bytes = serialize_root_nbt(&NbtTag::Compound(root)).expect("serialize");
let events = NbtReader::new(&bytes).view().events().expect("events");
assert!(matches!(
events.first(),
Some(NbtEvent::BeginCompound { name: Some("") })
));
assert!(events.iter().any(|event| matches!(
event,
NbtEvent::String {
name: Some("Name"),
value: "Borrowed"
}
)));
assert!(events.iter().any(|event| matches!(
event,
NbtEvent::Long {
name: Some("Seed"),
value: 42
}
)));
assert!(events.iter().any(|event| matches!(
event,
NbtEvent::ByteArray {
name: Some("Bytes"),
bytes
} if *bytes == [1, 2, 3]
)));
}
#[test]
fn consecutive_root_parser_advances_between_compounds() {
let mut root = IndexMap::new();
root.insert("Name".to_string(), NbtTag::String("First".to_string()));
let bytes = serialize_root_nbt(&NbtTag::Compound(root)).expect("serialize");
let mut combined = bytes.clone();
combined.extend_from_slice(&bytes);
let tags = parse_consecutive_root_nbt(&combined).expect("parse consecutive");
assert_eq!(tags.len(), 2);
}
}