Skip to content

Commit

Permalink
feat: Serde support
Browse files Browse the repository at this point in the history
  • Loading branch information
Norbiros committed Sep 1, 2024
1 parent d033f25 commit 7d9209c
Show file tree
Hide file tree
Showing 11 changed files with 766 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Run Clippy
run: cargo clippy --all-targets --all-features
run: cargo clippy --all-features
- name: Run tests
run: cargo test --verbose
run: cargo test --all-features --verbose
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ readme = "README.md"
license = "GPL-3.0-only"
version = "0.1.2"
edition = "2021"
exclude = ["tests"]
exclude = ["tests", ".github"]

[features]
full = ["macro"]
full = ["macro", "serde"]
default = ["macro"]
macro = []
serde = ["dep:serde"]

[dependencies]
bytes = "1.6.0"
cesu8 = "1.1.0"
derive_more = { version = "1.0.0-beta.6", features = ["into", "from"] }
thiserror = "1.0.61"
serde = { version = "1.0.209", optional = true, features = ["derive"] }
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
Up-to-date Rust crate for easy and intuitive working with NBT data.

## Why not other libraries?
CrabNBT combines best features of existing NBT crates, to create perfect solution.<br>
CrabNBT combines best features of existing NBT crates, to create perfect, easy to use solution.<br>
Big thanks to [simdnbt](https://github.com/azalea-rs/simdnbt) and [fastnbt](https://github.com/owengage/fastnbt) for ideas!

## Features
🚧 Support for serializing to/from Struct *(soon)*<br>
Support for serializing to/from Struct (serde)<br>
[Java string](https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8) support <br>
NBT! macro for easy creation <br>
Good system of getting values from NBT <br>
Serializing for single tags <br>
✅ Support of [Network NBT](https://wiki.vg/NBT#Network_NBT_(Java_Edition))
`nbt!` macro for easy creation <br>
Easy to use system of retrieving values from NBT <br>
Serialization support for individual tags <br>
✅ Support for [Network NBT](https://wiki.vg/NBT#Network_NBT_(Java_Edition))

## Installing
```shell
Expand Down Expand Up @@ -45,6 +45,9 @@ let nbt = Nbt::new(
]).into())
])
);

let network_bytes = nbt.write_unnamed();
let normal_bytes = nbt.write();
```

## Deserializing
Expand All @@ -62,3 +65,29 @@ fn example(bytes: &mut Bytes) {
.unwrap();
}
```

## Serde
*Requires `serde` feature.*

```ignore
use crab_nbt::serde::arrays::IntArray;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, PartialEq, Debug)]
struct Test {
number: i32,
#[serde(with = "IntArray")]
int_array: Vec<i32>,
}
fn cycle() {
let test = Test {
number: 5,
int_array: vec![7, 8]
};
let bytes = to_bytes_unnamed(&test).unwrap();
let recreated_struct: Test = from_bytes_unnamed(&mut expected).unwrap();
assert_eq!(test, recreated_struct);
}
```
26 changes: 25 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
#[cfg(feature = "serde")]
use serde::{de, ser};
#[cfg(feature = "serde")]
use std::fmt::Display;
use thiserror::Error;

#[derive(Error, Copy, Clone, Debug)]
pub type Result<T> = std::result::Result<T, Error>;

#[derive(Error, Clone, Debug)]
pub enum Error {
#[error("The root tag of the NBT file is not a compound tag. Received tag id: {0}")]
NoRootCompound(u8),
#[error("The provided string is not a valid Java string.")]
InvalidJavaString,
#[error("Encountered an unknown NBT tag id {0}.")]
UnknownTagId(u8),
#[error("Serde error: {0}")]
SerdeError(String),
#[error("NBT doesn't support this type {0}")]
UnsupportedType(String),
}

#[cfg(feature = "serde")]
impl de::Error for Error {
fn custom<T: Display>(msg: T) -> Self {
Error::SerdeError(msg.to_string())
}
}

#[cfg(feature = "serde")]
impl ser::Error for Error {
fn custom<T: Display>(msg: T) -> Self {
Error::SerdeError(msg.to_string())
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
pub mod error;
mod macros;
mod nbt;
#[cfg(feature = "serde")]
pub mod serde;

pub use crab_nbt::nbt::compound::NbtCompound;
pub use crab_nbt::nbt::tag::NbtTag;
Expand Down
2 changes: 1 addition & 1 deletion src/nbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::ops::Deref;

pub mod compound;
pub mod tag;
mod utils;
pub mod utils;

/// Represents the main NBT structure.
/// It contains the root compound tag of the NBT structure and its associated name
Expand Down
33 changes: 33 additions & 0 deletions src/serde/arrays.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use serde::{Deserialize, Deserializer};

/// Crates structs, that allow for serializing fields as arrays (not lists)
/// ```ignore
/// #[serde(with = IntArray)]
/// ```
macro_rules! impl_array {
($name:ident, $variant:expr) => {
pub struct $name;

impl $name {
pub fn serialize<T, S>(input: T, serializer: S) -> Result<S::Ok, S::Error>
where
T: serde::Serialize,
S: serde::Serializer,
{
serializer.serialize_newtype_variant("nbt_array", 0, $variant, &input)
}

pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
T::deserialize(deserializer)
}
}
};
}

impl_array!(IntArray, "int");
impl_array!(LongArray, "long");
impl_array!(BytesArray, "byte");
201 changes: 201 additions & 0 deletions src/serde/de.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use crate::error::{Error, Result};
use crate::nbt::utils::{
BYTE_ARRAY_ID, BYTE_ID, COMPOUND_ID, INT_ARRAY_ID, INT_ID, LIST_ID, LONG_ARRAY_ID, LONG_ID,
};
use crate::NbtTag;
use bytes::{Buf, BytesMut};
use crab_nbt::nbt::utils::{get_nbt_string, END_ID};
use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor};
use serde::{forward_to_deserialize_any, Deserialize};

#[derive(Debug)]
pub struct Deserializer<'de> {
input: &'de mut BytesMut,
tag_to_deserialize: Option<u8>,
is_named: bool,
}

impl<'de> Deserializer<'de> {
pub fn new(input: &'de mut BytesMut, is_named: bool) -> Self {
Deserializer {
input,
tag_to_deserialize: None,
is_named,
}
}
}

/// Deserializes struct using Serde Deserializer from unnamed (network) NBT
pub fn from_bytes<'a, T>(s: &'a mut BytesMut) -> Result<T>
where
T: Deserialize<'a>,
{
let mut deserializer = Deserializer::new(s, true);
Ok(T::deserialize(&mut deserializer)?)
}

/// Deserializes struct using Serde Deserializer from normal NBT
pub fn from_bytes_unnamed<'a, T>(s: &'a mut BytesMut) -> Result<T>
where
T: Deserialize<'a>,
{
let mut deserializer = Deserializer::new(s, false);
Ok(T::deserialize(&mut deserializer)?)
}

impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
type Error = Error;

forward_to_deserialize_any!(i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 seq char str string bytes byte_buf tuple tuple_struct enum ignored_any unit unit_struct option newtype_struct);

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
let tag_to_deserialize = self.tag_to_deserialize.unwrap();

let list_type = match tag_to_deserialize {
LIST_ID => Some(self.input.get_u8()),
INT_ARRAY_ID => Some(INT_ID),
LONG_ARRAY_ID => Some(LONG_ID),
BYTE_ARRAY_ID => Some(BYTE_ID),
_ => None,
};

if let Some(list_type) = list_type {
let remaining_values = self.input.get_u32();
return visitor.visit_seq(ListAccess {
de: self,
list_type,
remaining_values,
});
}

let res: Result<V::Value> = Ok(
match NbtTag::deserialize_data(self.input, tag_to_deserialize)? {
NbtTag::End => {
unimplemented!("end")
}
NbtTag::Byte(value) => visitor.visit_i8(value)?,
NbtTag::Short(value) => visitor.visit_i16(value)?,
NbtTag::Int(value) => visitor.visit_i32(value)?,
NbtTag::Long(value) => visitor.visit_i64(value)?,
NbtTag::Float(value) => visitor.visit_f32(value)?,
NbtTag::Double(value) => visitor.visit_f64(value)?,
NbtTag::String(value) => visitor.visit_string(value)?,
_ => unreachable!(),
},
);
self.tag_to_deserialize = None;
res
}

fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
if self.tag_to_deserialize.unwrap() == BYTE_ID {
let value = self.input.get_u8();
if value != 0 {
return visitor.visit_bool(true);
}
}
visitor.visit_bool(false)
}

fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
if self.tag_to_deserialize.is_none() {
let next_byte = self.input.get_u8();
if next_byte != COMPOUND_ID {
return Err(Error::NoRootCompound(next_byte));
}

if self.is_named {
// Consume struct name
NbtTag::deserialize(self.input)?;
}
}

let value = visitor.visit_map(CompoundAccess { de: self })?;
Ok(value)
}

fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_map(visitor)
}

fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
let str = get_nbt_string(&mut self.input)?;
visitor.visit_string(str)
}

fn is_human_readable(&self) -> bool {
false
}
}

struct CompoundAccess<'a, 'de: 'a> {
de: &'a mut Deserializer<'de>,
}

impl<'de, 'a> MapAccess<'de> for CompoundAccess<'a, 'de> {
type Error = Error;

fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
where
K: DeserializeSeed<'de>,
{
let tag = self.de.input.get_u8();
self.de.tag_to_deserialize = Some(tag);

if tag == END_ID {
return Ok(None);
}

seed.deserialize(&mut *self.de).map(Some)
}

fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
where
V: DeserializeSeed<'de>,
{
seed.deserialize(&mut *self.de)
}
}

struct ListAccess<'a, 'de: 'a> {
de: &'a mut Deserializer<'de>,
remaining_values: u32,
list_type: u8,
}

impl<'a, 'de> SeqAccess<'de> for ListAccess<'a, 'de> {
type Error = Error;

fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
where
T: DeserializeSeed<'de>,
{
if self.remaining_values == 0 {
return Ok(None);
}

self.remaining_values -= 1;
self.de.tag_to_deserialize = Some(self.list_type);
seed.deserialize(&mut *self.de).map(Some)
}
}
3 changes: 3 additions & 0 deletions src/serde/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod arrays;
pub mod de;
pub mod ser;
Loading

0 comments on commit 7d9209c

Please sign in to comment.