docs: document pumpkin-nbt (#2648)

This commit is contained in:
Giovanni Giordano
2026-07-30 14:13:47 +02:00
committed by GitHub
parent a2263c55cc
commit 0fe896892f
7 changed files with 265 additions and 52 deletions

View File

@@ -1,3 +1,5 @@
//! Storage and convenience methods for NBT compound tags.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
@@ -11,6 +13,9 @@ use std::collections::hash_map::IntoIter;
use std::io::ErrorKind;
#[macro_export]
/// Creates an [`NbtTag::Compound`](crate::tag::NbtTag::Compound) from key-value pairs.
///
/// The macro also accepts an empty invocation to create an empty compound tag.
macro_rules! nbt_compound_tag {
{ $($key:literal : $tag:expr),+ $(,)* } => {
{
@@ -32,10 +37,12 @@ macro_rules! nbt_compound_tag {
///
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NbtCompound {
/// Tags in the compound, indexed by their names.
pub child_tags: HashMap<Box<str>, NbtTag>,
}
impl NbtCompound {
/// Creates an empty compound.
#[must_use]
pub fn new() -> Self {
Self {
@@ -43,6 +50,7 @@ impl NbtCompound {
}
}
/// Advances a reader past a compound's payload without allocating its tags.
pub fn skip_content<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<(), Error> {
loop {
let tag_id = match reader.get_u8() {
@@ -64,6 +72,7 @@ impl NbtCompound {
Ok(())
}
/// Deserializes a compound payload, starting after the compound's name.
pub fn deserialize_content<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<Self, Error> {
let mut compound = Self::new();
@@ -87,6 +96,7 @@ impl NbtCompound {
Ok(compound)
}
/// Serializes the compound's entries followed by an end tag.
pub fn serialize_content<W: NbtWriteHelper>(self, w: &mut W) -> Result<(), Error> {
for (name, tag) in self.child_tags {
w.write_u8(tag.get_type_id())?;
@@ -97,52 +107,66 @@ impl NbtCompound {
Ok(())
}
/// Returns `true` when the compound contains no child tags.
#[must_use]
pub fn is_empty(&self) -> bool {
self.child_tags.is_empty()
}
/// Inserts a tag when `name` is not already present.
///
/// Existing entries are left unchanged.
pub fn put(&mut self, name: &str, value: impl Into<NbtTag>) {
if !self.child_tags.contains_key(name) {
self.child_tags.insert(name.into(), value.into());
}
}
/// Inserts a string tag when `name` is not already present.
pub fn put_string(&mut self, name: &str, value: String) {
self.put(name, NbtTag::String(value.into()));
}
/// Inserts a list tag when `name` is not already present.
pub fn put_list(&mut self, name: &str, value: Vec<NbtTag>) {
self.put(name, NbtTag::List(value));
}
/// Inserts a byte tag when `name` is not already present.
pub fn put_byte(&mut self, name: &str, value: i8) {
self.put(name, NbtTag::Byte(value));
}
/// Inserts a boolean encoded as a byte tag when `name` is not already present.
pub fn put_bool(&mut self, name: &str, value: bool) {
self.put(name, NbtTag::Byte(i8::from(value)));
}
/// Inserts a short tag when `name` is not already present.
pub fn put_short(&mut self, name: &str, value: i16) {
self.put(name, NbtTag::Short(value));
}
/// Inserts an integer tag when `name` is not already present.
pub fn put_int(&mut self, name: &str, value: i32) {
self.put(name, NbtTag::Int(value));
}
/// Inserts a long tag when `name` is not already present.
pub fn put_long(&mut self, name: &str, value: i64) {
self.put(name, NbtTag::Long(value));
}
/// Inserts a float tag when `name` is not already present.
pub fn put_float(&mut self, name: &str, value: f32) {
self.put(name, NbtTag::Float(value));
}
/// Inserts a double tag when `name` is not already present.
pub fn put_double(&mut self, name: &str, value: f64) {
self.put(name, NbtTag::Double(value));
}
/// Inserts a compound tag when `name` is not already present.
pub fn put_compound(&mut self, name: &str, value: Self) {
self.put(name, NbtTag::Compound(value));
}
@@ -162,73 +186,87 @@ impl NbtCompound {
);
}
/// Returns the named byte value, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_byte(&self, name: &str) -> Option<i8> {
self.get(name).and_then(super::tag::NbtTag::extract_byte)
}
/// Returns the named tag.
#[inline]
#[must_use]
pub fn get(&self, name: &str) -> Option<&NbtTag> {
self.child_tags.get(name)
}
/// Returns whether the compound contains `name`.
#[inline]
#[must_use]
pub fn has(&self, name: &str) -> bool {
self.child_tags.contains_key(name)
}
/// Returns the named short value, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_short(&self, name: &str) -> Option<i16> {
self.get(name).and_then(super::tag::NbtTag::extract_short)
}
/// Returns the named integer value, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_int(&self, name: &str) -> Option<i32> {
self.get(name).and_then(super::tag::NbtTag::extract_int)
}
/// Returns the named long value, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_long(&self, name: &str) -> Option<i64> {
self.get(name).and_then(super::tag::NbtTag::extract_long)
}
/// Returns the named float value, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_float(&self, name: &str) -> Option<f32> {
self.get(name).and_then(super::tag::NbtTag::extract_float)
}
/// Returns the named double value, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_double(&self, name: &str) -> Option<f64> {
self.get(name).and_then(super::tag::NbtTag::extract_double)
}
/// Returns the named byte as a boolean, where zero is `false`.
#[must_use]
pub fn get_bool(&self, name: &str) -> Option<bool> {
self.get(name).and_then(super::tag::NbtTag::extract_bool)
}
/// Returns the named string, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_string(&self, name: &str) -> Option<&str> {
self.get(name).and_then(|tag| tag.extract_string())
}
/// Returns the named list, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_list(&self, name: &str) -> Option<&[NbtTag]> {
self.get(name).and_then(|tag| tag.extract_list())
}
/// Returns the named compound, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_compound(&self, name: &str) -> Option<&Self> {
self.get(name).and_then(|tag| tag.extract_compound())
}
/// Returns the named integer array, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_int_array(&self, name: &str) -> Option<&[i32]> {
self.get(name).and_then(|tag| tag.extract_int_array())
}
/// Returns the named long array, or `None` if the tag is absent or has another type.
#[must_use]
pub fn get_long_array(&self, name: &str) -> Option<&[i64]> {
self.get(name).and_then(|tag| tag.extract_long_array())

View File

@@ -1,3 +1,5 @@
//! Deserialization from Java Edition, unnamed network, and Bedrock NBT.
use std::borrow::Cow;
use std::cell::RefCell;
use std::io::{Cursor, Seek, SeekFrom};
@@ -10,9 +12,14 @@ use io::Read;
use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, forward_to_deserialize_any};
/// Result type returned by NBT deserialization operations.
pub type Result<T> = std::result::Result<T, Error>;
thread_local! {
/// Tag ID of the sequence currently visited by Serde.
///
/// This is used to preserve the distinction between NBT lists and the
/// three specialized NBT array types.
pub static CURR_VISITOR_LIST_TYPE: RefCell<Option<u8>> = const { std::cell::RefCell::new(None) };
}
@@ -26,15 +33,28 @@ pub(super) fn set_curr_visitor_seq_list_id(tag: Option<u8>) {
});
}
/// Byte source used by NBT read helpers.
///
/// Implementations may return borrowed strings and byte arrays when the
/// underlying storage permits it.
pub trait NbtDataSource<'a> {
/// Reads one unsigned byte.
fn read_u8(&mut self) -> Result<u8>;
/// Fills `buf` with bytes from the source.
fn read_bytes(&mut self, buf: &mut [u8]) -> Result<()>;
/// Moves the current position by `offset` bytes.
fn seek_relative(&mut self, offset: i64) -> Result<()>;
/// Reads and decodes a string payload of `len` bytes.
fn read_string(&mut self, len: usize) -> Result<Cow<'a, str>>;
/// Reads a byte-array payload of `len` elements.
fn read_byte_array(&mut self, len: usize) -> Result<Cow<'a, [i8]>>;
}
pub struct NbtStreamReader<R>(pub R);
/// Adapts a [`Read`] and [`Seek`] stream into an [`NbtDataSource`].
pub struct NbtStreamReader<R>(
/// Wrapped input stream.
pub R,
);
impl<'a, R: Read + Seek> NbtDataSource<'a> for NbtStreamReader<R> {
fn read_u8(&mut self) -> Result<u8> {
@@ -191,63 +211,88 @@ impl<'a> NbtDataSource<'a> for Cursor<Vec<u8>> {
}
}
/// Format-specific primitive reader used by the NBT parser.
pub trait NbtReadHelper<'a> {
/// Underlying byte source.
type Reader: NbtDataSource<'a>;
/// Returns the underlying byte source.
fn reader(&mut self) -> &mut Self::Reader;
/// Advances by `count` bytes.
fn skip_bytes(&mut self, count: i64) -> Result<()> {
self.reader().seek_relative(count)
}
/// Advances past an unsigned byte.
fn skip_u8(&mut self) -> Result<()> {
self.skip_bytes(1)
}
/// Advances past a signed byte.
fn skip_i8(&mut self) -> Result<()> {
self.skip_bytes(1)
}
/// Advances past a 16-bit signed integer.
fn skip_i16(&mut self) -> Result<()> {
self.skip_bytes(2)
}
/// Advances past a 32-bit signed integer.
fn skip_i32(&mut self) -> Result<()> {
self.skip_bytes(4)
}
/// Advances past a 64-bit signed integer.
fn skip_i64(&mut self) -> Result<()> {
self.skip_bytes(8)
}
/// Advances past a 32-bit floating-point number.
fn skip_f32(&mut self) -> Result<()> {
self.skip_bytes(4)
}
/// Advances past a 64-bit floating-point number.
fn skip_f64(&mut self) -> Result<()> {
self.skip_bytes(8)
}
/// Advances past a length-prefixed string.
fn skip_string(&mut self) -> Result<()>;
/// Reads an unsigned byte.
fn get_u8(&mut self) -> Result<u8>;
/// Reads a signed byte.
fn get_i8(&mut self) -> Result<i8>;
/// Reads a 16-bit signed integer.
fn get_i16(&mut self) -> Result<i16>;
/// Reads a 32-bit signed integer.
fn get_i32(&mut self) -> Result<i32>;
/// Reads a 64-bit signed integer.
fn get_i64(&mut self) -> Result<i64>;
/// Reads a 32-bit floating-point number.
fn get_f32(&mut self) -> Result<f32>;
/// Reads a 64-bit floating-point number.
fn get_f64(&mut self) -> Result<f64>;
/// Reads a length-prefixed string.
fn get_string(&mut self) -> Result<Cow<'a, str>>;
/// Reads a byte array with the supplied element count.
fn get_byte_array(&mut self, len: usize) -> Result<Cow<'a, [i8]>>;
}
/// Reads Java Edition NBT primitives using big-endian numeric encoding.
pub struct NbtReadHelperJava<D> {
reader: D,
}
impl<D> NbtReadHelperJava<D> {
/// Creates a Java Edition reader over `r`.
pub const fn new(r: D) -> Self {
Self { reader: r }
}
}
/// Reads Bedrock network NBT primitives using little-endian and variable-length encoding.
pub struct NbtReadHelperBedrock<D> {
reader: D,
}
impl<D> NbtReadHelperBedrock<D> {
/// Creates a Bedrock network reader over `r`.
pub const fn new(r: D) -> Self {
Self { reader: r }
}
@@ -409,6 +454,7 @@ impl<'a, D: NbtDataSource<'a>> NbtReadHelper<'a> for NbtReadHelperBedrock<D> {
}
}
/// A Serde deserializer backed by a format-specific NBT reader.
pub struct Deserializer<R> {
input: R,
tag_to_deserialize_stack: Option<u8>,
@@ -417,6 +463,10 @@ pub struct Deserializer<R> {
}
impl<R> Deserializer<R> {
/// Creates a deserializer.
///
/// When `is_named` is `true`, the root compound name is consumed from the
/// input. Unnamed network NBT must pass `false`.
pub const fn new(input: R, is_named: bool) -> Self {
Self {
input,
@@ -427,37 +477,43 @@ impl<R> Deserializer<R> {
}
}
/// Deserializes struct using Serde Deserializer from normal NBT
/// Deserializes a value from named Java Edition NBT.
pub fn from_bytes<'a, T: Deserialize<'a>>(r: impl Read + Seek) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(NbtStreamReader(r)), true);
T::deserialize(&mut deserializer)
}
/// Deserializes struct using Serde Deserializer from network NBT
/// Deserializes a value from unnamed Java Edition network NBT.
pub fn from_bytes_unnamed<'a, T: Deserialize<'a>>(r: impl Read + Seek) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(NbtStreamReader(r)), false);
T::deserialize(&mut deserializer)
}
/// Deserializes struct using Serde Deserializer from Bedrock network NBT
/// Deserializes a value from named Bedrock network NBT.
pub fn from_bytes_bedrock<'a, T: Deserialize<'a>>(r: impl Read + Seek) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperBedrock::new(NbtStreamReader(r)), true);
T::deserialize(&mut deserializer)
}
/// Deserializes struct using Serde Deserializer from a normal NBT slice (zero-allocation)
/// Deserializes a value from a named Java Edition NBT slice.
///
/// Strings and byte arrays may borrow directly from `slice`.
pub fn from_slice<'a, T: Deserialize<'a>>(slice: &'a [u8]) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(Cursor::new(slice)), true);
T::deserialize(&mut deserializer)
}
/// Deserializes struct using Serde Deserializer from a network NBT slice (zero-allocation)
/// Deserializes a value from an unnamed Java Edition network NBT slice.
///
/// Strings and byte arrays may borrow directly from `slice`.
pub fn from_slice_unnamed<'a, T: Deserialize<'a>>(slice: &'a [u8]) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperJava::new(Cursor::new(slice)), false);
T::deserialize(&mut deserializer)
}
/// Deserializes struct using Serde Deserializer from a Bedrock network NBT slice (zero-allocation)
/// Deserializes a value from a named Bedrock network NBT slice.
///
/// Strings and byte arrays may borrow directly from `slice`.
pub fn from_slice_bedrock<'a, T: Deserialize<'a>>(slice: &'a [u8]) -> Result<T> {
let mut deserializer = Deserializer::new(NbtReadHelperBedrock::new(Cursor::new(slice)), true);
T::deserialize(&mut deserializer)

View File

@@ -1,3 +1,34 @@
//! Reading, writing, and manipulating Minecraft's Named Binary Tag (NBT) data.
//!
//! The crate supports the standard Java Edition representation, unnamed network
//! NBT, Bedrock network NBT, and gzip-compressed NBT. Data can be handled either
//! as [`Nbt`] and [`NbtCompound`] values or through Serde with [`to_bytes`] and
//! [`from_slice`].
//!
//! # Serde round trip
//!
//! ```
//! use pumpkin_nbt::{from_slice, to_bytes};
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
//! struct Player {
//! name: String,
//! health: i16,
//! }
//!
//! let player = Player {
//! name: "Steve".to_owned(),
//! health: 20,
//! };
//! let mut encoded = Vec::new();
//! to_bytes(&player, &mut encoded)?;
//!
//! let decoded: Player = from_slice(&encoded)?;
//! assert_eq!(decoded, player);
//! # Ok::<(), pumpkin_nbt::Error>(())
//! ```
use std::{
fmt::Display,
io::{self, Write},
@@ -11,11 +42,17 @@ use serializer::{NbtWriteHelper, NbtWriteHelperBedrock, NbtWriteHelperJava};
use tag::NbtTag;
use thiserror::Error;
/// Compound-tag storage and construction helpers.
pub mod compound;
/// Serde and low-level NBT deserialization support.
pub mod deserializer;
/// Reading and writing gzip-compressed NBT.
pub mod nbt_compress;
/// Integration with Pumpkin's dynamic codec operations.
pub mod nbt_ops;
/// Serde and low-level NBT serialization support.
pub mod serializer;
/// The individual NBT tag types.
pub mod tag;
pub use compound::NbtCompound;
@@ -27,44 +64,70 @@ pub use serializer::{to_bytes, to_bytes_named, to_bytes_unnamed};
// This NBT crate is inspired from CrabNBT
/// Numeric identifier for an end tag.
pub const END_ID: u8 = 0x00;
/// Numeric identifier for a byte tag.
pub const BYTE_ID: u8 = 0x01;
/// Numeric identifier for a short tag.
pub const SHORT_ID: u8 = 0x02;
/// Numeric identifier for an integer tag.
pub const INT_ID: u8 = 0x03;
/// Numeric identifier for a long tag.
pub const LONG_ID: u8 = 0x04;
/// Numeric identifier for a float tag.
pub const FLOAT_ID: u8 = 0x05;
/// Numeric identifier for a double tag.
pub const DOUBLE_ID: u8 = 0x06;
/// Numeric identifier for a byte-array tag.
pub const BYTE_ARRAY_ID: u8 = 0x07;
/// Numeric identifier for a string tag.
pub const STRING_ID: u8 = 0x08;
/// Numeric identifier for a list tag.
pub const LIST_ID: u8 = 0x09;
/// Numeric identifier for a compound tag.
pub const COMPOUND_ID: u8 = 0x0A;
/// Numeric identifier for an integer-array tag.
pub const INT_ARRAY_ID: u8 = 0x0B;
/// Numeric identifier for a long-array tag.
pub const LONG_ARRAY_ID: u8 = 0x0C;
/// Maximum number of elements accepted when decoding a list or array.
pub const MAX_ARRAY_LENGTH: usize = 2_000_000;
/// Errors produced while reading, writing, or converting NBT data.
#[derive(Error, Debug)]
pub enum Error {
/// The root tag was not a compound tag and contains the reported tag ID.
#[error("The root tag of the NBT file is not a compound tag. Received tag id: {0}")]
NoRootCompound(u8),
/// A tag ID not defined by the NBT format was encountered.
#[error("Encountered an unknown NBT tag id: {0}.")]
UnknownTagId(u8),
/// A Java CESU-8 string could not be decoded.
#[error("Failed to Cesu 8 Decode")]
Cesu8DecodingError,
/// A string could not be decoded as UTF-8.
#[error("Failed to UTF-8 Decode")]
Utf8DecodingError,
/// Serde reported an invalid value or serializer state.
#[error("Serde error: {0}")]
SerdeError(String),
/// The requested Rust type has no NBT representation.
#[error("NBT doesn't support this type: {0}")]
UnsupportedType(String),
/// The underlying reader or writer returned an I/O error.
#[error("NBT reading was cut short: {0}")]
Incomplete(io::Error),
/// A list or array declared a negative element count.
#[error("Negative list length: {0}")]
NegativeLength(i32),
/// A string, list, or array exceeded the supported length.
#[error("Length too large: {0}")]
LargeLength(usize),
/// A Bedrock variable-length integer exceeded its maximum encoded size.
#[error("Failed to decode varint - value too large")]
VarIntTooLarge,
/// A Bedrock variable-length long exceeded its maximum encoded size.
#[error("Failed to decode varlong - value too large")]
VarLongTooLarge,
}
@@ -81,13 +144,17 @@ impl de::Error for Error {
}
}
/// A complete NBT document containing a named root compound.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Nbt {
/// Name stored alongside the root compound.
pub name: String,
/// Root compound containing the document's tags.
pub root_tag: NbtCompound,
}
impl Nbt {
/// Creates a document from a root name and compound.
#[must_use]
pub const fn new(name: String, tag: NbtCompound) -> Self {
Self {
@@ -96,6 +163,9 @@ impl Nbt {
}
}
/// Reads a named NBT document from a format-specific reader.
///
/// Returns [`Error::NoRootCompound`] when the first tag is not a compound.
pub fn read<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<Self, Error> {
let tag_type_id = reader.get_u8()?;
@@ -109,7 +179,9 @@ impl Nbt {
})
}
/// Reads an NBT tag that doesn't contain the name of the root `Compound`.
/// Reads an NBT document that omits the root compound's name.
///
/// The returned document has an empty [`Self::name`].
pub fn read_unnamed<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<Self, Error> {
let tag_type_id = reader.get_u8()?;
@@ -123,6 +195,7 @@ impl Nbt {
})
}
/// Serializes this document using the Java Edition NBT representation.
#[must_use]
pub fn write(self) -> Bytes {
let mut bytes = Vec::new();
@@ -136,6 +209,7 @@ impl Nbt {
bytes.into()
}
/// Serializes this document using the Bedrock network NBT representation.
#[must_use]
pub fn write_bedrock(self) -> Bytes {
let mut bytes = Vec::new();
@@ -149,17 +223,19 @@ impl Nbt {
bytes.into()
}
/// Writes this document in the Java Edition representation.
pub fn write_to_writer<W: Write>(self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.write())?;
Ok(())
}
/// Writes this document in the Bedrock network representation.
pub fn write_to_writer_bedrock<W: Write>(self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.write_bedrock())?;
Ok(())
}
/// Writes an NBT tag without a root `Compound` name.
/// Serializes this document without the root compound's name.
#[must_use]
pub fn write_unnamed(self) -> Bytes {
let mut bytes = Vec::new();
@@ -171,6 +247,7 @@ impl Nbt {
bytes.into()
}
/// Writes this document without the root compound's name.
pub fn write_unnamed_to_writer<W: Write>(self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&self.write_unnamed())?;
Ok(())
@@ -215,6 +292,9 @@ pub(crate) const NBT_BYTE_ARRAY_TAG: &str = "__nbt_byte_array";
macro_rules! impl_array {
($name:ident, $variant:expr) => {
#[doc = "Serializes a sequence using its specialized NBT array representation."]
#[doc = ""]
#[doc = "Use this function with Serde's `serialize_with` field attribute."]
pub fn $name<T: serde::Serialize, S: serde::Serializer>(
input: T,
serializer: S,

View File

@@ -1,17 +1,13 @@
//! Helpers for reading and writing gzip-compressed NBT data.
use crate::deserializer::NbtReadHelperJava;
use crate::{Error, Nbt, NbtCompound, deserializer, serializer};
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use std::io::{Cursor, Read, Seek, Write};
/// Reads a `GZipped` NBT compound tag from any reader.
/// Reads a gzip-compressed, named NBT compound from a seekable reader.
///
/// # Arguments
///
/// * `input` - Any type implementing the Read trait containing `GZipped` NBT data
///
/// # Returns
///
/// A Result containing either the parsed `NbtCompound` or an Error
/// Decompressed data is limited to 64 MiB.
pub fn read_gzip_compound_tag(input: impl Read + Seek) -> Result<NbtCompound, Error> {
// Create a GZip decoder and directly chain it to the NBT reader
let mut decoder = GzDecoder::new(input).take(64 * 1024 * 1024); // 64 MB limit
@@ -24,18 +20,9 @@ pub fn read_gzip_compound_tag(input: impl Read + Seek) -> Result<NbtCompound, Er
Ok(nbt.root_tag)
}
/// Writes an NBT compound tag with `GZip` compression.
/// Writes a named NBT compound with gzip compression.
///
/// This function takes an `NbtCompound` and writes it as a `GZipped` byte vector.
///
/// # Arguments
///
/// * `compound` - The `NbtCompound` to serialize and compress
/// * `output` - Any type implementing the Write trait where the compressed data will be written
///
/// # Returns
///
/// A Result containing either the compressed data as a byte vector or an Error
/// The root name is written as an empty string.
pub fn write_gzip_compound_tag(compound: NbtCompound, output: impl Write) -> Result<(), Error> {
// Create a GZip encoder that writes to the output
let mut encoder = GzEncoder::new(output, Compression::default());
@@ -51,22 +38,18 @@ pub fn write_gzip_compound_tag(compound: NbtCompound, output: impl Write) -> Res
Ok(())
}
/// Convenience function that returns compressed bytes
/// Serializes a named NBT compound into a gzip-compressed byte vector.
///
/// The root name is written as an empty string.
pub fn write_gzip_compound_tag_to_bytes(compound: NbtCompound) -> Result<Vec<u8>, Error> {
let mut buffer = Vec::new();
write_gzip_compound_tag(compound, &mut buffer)?;
Ok(buffer)
}
/// Reads a `GZipped` NBT structure into a Rust type.
/// Deserializes a value from gzip-compressed, named Java Edition NBT.
///
/// # Arguments
///
/// * `input` - Any type implementing the Read trait containing `GZipped` NBT data
///
/// # Returns
///
/// A Result containing either the deserialized type or an Error
/// Decompressed data is limited to 64 MiB.
pub fn from_gzip_bytes<'a, T: serde::Deserialize<'a>, R: Read>(input: R) -> Result<T, Error> {
// Create a GZip decoder and directly use it for deserialization
let mut decoder = GzDecoder::new(input).take(64 * 1024 * 1024); // 64 MB limit
@@ -75,16 +58,9 @@ pub fn from_gzip_bytes<'a, T: serde::Deserialize<'a>, R: Read>(input: R) -> Resu
deserializer::from_bytes(Cursor::new(buf))
}
/// Writes a Rust type as `GZipped` NBT to any writer.
/// Serializes a value as gzip-compressed Java Edition NBT.
///
/// # Arguments
///
/// * `value` - The value to serialize and compress
/// * `output` - Any type implementing the Write trait where the compressed data will be written
///
/// # Returns
///
/// A Result indicating success or an Error
/// The root name is written as an empty string.
pub fn to_gzip_bytes<T: serde::Serialize, W: Write>(value: &T, output: W) -> Result<(), Error> {
// Create a GZip encoder that writes to the output
let encoder = GzEncoder::new(output, Compression::default());
@@ -93,7 +69,9 @@ pub fn to_gzip_bytes<T: serde::Serialize, W: Write>(value: &T, output: W) -> Res
serializer::to_bytes(value, encoder)
}
/// Convenience function that returns compressed bytes
/// Serializes a value into a gzip-compressed Java Edition NBT byte vector.
///
/// The root name is written as an empty string.
pub fn to_gzip_bytes_vec<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, Error> {
let mut buffer = Vec::new();
to_gzip_bytes(value, &mut buffer)?;

View File

@@ -1,3 +1,5 @@
//! NBT support for Pumpkin's dynamic serialization operations.
use crate::compound::NbtCompound;
use crate::tag::NbtTag;
use pumpkin_codecs::DataResult;
@@ -11,7 +13,7 @@ use std::iter::Map;
use std::vec::IntoIter;
use tracing::warn;
/// A [`DynamicOps`] to serialize to/deserialize from NBT data.
/// A [`DynamicOps`] implementation that represents values as [`NbtTag`]s.
pub struct NbtOps;
impl DynamicOps for NbtOps {
@@ -387,7 +389,7 @@ impl MapLike for NbtMapLike<'_> {
}
}
/// An implementation of [`StructBuilder`] for NBT objects.
/// Builds NBT compounds for the [`NbtOps`] dynamic codec implementation.
pub struct NbtStructBuilder {
builder: DataResult<NbtTag>,
}

View File

@@ -1,3 +1,5 @@
//! Serialization to Java Edition, unnamed network, and Bedrock NBT.
use serde::ser::Impossible;
use serde::{Serialize, ser};
use std::io::Write;
@@ -8,6 +10,7 @@ use crate::{
NBT_LONG_ARRAY_TAG, SHORT_ID, STRING_ID,
};
/// Result type returned by NBT serialization operations.
pub type Result<T> = std::result::Result<T, Error>;
macro_rules! define_write_number_be {
@@ -30,30 +33,44 @@ macro_rules! define_write_number_le {
};
}
/// Format-specific primitive writer used by the NBT serializer.
pub trait NbtWriteHelper {
/// Underlying output writer.
type Writer: Write;
/// Returns the underlying output writer.
fn writer(&mut self) -> &mut Self::Writer;
/// Writes an unsigned byte.
fn write_u8(&mut self, value: u8) -> Result<()>;
/// Writes a signed byte.
fn write_i8(&mut self, value: i8) -> Result<()>;
/// Writes a 16-bit signed integer.
fn write_i16(&mut self, value: i16) -> Result<()>;
/// Writes a 32-bit signed integer.
fn write_i32(&mut self, value: i32) -> Result<()>;
/// Writes a 64-bit signed integer.
fn write_i64(&mut self, value: i64) -> Result<()>;
/// Writes a 32-bit floating-point number.
fn write_f32(&mut self, value: f32) -> Result<()>;
/// Writes a 64-bit floating-point number.
fn write_f64(&mut self, value: f64) -> Result<()>;
/// Writes a length-prefixed string.
fn write_string(&mut self, value: &str) -> Result<()>;
/// Writes an unmodified byte slice.
fn write_slice(&mut self, value: &[u8]) -> Result<()> {
self.writer().write_all(value).map_err(Error::Incomplete)?;
Ok(())
}
}
/// Writes Java Edition NBT primitives using big-endian numeric encoding.
pub struct NbtWriteHelperJava<W: Write> {
writer: W,
}
impl<W: Write> NbtWriteHelperJava<W> {
/// Creates a Java Edition writer over `w`.
pub const fn new(w: W) -> Self {
Self { writer: w }
}
@@ -93,11 +110,13 @@ impl<W: Write> NbtWriteHelper for NbtWriteHelperJava<W> {
}
}
/// Writes Bedrock network NBT primitives using little-endian and variable-length encoding.
pub struct NbtWriteHelperBedrock<W: Write> {
writer: W,
}
impl<W: Write> NbtWriteHelperBedrock<W> {
/// Creates a Bedrock network writer over `w`.
pub const fn new(w: W) -> Self {
Self { writer: w }
}
@@ -185,6 +204,7 @@ impl<W: Write> NbtWriteHelper for NbtWriteHelperBedrock<W> {
}
}
/// A Serde serializer backed by a format-specific NBT writer.
pub struct Serializer<W: NbtWriteHelper> {
output: W,
state: State,
@@ -193,6 +213,9 @@ pub struct Serializer<W: NbtWriteHelper> {
}
impl<W: NbtWriteHelper> Serializer<W> {
/// Creates a serializer with an optional root compound name.
///
/// Passing `None` writes unnamed network NBT.
pub const fn new(output: W, name: Option<String>) -> Self {
Self {
output,
@@ -276,31 +299,33 @@ impl<W: NbtWriteHelper> Serializer<W> {
}
}
/// Serializes struct using Serde Serializer to unnamed (network) NBT
/// Serializes a value as unnamed Java Edition network NBT.
pub fn to_bytes_unnamed<T: Serialize>(value: &T, w: impl Write) -> Result<()> {
let mut serializer = Serializer::new(NbtWriteHelperJava::new(w), None);
value.serialize(&mut serializer)?;
Ok(())
}
/// Serializes struct using Serde Serializer to normal NBT
/// Serializes a value as named Java Edition NBT using `name` for the root compound.
pub fn to_bytes_named<T: Serialize>(value: &T, name: String, w: impl Write) -> Result<()> {
let mut serializer = Serializer::new(NbtWriteHelperJava::new(w), Some(name));
value.serialize(&mut serializer)?;
Ok(())
}
/// Serializes struct using Serde Serializer to Bedrock network NBT
/// Serializes a value as named Bedrock network NBT using `name` for the root compound.
pub fn to_bytes_named_bedrock<T: Serialize>(value: &T, name: String, w: impl Write) -> Result<()> {
let mut serializer = Serializer::new(NbtWriteHelperBedrock::new(w), Some(name));
value.serialize(&mut serializer)?;
Ok(())
}
/// Serializes a value as Java Edition NBT with an empty root name.
pub fn to_bytes<T: Serialize>(value: &T, w: impl Write) -> Result<()> {
to_bytes_named(value, String::new(), w)
}
/// Serializes a value as Bedrock network NBT with an empty root name.
pub fn to_bytes_bedrock<T: Serialize>(value: &T, w: impl Write) -> Result<()> {
to_bytes_named_bedrock(value, String::new(), w)
}

View File

@@ -1,3 +1,5 @@
//! The in-memory representation of individual NBT tags.
use compound::NbtCompound;
use deserializer::NbtReadHelper;
use serde::{Deserialize, Serialize};
@@ -9,21 +11,35 @@ use crate::{
nbt_byte_array, nbt_int_array, nbt_long_array, serializer,
};
/// A value represented by one of the tag types defined by the NBT format.
#[derive(Clone, Debug, PartialEq)]
#[repr(u8)]
pub enum NbtTag {
/// Marks the end of a compound.
End = END_ID,
/// An 8-bit signed integer.
Byte(i8) = BYTE_ID,
/// A 16-bit signed integer.
Short(i16) = SHORT_ID,
/// A 32-bit signed integer.
Int(i32) = INT_ID,
/// A 64-bit signed integer.
Long(i64) = LONG_ID,
/// A 32-bit floating-point number.
Float(f32) = FLOAT_ID,
/// A 64-bit floating-point number.
Double(f64) = DOUBLE_ID,
/// An array of 8-bit signed integers.
ByteArray(Box<[i8]>) = BYTE_ARRAY_ID,
/// A string.
String(Box<str>) = STRING_ID,
/// A sequence of tags.
List(Vec<Self>) = LIST_ID,
/// A map of named tags.
Compound(NbtCompound) = COMPOUND_ID,
/// An array of 32-bit signed integers.
IntArray(Vec<i32>) = INT_ARRAY_ID,
/// An array of 64-bit signed integers.
LongArray(Vec<i64>) = LONG_ARRAY_ID,
}
@@ -36,6 +52,7 @@ impl NbtTag {
unsafe { *std::ptr::from_ref::<Self>(self).cast::<u8>() }
}
/// Serializes the tag's type ID followed by its payload.
pub fn serialize<W: NbtWriteHelper>(self, w: &mut W) -> serializer::Result<()> {
w.write_u8(self.get_type_id())?;
self.serialize_data(w)?;
@@ -106,6 +123,7 @@ impl NbtTag {
Self::Compound(compound)
}
/// Serializes the tag payload without writing its type ID.
pub fn serialize_data<W: NbtWriteHelper>(self, w: &mut W) -> serializer::Result<()> {
match self {
Self::End => {}
@@ -175,11 +193,13 @@ impl NbtTag {
Ok(())
}
/// Deserializes a type ID and its following payload.
pub fn deserialize<'a, R: NbtReadHelper<'a>>(reader: &mut R) -> Result<Self, Error> {
let tag_id = reader.get_u8()?;
Self::deserialize_data(reader, tag_id)
}
/// Advances a reader past the payload belonging to `tag_id`.
pub fn skip_data<'a, R: NbtReadHelper<'a>>(reader: &mut R, tag_id: u8) -> Result<(), Error> {
match tag_id {
END_ID => Ok(()),
@@ -239,6 +259,7 @@ impl NbtTag {
}
}
/// Deserializes a payload whose type is identified by `tag_id`.
pub fn deserialize_data<'a, R: NbtReadHelper<'a>>(
reader: &mut R,
tag_id: u8,
@@ -347,6 +368,7 @@ impl NbtTag {
}
}
/// Returns the contained byte, if this is a byte tag.
#[must_use]
pub const fn extract_byte(&self) -> Option<i8> {
match self {
@@ -355,6 +377,7 @@ impl NbtTag {
}
}
/// Returns the contained short, if this is a short tag.
#[must_use]
pub const fn extract_short(&self) -> Option<i16> {
match self {
@@ -363,6 +386,7 @@ impl NbtTag {
}
}
/// Returns the contained integer, if this is an integer tag.
#[must_use]
pub const fn extract_int(&self) -> Option<i32> {
match self {
@@ -371,6 +395,7 @@ impl NbtTag {
}
}
/// Returns the contained long, if this is a long tag.
#[must_use]
pub const fn extract_long(&self) -> Option<i64> {
match self {
@@ -379,6 +404,7 @@ impl NbtTag {
}
}
/// Returns the contained float, if this is a float tag.
#[must_use]
pub const fn extract_float(&self) -> Option<f32> {
match self {
@@ -387,6 +413,7 @@ impl NbtTag {
}
}
/// Returns the contained double, if this is a double tag.
#[must_use]
pub const fn extract_double(&self) -> Option<f64> {
match self {
@@ -395,6 +422,7 @@ impl NbtTag {
}
}
/// Returns the contained byte as a boolean, where zero is `false`.
#[must_use]
pub fn extract_bool(&self) -> Option<bool> {
match self {
@@ -403,6 +431,7 @@ impl NbtTag {
}
}
/// Returns the contained byte array, if this is a byte-array tag.
#[must_use]
pub fn extract_byte_array(&self) -> Option<&[i8]> {
match self {
@@ -411,6 +440,7 @@ impl NbtTag {
}
}
/// Returns the contained string, if this is a string tag.
#[must_use]
pub fn extract_string(&self) -> Option<&str> {
match self {
@@ -419,6 +449,7 @@ impl NbtTag {
}
}
/// Returns the contained list, if this is a list tag.
#[must_use]
pub fn extract_list(&self) -> Option<&[Self]> {
match self {
@@ -427,6 +458,7 @@ impl NbtTag {
}
}
/// Returns the contained compound, if this is a compound tag.
#[must_use]
pub const fn extract_compound(&self) -> Option<&NbtCompound> {
match self {
@@ -435,6 +467,7 @@ impl NbtTag {
}
}
/// Returns the contained integer array, if this is an integer-array tag.
#[must_use]
pub fn extract_int_array(&self) -> Option<&[i32]> {
match self {
@@ -443,6 +476,7 @@ impl NbtTag {
}
}
/// Returns the contained long array, if this is a long-array tag.
#[must_use]
pub fn extract_long_array(&self) -> Option<&[i64]> {
match self {