na_nbt 0.1.3

High-performance NBT (Named Binary Tag) library with zero-copy parsing and serde support
Documentation

na_nbt

A high-performance NBT (Named Binary Tag) library for Rust with zero-copy parsing, full mutation support and serde integration.

⚠️ Note: This crate is under active development. APIs may change between versions. Issues and contributions are welcome!

Features

  • Zero-copy parsing - Read NBT data without allocating memory for values
  • Full mutation - Create and modify NBT structures with an owned representation
  • Endianness support - Convert between BigEndian and LittleEndian on read or write
  • Generic traits - Write code that works with any value type
  • Serde integration - Serialize/deserialize Rust types directly to/from NBT (optional)
  • Shared values - Thread-safe Arc-based values with bytes crate (optional)

Installation

[dependencies]

na_nbt = "0.1"

Optional Features

Both serde and shared features are enabled by default. To use without optional dependencies:

[dependencies]

na_nbt = { version = "0.1", default-features = false }



# Or enable only what you need:

na_nbt = { version = "0.1", default-features = false, features = ["serde"] }

Feature Description Dependencies
serde Serialize/deserialize Rust types to/from NBT serde
shared SharedValue with Arc ownership bytes

Todo

  • More convenient APIs
  • Documentation
  • Benchmarks
  • Tests and fuzzing
  • MSRV testing
  • no_std support
  • Older Rust support

Quick Start

use na_nbt::read_borrowed;
use zerocopy::byteorder::BigEndian;

let data = [
    0x0a, 0x00, 0x00, // Compound with empty name
    0x01, 0x00, 0x03, b'f', b'o', b'o', 42u8, // Byte "foo" = 42
    0x00, // End
];

let doc = read_borrowed::<BigEndian>(&data).unwrap();
let root = doc.root();

if let Some(value) = root.get("foo") {
    assert_eq!(value.as_byte(), Some(42));
}

Two Parsing Modes

Mode Function Type Use Case
Zero-copy (borrowed) read_borrowed BorrowedValue Fast reads, data lives on stack/slice
Zero-copy (shared) read_shared SharedValue Pass values across threads
Owned read_owned OwnedValue Need to modify or outlive source data

Zero-Copy Mode (Borrowed)

Parses NBT without copying data. Values reference the original byte slice directly.

use na_nbt::read_borrowed;
use zerocopy::byteorder::BigEndian;

let data: &[u8] = &[0x0a, 0x00, 0x00, 0x00]; // Empty compound
let doc = read_borrowed::<BigEndian>(data).unwrap();
let root = doc.root(); // Zero-copy reference into `data`

Zero-Copy Mode (Shared)

Like borrowed mode, but wraps data in Arc for shared ownership. Values are Clone, Send, Sync, and 'static - perfect for multi-threaded scenarios.

use na_nbt::read_shared;
use bytes::Bytes;
use zerocopy::byteorder::BigEndian;

let data = Bytes::from_static(&[0x0a, 0x00, 0x00, 0x00]);
let root = read_shared::<BigEndian>(data).unwrap();

// Can clone and send to other threads
let cloned = root.clone();
std::thread::spawn(move || {
    assert!(cloned.as_compound().is_some());
}).join().unwrap();

Owned Mode

Parses NBT into an owned structure that can be modified.

use na_nbt::{read_owned, OwnedValue};
use zerocopy::byteorder::{BigEndian, LittleEndian};

let data: &[u8] = &[0x0a, 0x00, 0x00, 0x00];

// Convert from BigEndian source to LittleEndian storage
let mut root: OwnedValue<LittleEndian> = read_owned::<BigEndian, LittleEndian>(data).unwrap();

if let OwnedValue::Compound(ref mut compound) = root {
    compound.insert("score", 100i32);
}

Writing Generic Code

Trait Hierarchy

ScopedReadableValue (all types)
        ▲
        │
┌───────┴───────┐
│               │
ReadableValue   ScopedWritableValue
(immutable)     (scoped mutation)
                        ▲
                        │
                  WritableValue
                  (full mutation)
  • ScopedReadableValue - Base trait implemented by all value types
  • ReadableValue - Extends ScopedReadableValue with document-lifetime references
  • ScopedWritableValue - Extends ScopedReadableValue with scoped mutation
  • WritableValue - Extends ScopedWritableValue with full mutable references

Scoped vs Unscoped Methods

The "scoped" suffix indicates bounded lifetime and indirection:

  • Unscoped methods (e.g., get(), as_byte_array()) return references to data already stored in the value type with document lifetime 'doc - can be stored independently.

  • Scoped methods (e.g., get_scoped(), as_byte_array_scoped()) construct new view types on demand with borrow lifetime 'a - necessary for types like OwnedValue that don't directly store container types.

When to use which:

  • Use scoped methods when writing generic code that works with all value types
  • Use unscoped methods when you need direct access to stored fields with longer lifetime
Trait Capability Implemented By
ScopedReadableValue Read primitives, iterate (scoped) All value types
ReadableValue + Document-lifetime references BorrowedValue, SharedValue, ImmutableValue
ScopedWritableValue + Mutation (scoped) OwnedValue, MutableValue
WritableValue + Mutable references to containers MutableValue
use na_nbt::{ScopedReadableValue, ScopedReadableCompound, ReadableString};

fn print_compound_keys<'doc>(value: &impl ScopedReadableValue<'doc>) {
    if let Some(compound) = value.as_compound_scoped() {
        for (key, _) in compound.iter() {
            println!("Key: {}", key.decode());
        }
    }
}

Type Overview

Zero-Copy Types

Type Description
ReadonlyValue Underlying zero-copy value
BorrowedValue Type alias for borrowed data
SharedValue Type alias for Arc-wrapped data

Owned Types

Type Description
OwnedValue Fully owned, mutable NBT value
MutableValue Mutable view into an OwnedValue
ImmutableValue Immutable view into an OwnedValue

Serde Integration

Serialize and deserialize Rust types directly to/from NBT binary format using serde.

Basic Usage

use serde::{Serialize, Deserialize};
use na_nbt::{to_vec_be, from_slice_be};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Player {
    name: String,
    health: f32,
    score: i32,
}

// Serialize to NBT
let player = Player {
    name: "Steve".to_string(),
    health: 20.0,
    score: 100,
};
let bytes = to_vec_be(&player).unwrap();

// Deserialize from NBT
let loaded: Player = from_slice_be(&bytes).unwrap();
assert_eq!(player, loaded);

Convenience Functions

Function Description
to_vec_be / to_vec_le Serialize to Vec<u8>
to_writer_be / to_writer_le Serialize to any io::Write
from_slice_be / from_slice_le Deserialize from &[u8]
from_reader_be / from_reader_le Deserialize from any io::Read

The _be suffix means big-endian (Java Edition), _le means little-endian (Bedrock Edition).

File I/O

use std::fs::File;
use na_nbt::{to_writer_be, from_reader_be};

// Write to file
let mut file = File::create("player.nbt")?;
to_writer_be(&mut file, &player)?;

// Read from file
let file = File::open("player.nbt")?;
let player: Player = from_reader_be(file)?;

Type Mapping

Rust Type NBT Tag
bool, i8, u8 Byte
i16, u16 Short
i32, u32, char Int
i64, u64 Long
f32 Float
f64 Double
String, &str String
Vec<T> List
struct Compound
HashMap<String, T> Compound
Option<T> Compound
enum variants Various (see docs)

Native Array Types

NBT has efficient native array types (ByteArray, IntArray, LongArray).

Deserialization is automatic! Native arrays are detected and read correctly to Vec<T>:

#[derive(Deserialize)]
struct ChunkData {
    block_states: Vec<i64>,  // Auto-detects LongArray or List<Long>
    biomes: Vec<i32>,        // Auto-detects IntArray or List<Int>
    heightmap: Vec<i8>,      // Auto-detects ByteArray or List<Byte>
}

For serialization, use #[serde(with = "...")] for zero-copy performance:

#[derive(Serialize, Deserialize)]
struct ChunkData {
    #[serde(with = "na_nbt::long_array")]
    block_states: Vec<i64>,  // Zero-copy → LongArray
    
    #[serde(with = "na_nbt::int_array")]
    biomes: Vec<i32>,        // Zero-copy → IntArray
    
    #[serde(with = "na_nbt::byte_array")]
    heightmap: Vec<i8>,      // Zero-copy → ByteArray
}

For full serde documentation, see the de and ser module docs.

Contributing

This crate is under active development. Contributions are welcome!

  • Bug reports: Please open an issue with a minimal reproducible example
  • Feature requests: Open an issue describing the use case
  • Pull requests: Fork the repo, make your changes, and submit a PR

License

MIT OR Apache-2.0