Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/
111 changes: 83 additions & 28 deletions src/blob.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<String, Value>`, 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<String>,
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<String, Value> = 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<S>(header: S) -> Blob
where S: Into<String>
{
let map: HashMap<String, Value> = 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<Blob> {
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
Expand All @@ -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)
}

Expand All @@ -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<V>(&mut self, name: String, value: V) -> Result<()>
where V: Into<Value> {
pub fn insert<V, S>(&mut self, name: S, value: V) -> Result<()>
where S: Into<String>, V: Into<Value> {
// 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`.
Expand All @@ -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!();
}
Expand All @@ -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(),
}
}
}

Expand All @@ -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),
}
}
}
38 changes: 19 additions & 19 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}

Expand All @@ -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();
Expand Down
Loading