From c2dc3bf49cf06ca8ca985bc88500190cac1d86b5 Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Sat, 15 Apr 2017 20:29:08 -0400 Subject: [PATCH 1/3] Fills out the README with examples and installation info. Signed-off-by: Aaron Jacobs --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0289c8..e8f410f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,92 @@ # hematite_nbt [![Build Status](https://travis-ci.org/PistonDevelopers/hematite_nbt.svg?branch=master)](https://travis-ci.org/PistonDevelopers/hematite_nbt) -This is a [Rust][] library for working with [Minecraft][]'s Named Binary Tag (NBT) file format. It is maintained by the [Hematite][] project, and is used in the [Hematite server][]. +`hematite-nbt` is a Rust library for reading and writing Minecraft's +[Named Binary Tag file format](http://minecraft.gamepedia.com/NBT_format) (NBT). -Unlike the Hematite server, this library should be functional, and may be published on [crates.io][] soon. +This crate is maintained by the [Hematite][] project, and is used in the +[Hematite server][]. + +## Basic Usage + +The `nbt` crate can be used to read and write NBT-format streams. NBT-format +files are represented as `Blob` objects, which contain NBT-compatible `Value` +elements. For example: + +```rust +use nbt::{Blob, Value}; + +// Create a `Blob` from key/value pairs. +let mut nbt = Blob::new("".to_string()); +nbt.insert("name".to_string(), "Herobrine").unwrap(); +nbt.insert("health".to_string(), 100i8).unwrap(); +nbt.insert("food".to_string(), 20.0f32).unwrap(); + +// Write a compressed binary representation to a byte array. +let mut dst = Vec::new(); +nbt.write_zlib(&mut dst).unwrap(); +``` + +As of 0.3, the API is still experimental, and may change in future versions. + +## Usage with `serde_derive` + +This repository also contains an `nbt-serde` crate that supports reading and +writing the NBT format using the [`serde` framework][]. This enables +automatically deriving serialization/deserialization of user-defined types, +bypassing the use of the generic `Blob` and `Value` types illustrated above. + +For example: + +``` rust +#[macro_use] extern crate serde_derive; +extern crate serde; +extern crate nbt_serde; + +use nbt_serde::encode::to_writer; + +#[derive(Debug, PartialEq, Serialize)] +struct MyNbt { + name: String, + score: i8, +} + +fn main() { + let nbt = MyNbt { name: "Herobrine".to_string(), score: 42 }; + + let mut dst = Vec::new(); + to_writer(&mut dst, &nbt, None).unwrap(); + + let read: MyNbt = from_reader(&dst[..]).unwrap(); + + assert_eq!(read, nbt); +} +``` + +This approach can be considerably faster if one knows the data encoded in the +incoming NBT stream beforehand. + +## Installation + +Neither `nbt` nor `nbt_serde` is available on [crates.io][]. However, they can +still be listed as a dependency in your `Cargo.toml` file as follows: + +``` toml +[dependencies] +hematite-nbt = { git = "https://github.com/PistonDevelopers/hematite_nbt.git" } +hematite-nbt-serde = { git = "https://github.com/PistonDevelopers/hematite_nbt.git" } +``` + +(You can also clone the repository into a subdirectory and specify the crates as +a dependency with `path = "..."` if you wish.) + +## License + +All code is available under the terms of the MIT license. See the `LICENSE` file +for details. [Hematite]: http://hematite.piston.rs/ (Hematite) [Hematite server]: https://github.com/PistonDevelopers/hematite_server (github: PistonDevelopers: hematite_server) [Minecraft]: https://minecraft.net/ (Minecraft) [Rust]: http://www.rust-lang.org/ (The Rust Programming Language) [crates.io]: https://crates.io/ (crates.io) +[`serde` framework]: https://serde.rs/ From 6bef7352583e7de99edb39647227c99d4734b56d Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Mon, 17 Apr 2017 23:02:17 -0400 Subject: [PATCH 2/3] Improves ergonomics and documentation of nbt::Blob. Signed-off-by: Aaron Jacobs --- src/blob.rs | 111 ++++++++++++++++++++++++++++++++++++++------------- src/tests.rs | 38 +++++++++--------- src/value.rs | 24 +++++++---- 3 files changed, 118 insertions(+), 55 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index f64c94e..cf8df48 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fmt; use std::io; -use std::ops::Index; +use std::ops::{Index, IndexMut}; use flate2::Compression; use flate2::read::{GzDecoder, ZlibDecoder}; @@ -10,53 +10,90 @@ use flate2::write::{GzEncoder, ZlibEncoder}; use error::{Error, Result}; use value::Value; -/// A generic, complete object in Named Binary Tag format. +/// A blob of Named Binary Tag (NBT) data. /// -/// This is essentially a map of names to `Value`s, with an optional top-level -/// name of its own. It can be created in a similar way to a `HashMap`, or read -/// from an `io::Read` source, and its binary representation can be written to -/// an `io::Write` destination. +/// This struct provides methods to read, write, create, and modify data with a +/// well-defined NBT binary representation (that is, it contains only valid +/// [`Value`](enum.Value.html) entries). The API is similar to a `HashMap`. /// -/// These read and write methods support both uncompressed and compressed -/// (through Gzip or zlib compression) methods. +/// # Reading NBT-encoded Data +/// +/// Minecraft encodes many data files in the NBT format, which you can inspect +/// and modify using `Blob` objects. For example, to print out the contents of a +/// [`level.dat` file](http://minecraft.gamepedia.com/Level_format#level.dat_format), +/// one could use the following: +/// +/// ```ignore +/// use std::fs; +/// use nbt::Blob; +/// +/// let mut file = fs::File::open("level.dat").unwrap(); +/// let level = Blob::from_gzip(&mut file).unwrap(); +/// println!("File contents:\n{}", level); +/// ``` +/// +/// # Creating or Modifying NBT-encoded Data +/// +/// `Blob` objects have an API similar to `HashMap`, supporting +/// an `insert()` method for inserting new data and the index operator for +/// accessing or modifying this data. /// /// ```rust /// use nbt::{Blob, Value}; /// /// // Create a `Blob` from key/value pairs. -/// let mut nbt = Blob::new("".to_string()); -/// nbt.insert("name".to_string(), "Herobrine").unwrap(); -/// nbt.insert("health".to_string(), 100i8).unwrap(); -/// nbt.insert("food".to_string(), 20.0f32).unwrap(); +/// let mut nbt = Blob::new(); +/// nbt.insert("player", "Herobrine"); // Implicit conversion to `Value`. +/// nbt.insert("score", Value::Int(1400)); // Explicit `Value` type. /// -/// // Write a compressed binary representation to a byte array. +/// // Modify a value using the Index operator. +/// nbt["score"] = Value::Int(1401); +/// ``` +/// +/// # Writing NBT-encoded Data +/// +/// `Blob` provides methods for writing uncompressed and compressed binary NBT +/// data to arbitrary `io::Write` destinations. For example, to write compressed +/// data to a byte vector: +/// +/// ```rust +/// # use nbt::Blob; +/// # let nbt = Blob::new(); /// let mut dst = Vec::new(); -/// nbt.write_zlib(&mut dst).unwrap(); +/// nbt.write_gzip(&mut dst); /// ``` #[derive(Clone, Debug, PartialEq)] pub struct Blob { - title: String, + header: Option, content: Value } impl Blob { - /// Create a new NBT file format representation with the given name. - pub fn new(title: String) -> Blob { + /// Create a new NBT file format representation with an empty header. + pub fn new() -> Blob { let map: HashMap = HashMap::new(); - Blob { title: title, content: Value::Compound(map) } + Blob { header: None, content: Value::Compound(map) } + } + + /// Create a new NBT file format representation the given header. + pub fn with_header(header: S) -> Blob + where S: Into + { + let map: HashMap = HashMap::new(); + Blob { header: Some(header.into()), content: Value::Compound(map) } } /// Extracts an `Blob` object from an `io::Read` source. pub fn from_reader(mut src: &mut io::Read) -> Result { - let header = try!(Value::read_header(src)); + let (tag, header) = try!(Value::read_header(src)); // Although it would be possible to read NBT format files composed of // arbitrary objects using the current API, by convention all files // have a top-level Compound. - if header.0 != 0x0a { + if tag != 0x0a { return Err(Error::NoRootCompound); } - let content = try!(Value::from_reader(header.0, src)); - Ok(Blob { title: header.1, content: content }) + let content = try!(Value::from_reader(tag, src)); + Ok(Blob { header: header, content: content }) } /// Extracts an `Blob` object from an `io::Read` source that is @@ -76,7 +113,10 @@ impl Blob { /// Writes the binary representation of this `Blob` to an `io::Write` /// destination. pub fn write(&self, dst: &mut io::Write) -> Result<()> { - try!(self.content.write_header(dst, &self.title)); + match self.header { + None => try!(self.content.write_header(dst, None)), + Some(ref h) => try!(self.content.write_header(dst, Some(&h[..]))), + } self.content.write(dst) } @@ -99,8 +139,8 @@ impl Blob { /// This method will also return an error if a `Value::List` with /// heterogeneous elements is passed in, because this is illegal in the NBT /// file format. - pub fn insert(&mut self, name: String, value: V) -> Result<()> - where V: Into { + pub fn insert(&mut self, name: S, value: V) -> Result<()> + where S: Into, V: Into { // The follow prevents `List`s with heterogeneous tags from being // inserted into the file. It would be nicer to return an error, but // this would depart from the `HashMap` API for `insert`. @@ -116,7 +156,7 @@ impl Blob { } } if let Value::Compound(ref mut v) = self.content { - v.insert(name, nvalue); + v.insert(name.into(), nvalue); } else { unreachable!(); } @@ -126,7 +166,10 @@ impl Blob { /// The uncompressed length of this `Blob`, in bytes. pub fn len(&self) -> usize { // tag + name + content - 1 + 2 + self.title.len() + self.content.len() + match self.header { + None => 1 + 2 + self.content.len(), + Some(ref h) => 1 + 2 + h.len() + self.content.len(), + } } } @@ -141,8 +184,20 @@ impl<'a> Index<&'a str> for Blob { } } +impl<'a> IndexMut<&'a str> for Blob { + fn index_mut<'b>(&'b mut self, s: &'a str) -> &'b mut Value { + match self.content { + Value::Compound(ref mut v) => v.get_mut(s).unwrap(), + _ => unreachable!() + } + } +} + impl fmt::Display for Blob { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "TAG_Compound(\"{}\"): {}", self.title, self.content) + match self.header { + Some(ref h) => write!(f, "TAG_Compound(\"{}\"): {}", h, self.content), + None => write!(f, "TAG_Compound(): {}", self.content), + } } } diff --git a/src/tests.rs b/src/tests.rs index 3cd2e2a..4af598e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -10,12 +10,12 @@ use value::Value; #[test] fn nbt_nonempty() { - let mut nbt = Blob::new("".to_string()); - nbt.insert("name".to_string(), "Herobrine").unwrap(); - nbt.insert("health".to_string(), 100i8).unwrap(); - nbt.insert("food".to_string(), 20.0f32).unwrap(); - nbt.insert("emeralds".to_string(), 12345i16).unwrap(); - nbt.insert("timestamp".to_string(), 1424778774i32).unwrap(); + let mut nbt = Blob::new(); + nbt.insert("name", "Herobrine").unwrap(); + nbt.insert("health", 100i8).unwrap(); + nbt.insert("food", 20.0f32).unwrap(); + nbt.insert("emeralds", 12345i16).unwrap(); + nbt.insert("timestamp", 1424778774i32).unwrap(); let bytes = vec![ 0x0a, @@ -57,7 +57,7 @@ fn nbt_nonempty() { #[test] fn nbt_empty_nbtfile() { - let nbt = Blob::new("".to_string()); + let nbt = Blob::new(); let bytes = vec![ 0x0a, @@ -83,8 +83,8 @@ fn nbt_empty_nbtfile() { fn nbt_nested_compound() { let mut inner = HashMap::new(); inner.insert("test".to_string(), Value::Byte(123)); - let mut nbt = Blob::new("".to_string()); - nbt.insert("inner".to_string(), Value::Compound(inner)).unwrap(); + let mut nbt = Blob::new(); + nbt.insert("inner", Value::Compound(inner)).unwrap(); let bytes = vec![ 0x0a, @@ -116,8 +116,8 @@ fn nbt_nested_compound() { #[test] fn nbt_empty_list() { - let mut nbt = Blob::new("".to_string()); - nbt.insert("list".to_string(), Value::List(Vec::new())).unwrap(); + let mut nbt = Blob::new(); + nbt.insert("list", Value::List(Vec::new())).unwrap(); let bytes = vec![ 0x0a, @@ -186,12 +186,12 @@ fn nbt_invalid_id() { #[test] fn nbt_invalid_list() { - let mut nbt = Blob::new("".to_string()); + let mut nbt = Blob::new(); let mut badlist = Vec::new(); badlist.push(Value::Byte(1)); badlist.push(Value::Short(1)); // Will fail to insert, because the List is heterogeneous. - assert_eq!(nbt.insert("list".to_string(), Value::List(badlist)), + assert_eq!(nbt.insert("list", Value::List(badlist)), Err(Error::HeterogeneousList)); } @@ -206,12 +206,12 @@ fn nbt_bad_compression() { #[test] fn nbt_compression() { // Create a non-trivial Blob. - let mut nbt = Blob::new("".to_string()); - nbt.insert("name".to_string(), Value::String("Herobrine".to_string())).unwrap(); - nbt.insert("health".to_string(), Value::Byte(100)).unwrap(); - nbt.insert("food".to_string(), Value::Float(20.0)).unwrap(); - nbt.insert("emeralds".to_string(), Value::Short(12345)).unwrap(); - nbt.insert("timestamp".to_string(), Value::Int(1424778774)).unwrap(); + let mut nbt = Blob::new(); + nbt.insert("name", Value::String("Herobrine".to_string())).unwrap(); + nbt.insert("health", Value::Byte(100)).unwrap(); + nbt.insert("food", Value::Float(20.0)).unwrap(); + nbt.insert("emeralds", Value::Short(12345)).unwrap(); + nbt.insert("timestamp", Value::Int(1424778774)).unwrap(); // Test zlib encoding/decoding. let mut zlib_dst = Vec::new(); diff --git a/src/value.rs b/src/value.rs index 672c940..a1500e6 100644 --- a/src/value.rs +++ b/src/value.rs @@ -84,11 +84,16 @@ impl Value { } } - /// Writes the header (that is, the value's type ID and optionally a title) + /// Writes the header (that is, the value's type ID and optionally a header) /// of this `Value` to an `io::Write` destination. - pub fn write_header(&self, mut dst: &mut io::Write, title: &str) -> Result<()> { + pub fn write_header(&self, mut dst: &mut io::Write, header: Option<&str>) + -> Result<()> + { try!(dst.write_u8(self.id())); - raw::write_bare_string(&mut dst, title) + match header { + Some(ref s) => raw::write_bare_string(&mut dst, s), + None => raw::write_bare_string(&mut dst, ""), + } } /// Writes the payload of this `Value` to an `io::Write` destination. @@ -126,7 +131,7 @@ impl Value { Value::Compound(ref vals) => { for (name, ref nbt) in vals { // Write the header for the tag. - try!(nbt.write_header(dst, &name)); + try!(nbt.write_header(dst, Some(&name))); try!(nbt.write(dst)); } @@ -138,12 +143,15 @@ impl Value { /// Reads any valid `Value` header (that is, a type ID and a title of /// arbitrary UTF-8 bytes) from an `io::Read` source. - pub fn read_header(mut src: &mut io::Read) -> Result<(u8, String)> { + pub fn read_header(mut src: &mut io::Read) -> Result<(u8, Option)> { let id = try!(src.read_u8()); - if id == 0x00 { return Ok((0x00, "".to_string())); } + if id == 0x00 { return Ok((0x00, None)); } // Extract the name. let name = try!(raw::read_bare_string(&mut src)); - Ok((id, name)) + match name.len() { + 0 => Ok((id, None)), + _ => Ok((id, Some(name))), + } } /// Reads the payload of an `Value` with a given type ID from an @@ -173,7 +181,7 @@ impl Value { let (id, name) = try!(Value::read_header(src)); if id == 0x00 { break; } let tag = try!(Value::from_reader(id, src)); - buf.insert(name, tag); + buf.insert(name.unwrap(), tag); } Ok(Value::Compound(buf)) }, From 2fbd6371178568b8ea4f5e6a0593fc11df1d4599 Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Mon, 17 Apr 2017 23:34:20 -0400 Subject: [PATCH 3/3] Improves the documentation for nbt::Value. This also hides the various read* and write* methods from the docs, since they are not really intended to be part of the public API. Signed-off-by: Aaron Jacobs --- src/value.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/value.rs b/src/value.rs index a1500e6..1778bcc 100644 --- a/src/value.rs +++ b/src/value.rs @@ -7,19 +7,52 @@ use byteorder::{BigEndian, WriteBytesExt, ReadBytesExt}; use error::{Error, Result}; use raw; -/// Values which can be represented in the Named Binary Tag format. +/// Values which can be represented in Named Binary Tag (NBT) format. +/// +/// This enum has one variant for each of the [TAG +/// types](http://minecraft.gamepedia.com/NBT_format#TAG_definition) defined in +/// the NBT specification, which can be used to create `Value` objects in the +/// usual fashion. It is also possible to construct `Value` objects using one of +/// the provided `Into` trait implementations. For example: +/// +/// ```rust +/// use nbt::Value; +/// +/// let a: Value = 100i32.into(); +/// let b = Value::Int(100); +/// assert_eq!(a, b); +/// ``` +/// +/// For reading and writing of NBT binary data directly, see the +/// [`Blob`](struct.Blob.html) struct. #[derive(Clone, Debug, PartialEq)] pub enum Value { + /// A variant for `TAG_Byte` data, containing a single signed byte. Byte(i8), + /// A variant for `TAG_Short` data, containing a single signed short. Short(i16), + /// A variant for `TAG_Int` data, containing a single signed int. Int(i32), + /// A variant for `TAG_Long` data, containing a single signed long. Long(i64), + /// A variant for `TAG_Float` data, containing a single 32-bit floating + /// point value. Float(f32), + /// A variant for `TAG_Double` data, containing a single 64-bit floating point + /// value. Double(f64), + /// A variant for `TAG_ByteArray` data, containing a stream of signed bytes. ByteArray(Vec), + /// A variant for `TAG_String` data, containing a stream of UTF-8 bytes. String(String), + /// A variant for `TAG_List` data, containing a homogenous list of another + /// TAG type. While it is possible to construct heterogenous lists, `nbt` + /// will refuse to serialize them. List(Vec), + /// A variant for `TAG_Compound` data, containing key-value pairs of named + /// TAG types. Compound(HashMap), + /// A variant for `TAG_IntArray` data, containing a stream of signed ints. IntArray(Vec), } @@ -60,6 +93,7 @@ impl Value { } /// The length of the payload of this `Value`, in bytes. + #[doc(hidden)] pub fn len(&self) -> usize { match *self { Value::Byte(_) => 1, @@ -86,6 +120,7 @@ impl Value { /// Writes the header (that is, the value's type ID and optionally a header) /// of this `Value` to an `io::Write` destination. + #[doc(hidden)] pub fn write_header(&self, mut dst: &mut io::Write, header: Option<&str>) -> Result<()> { @@ -97,6 +132,7 @@ impl Value { } /// Writes the payload of this `Value` to an `io::Write` destination. + #[doc(hidden)] pub fn write(&self, mut dst: &mut io::Write) -> Result<()> { match *self { Value::Byte(val) => raw::write_bare_byte(&mut dst, val), @@ -141,8 +177,9 @@ impl Value { } } - /// Reads any valid `Value` header (that is, a type ID and a title of + /// Reads any valid `Value` header (that is, a type ID and an optional header of /// arbitrary UTF-8 bytes) from an `io::Read` source. + #[doc(hidden)] pub fn read_header(mut src: &mut io::Read) -> Result<(u8, Option)> { let id = try!(src.read_u8()); if id == 0x00 { return Ok((0x00, None)); } @@ -156,6 +193,7 @@ impl Value { /// Reads the payload of an `Value` with a given type ID from an /// `io::Read` source. + #[doc(hidden)] pub fn from_reader(id: u8, mut src: &mut io::Read) -> Result { match id { 0x01 => Ok(Value::Byte(raw::read_bare_byte(&mut src)?)),