Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Serde support #15

Merged
merged 2 commits into from
Sep 1, 2024
Merged
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
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.*

```rust
use crab_nbt::serde::{arrays::IntArray, ser::to_bytes_unnamed, de::from_bytes_unnamed};
use serde::{Deserialize, Serialize};

#[derive(Serialize, 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 mut bytes = to_bytes_unnamed(&test).unwrap();
let recreated_struct: Test = from_bytes_unnamed(&mut bytes).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);
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);
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