diff --git a/pumpkin-nbt/src/compound.rs b/pumpkin-nbt/src/compound.rs index 4d157189a..1720a29ea 100644 --- a/pumpkin-nbt/src/compound.rs +++ b/pumpkin-nbt/src/compound.rs @@ -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, 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 { 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(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) { 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) { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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()) diff --git a/pumpkin-nbt/src/deserializer.rs b/pumpkin-nbt/src/deserializer.rs index 85d25a20e..272fea354 100644 --- a/pumpkin-nbt/src/deserializer.rs +++ b/pumpkin-nbt/src/deserializer.rs @@ -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 = std::result::Result; 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> = const { std::cell::RefCell::new(None) }; } @@ -26,15 +33,28 @@ pub(super) fn set_curr_visitor_seq_list_id(tag: Option) { }); } +/// 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; + /// 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>; + /// Reads a byte-array payload of `len` elements. fn read_byte_array(&mut self, len: usize) -> Result>; } -pub struct NbtStreamReader(pub R); +/// Adapts a [`Read`] and [`Seek`] stream into an [`NbtDataSource`]. +pub struct NbtStreamReader( + /// Wrapped input stream. + pub R, +); impl<'a, R: Read + Seek> NbtDataSource<'a> for NbtStreamReader { fn read_u8(&mut self) -> Result { @@ -191,63 +211,88 @@ impl<'a> NbtDataSource<'a> for Cursor> { } } +/// 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; + /// Reads a signed byte. fn get_i8(&mut self) -> Result; + /// Reads a 16-bit signed integer. fn get_i16(&mut self) -> Result; + /// Reads a 32-bit signed integer. fn get_i32(&mut self) -> Result; + /// Reads a 64-bit signed integer. fn get_i64(&mut self) -> Result; + /// Reads a 32-bit floating-point number. fn get_f32(&mut self) -> Result; + /// Reads a 64-bit floating-point number. fn get_f64(&mut self) -> Result; + /// Reads a length-prefixed string. fn get_string(&mut self) -> Result>; + /// Reads a byte array with the supplied element count. fn get_byte_array(&mut self, len: usize) -> Result>; } +/// Reads Java Edition NBT primitives using big-endian numeric encoding. pub struct NbtReadHelperJava { reader: D, } impl NbtReadHelperJava { + /// 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 { reader: D, } impl NbtReadHelperBedrock { + /// 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 { } } +/// A Serde deserializer backed by a format-specific NBT reader. pub struct Deserializer { input: R, tag_to_deserialize_stack: Option, @@ -417,6 +463,10 @@ pub struct Deserializer { } impl Deserializer { + /// 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 Deserializer { } } -/// 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 { 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 { 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 { 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 { 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 { 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 { let mut deserializer = Deserializer::new(NbtReadHelperBedrock::new(Cursor::new(slice)), true); T::deserialize(&mut deserializer) diff --git a/pumpkin-nbt/src/lib.rs b/pumpkin-nbt/src/lib.rs index 9cd87e218..827e9f0a5 100644 --- a/pumpkin-nbt/src/lib.rs +++ b/pumpkin-nbt/src/lib.rs @@ -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 { 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 { 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(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(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(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( input: T, serializer: S, diff --git a/pumpkin-nbt/src/nbt_compress.rs b/pumpkin-nbt/src/nbt_compress.rs index b8bfbef21..822921491 100644 --- a/pumpkin-nbt/src/nbt_compress.rs +++ b/pumpkin-nbt/src/nbt_compress.rs @@ -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 { // 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 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, 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 { // 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(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(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(value: &T) -> Result, Error> { let mut buffer = Vec::new(); to_gzip_bytes(value, &mut buffer)?; diff --git a/pumpkin-nbt/src/nbt_ops.rs b/pumpkin-nbt/src/nbt_ops.rs index fe1015d22..da699573b 100644 --- a/pumpkin-nbt/src/nbt_ops.rs +++ b/pumpkin-nbt/src/nbt_ops.rs @@ -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, } diff --git a/pumpkin-nbt/src/serializer.rs b/pumpkin-nbt/src/serializer.rs index b167f7dbb..49254612b 100644 --- a/pumpkin-nbt/src/serializer.rs +++ b/pumpkin-nbt/src/serializer.rs @@ -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 = std::result::Result; 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 { writer: W, } impl NbtWriteHelperJava { + /// Creates a Java Edition writer over `w`. pub const fn new(w: W) -> Self { Self { writer: w } } @@ -93,11 +110,13 @@ impl NbtWriteHelper for NbtWriteHelperJava { } } +/// Writes Bedrock network NBT primitives using little-endian and variable-length encoding. pub struct NbtWriteHelperBedrock { writer: W, } impl NbtWriteHelperBedrock { + /// Creates a Bedrock network writer over `w`. pub const fn new(w: W) -> Self { Self { writer: w } } @@ -185,6 +204,7 @@ impl NbtWriteHelper for NbtWriteHelperBedrock { } } +/// A Serde serializer backed by a format-specific NBT writer. pub struct Serializer { output: W, state: State, @@ -193,6 +213,9 @@ pub struct Serializer { } impl Serializer { + /// Creates a serializer with an optional root compound name. + /// + /// Passing `None` writes unnamed network NBT. pub const fn new(output: W, name: Option) -> Self { Self { output, @@ -276,31 +299,33 @@ impl Serializer { } } -/// Serializes struct using Serde Serializer to unnamed (network) NBT +/// Serializes a value as unnamed Java Edition network NBT. pub fn to_bytes_unnamed(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(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(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(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(value: &T, w: impl Write) -> Result<()> { to_bytes_named_bedrock(value, String::new(), w) } diff --git a/pumpkin-nbt/src/tag.rs b/pumpkin-nbt/src/tag.rs index 4b309e2cd..d959fc419 100644 --- a/pumpkin-nbt/src/tag.rs +++ b/pumpkin-nbt/src/tag.rs @@ -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) = STRING_ID, + /// A sequence of tags. List(Vec) = LIST_ID, + /// A map of named tags. Compound(NbtCompound) = COMPOUND_ID, + /// An array of 32-bit signed integers. IntArray(Vec) = INT_ARRAY_ID, + /// An array of 64-bit signed integers. LongArray(Vec) = LONG_ARRAY_ID, } @@ -36,6 +52,7 @@ impl NbtTag { unsafe { *std::ptr::from_ref::(self).cast::() } } + /// Serializes the tag's type ID followed by its payload. pub fn serialize(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(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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 {