From 427e0d24df52e57242495a89c6cda476f073b206 Mon Sep 17 00:00:00 2001 From: Owen Gage Date: Sat, 2 Mar 2024 11:54:05 +0000 Subject: [PATCH] Allow a non-empty root compound name when serializing. --- Cargo.lock | 18 +++++++------- fastnbt/Cargo.toml | 2 +- fastnbt/src/lib.rs | 46 ++++++++++++++++++++++++++++++++--- fastnbt/src/ser/mod.rs | 33 ++++++++++++++++++++++++- fastnbt/src/ser/serializer.rs | 21 ++++++++++------ fastnbt/src/test/ser.rs | 23 ++++++++++++++++-- 6 files changed, 120 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7aedba5..a4bad6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,7 +312,7 @@ dependencies = [ "bit_field", "byteorder", "criterion", - "fastnbt 2.4.4", + "fastnbt 2.5.0", "flate2", "image", "log", @@ -326,26 +326,26 @@ dependencies = [ [[package]] name = "fastnbt" version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369bd70629bccfda7e344883c9ae3ab7f3b10a357bcf8b0f69caa7256bcf188" dependencies = [ - "arbitrary", "byteorder", "cesu8", - "flate2", "serde", "serde_bytes", - "serde_json", ] [[package]] name = "fastnbt" -version = "2.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369bd70629bccfda7e344883c9ae3ab7f3b10a357bcf8b0f69caa7256bcf188" +version = "2.5.0" dependencies = [ + "arbitrary", "byteorder", "cesu8", + "flate2", "serde", "serde_bytes", + "serde_json", ] [[package]] @@ -355,7 +355,7 @@ dependencies = [ "clap 2.34.0", "env_logger", "fastanvil", - "fastnbt 2.4.4", + "fastnbt 2.5.0", "fastsnbt", "flate2", "image", @@ -372,7 +372,7 @@ name = "fastsnbt" version = "0.2.0" dependencies = [ "byteorder", - "fastnbt 2.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fastnbt 2.4.4", "itoa", "nom", "ryu", diff --git a/fastnbt/Cargo.toml b/fastnbt/Cargo.toml index 723d463..b92e902 100644 --- a/fastnbt/Cargo.toml +++ b/fastnbt/Cargo.toml @@ -3,7 +3,7 @@ name = "fastnbt" description = "Serde deserializer for Minecraft's NBT format" repository = "https://github.com/owengage/fastnbt" readme = "README.md" -version = "2.4.4" +version = "2.5.0" authors = ["Owen Gage "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/fastnbt/src/lib.rs b/fastnbt/src/lib.rs index 9003bc5..0257b8c 100644 --- a/fastnbt/src/lib.rs +++ b/fastnbt/src/lib.rs @@ -256,18 +256,57 @@ impl Display for Tag { /// Serialize some `T` into NBT data. See the [`ser`] module for more /// information. pub fn to_bytes(v: &T) -> Result> { + to_bytes_with_opts(v, Default::default()) +} + +/// Serialize some `T` into NBT data. See the [`ser`] module for more +/// information. +pub fn to_writer(writer: W, v: &T) -> Result<()> { + to_writer_with_opts(writer, v, Default::default()) +} + +/// Options for customizing serialization. +#[derive(Default, Clone)] +pub struct SerOpts { + root_name: String, +} + +impl SerOpts { + /// Create new options. This object follows a builder pattern. + pub fn new() -> Self { + Default::default() + } + + /// Set the root name (top level) of the compound. In most Minecraft data + /// structures this is the empty string. The [`ser`][`crate::ser`] module + /// contains an example. + pub fn root_name(mut self, root_name: impl Into) -> Self { + self.root_name = root_name.into(); + self + } +} + +/// Serialize some `T` into NBT data. See the [`ser`] module for more +/// information. The options allow you to set things like the root name of the +/// compound when serialized. +pub fn to_bytes_with_opts(v: &T, opts: SerOpts) -> Result> { let mut result = vec![]; let mut serializer = Serializer { writer: &mut result, + root_name: opts.root_name, }; v.serialize(&mut serializer)?; Ok(result) } /// Serialize some `T` into NBT data. See the [`ser`] module for more -/// information. -pub fn to_writer(writer: W, v: &T) -> Result<()> { - let mut serializer = Serializer { writer }; +/// information. The options allow you to set things like the root name of the +/// compound when serialized. +pub fn to_writer_with_opts(writer: W, v: &T, opts: SerOpts) -> Result<()> { + let mut serializer = Serializer { + writer, + root_name: opts.root_name, + }; v.serialize(&mut serializer)?; Ok(()) } @@ -324,6 +363,7 @@ where } /// Options for customizing deserialization. +#[derive(Clone)] pub struct DeOpts { /// Maximum number of bytes a list or array can be. max_seq_len: usize, diff --git a/fastnbt/src/ser/mod.rs b/fastnbt/src/ser/mod.rs index 2184cd0..34407f1 100644 --- a/fastnbt/src/ser/mod.rs +++ b/fastnbt/src/ser/mod.rs @@ -1,6 +1,7 @@ //! This module contains a serde serializer for NBT data. This should be able to //! serialize most structures to NBT. Use [`to_bytes`][`crate::to_bytes`] or -//! [`to_writer`][`crate::to_writer`]. +//! [`to_writer`][`crate::to_writer`]. There are 'with opts' functions for more +//! serialization control. //! //! Some Rust structures have no sensible mapping to NBT data. These cases will //! result in an error (not a panic). If you find a case where you think there @@ -15,6 +16,36 @@ //! `i128` or `u128`, an IntArray of length 4 will be produced. This is stored //! as big endian i.e. the most significant bit (and int) is first. //! +//! # Root compound name +//! +//! A valid NBT compound must have a name, including the root compound. For most +//! Minecraft data this is simply the empty string. If you need to control the +//! name of this root compound you can use +//! [`to_bytes_with_opts`][`crate::to_bytes_with_opts`] and +//! [`to_writer_with_opts`][`crate::to_writer_with_opts`]. For example the +//! [unofficial schematic +//! format](https://minecraft.wiki/w/Schematic_file_format): +//! +//! ```no_run +//! use serde::{Serialize, Deserialize}; +//! use fastnbt::{Value, ByteArray, SerOpts}; +//! +//! #[derive(Serialize, Deserialize, Debug)] +//! #[serde(rename_all = "PascalCase")] +//! pub struct Schematic { +//! blocks: ByteArray, +//! data: ByteArray, +//! tile_entities: Vec, +//! entities: Vec, +//! width: i16, +//! height: i16, +//! length: i16, +//! materials: String, +//! } +//! +//! let structure = todo!(); // make schematic +//! let bytes = fastnbt::to_bytes_with_opts(&structure, SerOpts::new().root_name("Schematic")).unwrap(); +//! ``` mod array_serializer; mod name_serializer; mod serializer; diff --git a/fastnbt/src/ser/serializer.rs b/fastnbt/src/ser/serializer.rs index 491fbd9..55bd1bc 100644 --- a/fastnbt/src/ser/serializer.rs +++ b/fastnbt/src/ser/serializer.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::{io::Write, mem}; use byteorder::{BigEndian, WriteBytesExt}; use serde::{ @@ -18,11 +18,15 @@ use super::{ enum DelayedHeader { List { len: usize }, // header for a list, so element tag and list size. MapEntry { outer_name: Vec }, // header for a compound, so tag, name of compound. - Root, // root compound, special because it isn't allowed to be an array type. Must be compound. + Root { root_name: String }, // root compound, special because it isn't allowed to be an array type. Must be compound. } pub struct Serializer { pub(crate) writer: W, + + // Desired name of the root compound, typically an empty string. + // NOTE: This is `mem:take`en, so is only valid at the start of serialization! + pub(crate) root_name: String, } macro_rules! no_root { @@ -132,10 +136,13 @@ impl<'a, W: 'a + Write> serde::ser::Serializer for &'a mut Serializer { } fn serialize_map(self, _len: Option) -> Result { + // Take the root name to avoid a clone. Need to be careful not to use + // self.root_name elsewhere. + let root_name = mem::take(&mut self.root_name); Ok(SerializerMap { ser: self, key: None, - header: Some(DelayedHeader::Root), + header: Some(DelayedHeader::Root { root_name }), trailer: Some(Tag::End), }) } @@ -160,19 +167,19 @@ pub struct SerializerMap<'a, W: Write> { key: Option>, header: Option, trailer: Option, - // compound_name: Vec, - // first: bool, } fn write_header(writer: &mut impl Write, header: DelayedHeader, actual_tag: Tag) -> Result<()> { match header { - DelayedHeader::Root => { + DelayedHeader::Root { + root_name: outer_name, + } => { if actual_tag != Tag::Compound { // TODO: Test case for this. return Err(Error::no_root_compound()); } writer.write_tag(Tag::Compound)?; - writer.write_size_prefixed_str("")?; + writer.write_size_prefixed_str(&outer_name)?; } DelayedHeader::MapEntry { ref outer_name } => { writer.write_tag(actual_tag)?; diff --git a/fastnbt/src/test/ser.rs b/fastnbt/src/test/ser.rs index abafadd..75f4b54 100644 --- a/fastnbt/src/test/ser.rs +++ b/fastnbt/src/test/ser.rs @@ -1,12 +1,14 @@ -use std::{collections::HashMap, iter::FromIterator}; +use std::{collections::HashMap, io::Cursor, iter::FromIterator}; use crate::{ borrow, from_bytes, test::{resources::CHUNK_RAW_WITH_ENTITIES, Single, Wrap}, - to_bytes, ByteArray, IntArray, LongArray, Tag, Value, + to_bytes, to_bytes_with_opts, to_writer_with_opts, ByteArray, IntArray, LongArray, SerOpts, + Tag, Value, }; use serde::{ser::SerializeMap, Deserialize, Serialize}; use serde_bytes::{ByteBuf, Bytes}; +use serde_json::to_writer; use super::builder::Builder; @@ -917,3 +919,20 @@ fn serialize_key_and_value() { let actual = to_bytes(&nbt!({"test":"value"})).unwrap(); assert_eq!(actual, bs); } + +#[test] +fn serialize_root_with_name() { + #[derive(Serialize)] + struct Empty {} + let expected = Builder::new().start_compound("test").end_compound().build(); + let opts = SerOpts::new().root_name("test"); + let mut actual_via_writer = Cursor::new(Vec::new()); + to_writer_with_opts(&mut actual_via_writer, &Empty {}, opts.clone()).unwrap(); + + let actual_via_bytes = to_bytes_with_opts(&Empty {}, opts.clone()).unwrap(); + let actual_value = to_bytes_with_opts(&Value::Compound(HashMap::new()), opts.clone()).unwrap(); + + assert_eq!(actual_via_bytes, expected); + assert_eq!(actual_via_writer.into_inner(), expected); + assert_eq!(actual_value, expected); +}