Skip to content

Commit

Permalink
Allow a non-empty root compound name when serializing.
Browse files Browse the repository at this point in the history
  • Loading branch information
owengage committed Mar 2, 2024
1 parent ed829ac commit 427e0d2
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 23 deletions.
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion fastnbt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <owengage@gmail.com>"]
edition = "2021"
license = "MIT OR Apache-2.0"
Expand Down
46 changes: 43 additions & 3 deletions fastnbt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Serialize>(v: &T) -> Result<Vec<u8>> {
to_bytes_with_opts(v, Default::default())
}

/// Serialize some `T` into NBT data. See the [`ser`] module for more
/// information.
pub fn to_writer<T: Serialize, W: Write>(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<String>) -> 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<T: Serialize>(v: &T, opts: SerOpts) -> Result<Vec<u8>> {
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<T: Serialize, W: Write>(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<T: Serialize, W: Write>(writer: W, v: &T, opts: SerOpts) -> Result<()> {
let mut serializer = Serializer {
writer,
root_name: opts.root_name,
};
v.serialize(&mut serializer)?;
Ok(())
}
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 32 additions & 1 deletion fastnbt/src/ser/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Value>,
//! entities: Vec<Value>,
//! 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;
Expand Down
21 changes: 14 additions & 7 deletions fastnbt/src/ser/serializer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::Write;
use std::{io::Write, mem};

use byteorder::{BigEndian, WriteBytesExt};
use serde::{
Expand All @@ -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<u8> }, // 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<W: Write> {
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 {
Expand Down Expand Up @@ -132,10 +136,13 @@ impl<'a, W: 'a + Write> serde::ser::Serializer for &'a mut Serializer<W> {
}

fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
// 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),
})
}
Expand All @@ -160,19 +167,19 @@ pub struct SerializerMap<'a, W: Write> {
key: Option<Vec<u8>>,
header: Option<DelayedHeader>,
trailer: Option<Tag>,
// compound_name: Vec<u8>,
// 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)?;
Expand Down
23 changes: 21 additions & 2 deletions fastnbt/src/test/ser.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
}

0 comments on commit 427e0d2

Please sign in to comment.