Source code for scfile.io.nbt
"""Structured I/O extensions for NBT."""
from enum import IntEnum
from typing import assert_never
from scfile.content.base import DocumentValue
from scfile.enums import ByteOrder, F
from scfile.exceptions import BinaryStructureError
from .base import StructReader, StructWriter
[docs]
class Tag(IntEnum):
END = 0
BYTE = 1
SHORT = 2
INT = 3
LONG = 4
FLOAT = 5
DOUBLE = 6
BYTE_ARRAY = 7
STRING = 8
LIST = 9
COMPOUND = 10
INT_ARRAY = 11
LONG_ARRAY = 12
[docs]
class NbtReader(StructReader):
order: ByteOrder = ByteOrder.BIG
[docs]
def document(self) -> DocumentValue:
"""Read one NBT document."""
tag = self.tag()
if tag == Tag.END:
return None
self.string(limit=None) # Skip root name
try:
return self.parse(tag)
except RecursionError as error:
raise BinaryStructureError(
location=self.location,
offset=self.tell(),
) from error
[docs]
def tag(self) -> Tag:
"""Read one NBT tag type."""
try:
return Tag(self.value(F.I8))
except ValueError:
raise BinaryStructureError(
location=self.location,
offset=self.tell(),
) from None
[docs]
def parse(
self,
tag: Tag,
) -> DocumentValue:
"""Read the payload of a known NBT tag."""
match tag:
case Tag.END:
return None
case Tag.BYTE:
return self.value(F.I8)
case Tag.SHORT:
return self.value(F.I16)
case Tag.INT:
return self.value(F.I32)
case Tag.LONG:
return self.value(F.I64)
case Tag.FLOAT:
return self.value(F.F32)
case Tag.DOUBLE:
return self.value(F.F64)
case Tag.BYTE_ARRAY:
return self._byte_array()
case Tag.STRING:
return self.string(limit=None)
case Tag.LIST:
return self._list()
case Tag.COMPOUND:
return self._compound()
case Tag.INT_ARRAY:
return self._int_array()
case Tag.LONG_ARRAY:
return self._long_array()
case _:
assert_never(tag)
def _byte_array(self) -> bytes:
return self.read_exact(self._length())
def _list(self) -> list[DocumentValue]:
tag = self.tag()
return [self.parse(tag) for _ in range(self._length())]
def _int_array(self) -> list[DocumentValue]:
return self.array(F.I32, self._length()).tolist()
def _long_array(self) -> list[DocumentValue]:
return self.array(F.I64, self._length()).tolist()
def _length(self) -> int:
length = self.value(F.I32)
if length < 0:
raise BinaryStructureError(
location=self.location,
offset=self.tell(),
)
return length
def _compound(self) -> dict[str, DocumentValue]:
data: dict[str, DocumentValue] = {}
while (tag := self.tag()) != Tag.END:
name = self.string(limit=None)
data[name] = self.parse(tag)
return data
[docs]
class NbtWriter(StructWriter):
"""Write NBT data to a binary stream."""
order: ByteOrder = ByteOrder.BIG
[docs]
def tag(
self,
tag: Tag,
name: bytes = b"",
) -> None:
"""Write a named NBT tag header."""
self.write(tag.header(name))
[docs]
def list(
self,
tag: Tag,
length: int,
) -> None:
"""Write an NBT list element type and length."""
self.value(F.I8, tag)
self.value(F.I32, length)
[docs]
def end(self) -> None:
"""Write an NBT compound terminator."""
self.value(F.I8, Tag.END)