diff --git a/Cargo.lock b/Cargo.lock index c4ecf771..6c46763e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "azalea-nbt" +version = "0.1.0" +dependencies = [ + "byteorder", + "num-derive", + "num-traits", +] + [[package]] name = "azalea-protocol" version = "0.1.0" @@ -417,6 +426,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -673,11 +702,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -690,9 +718,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0cf441eb..d22c326b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ "azalea-chat", "azalea-core", "azalea-auth", + "azalea-nbt", ] diff --git a/azalea-nbt/Cargo.toml b/azalea-nbt/Cargo.toml new file mode 100644 index 00000000..38fa4123 --- /dev/null +++ b/azalea-nbt/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "azalea-nbt" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +byteorder = "^1.4.3" +num-derive = "^0.3.3" +num-traits = "^0.2.14" diff --git a/azalea-nbt/README.md b/azalea-nbt/README.md new file mode 100644 index 00000000..448c306b --- /dev/null +++ b/azalea-nbt/README.md @@ -0,0 +1,3 @@ +Deserialize Minecraft NBT. This is somewhat based on [Hermatite NBT](https://github.com/PistonDevelopers/hematite_nbt). + + diff --git a/azalea-nbt/src/decode.rs b/azalea-nbt/src/decode.rs new file mode 100644 index 00000000..704c8e2a --- /dev/null +++ b/azalea-nbt/src/decode.rs @@ -0,0 +1,111 @@ +use crate::Error; +use crate::Tag; +use byteorder::{ReadBytesExt, BE}; +use std::{collections::HashMap, io::Read}; + +impl Tag { + fn read_known(stream: &mut impl Read, id: u8) -> Result { + let tag = match id { + // Signifies the end of a TAG_Compound. It is only ever used inside + // a TAG_Compound, and is not named despite being in a TAG_Compound + 0 => Tag::End, + // A single signed byte + 1 => Tag::Byte(stream.read_i8().map_err(|_| Error::InvalidTag)?), + // A single signed, big endian 16 bit integer + 2 => Tag::Short(stream.read_i16::().map_err(|_| Error::InvalidTag)?), + // A single signed, big endian 32 bit integer + 3 => Tag::Int(stream.read_i32::().map_err(|_| Error::InvalidTag)?), + // A single signed, big endian 64 bit integer + 4 => Tag::Long(stream.read_i64::().map_err(|_| Error::InvalidTag)?), + // A single, big endian IEEE-754 single-precision floating point + // number (NaN possible) + 5 => Tag::Float(stream.read_f32::().map_err(|_| Error::InvalidTag)?), + // A single, big endian IEEE-754 double-precision floating point + // number (NaN possible) + 6 => Tag::Double(stream.read_f64::().map_err(|_| Error::InvalidTag)?), + // A length-prefixed array of signed bytes. The prefix is a signed + // integer (thus 4 bytes) + 7 => { + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut bytes = Vec::new(); + for _ in 0..length { + bytes.push(stream.read_i8().map_err(|_| Error::InvalidTag)?); + } + Tag::ByteArray(bytes) + } + // A length-prefixed modified UTF-8 string. The prefix is an + // unsigned short (thus 2 bytes) signifying the length of the + // string in bytes + 8 => { + let length = stream.read_u16::().map_err(|_| Error::InvalidTag)?; + let mut bytes = Vec::new(); + for _ in 0..length { + bytes.push(stream.read_u8().map_err(|_| Error::InvalidTag)?); + } + Tag::String(String::from_utf8(bytes).map_err(|_| Error::InvalidTag)?) + } + // A list of nameless tags, all of the same type. The list is + // prefixed with the Type ID of the items it contains (thus 1 + // byte), and the length of the list as a signed integer (a further + // 4 bytes). If the length of the list is 0 or negative, the type + // may be 0 (TAG_End) but otherwise it must be any other type. (The + // notchian implementation uses TAG_End in that situation, but + // another reference implementation by Mojang uses 1 instead; + // parsers should accept any type if the length is <= 0). + 9 => { + let type_id = stream.read_u8().map_err(|_| Error::InvalidTag)?; + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut list = Vec::new(); + for _ in 0..length { + list.push(Tag::read_known(stream, type_id)?); + } + Tag::List(list) + } + // Effectively a list of a named tags. Order is not guaranteed. + 10 => { + let mut map = HashMap::new(); + loop { + let tag_id = stream.read_u8().unwrap_or(0); + if tag_id == 0 { + break; + } + let name = match Tag::read_known(stream, 8)? { + Tag::String(name) => name, + _ => panic!("Expected a string tag"), + }; + let tag = Tag::read_known(stream, tag_id).map_err(|_| Error::InvalidTag)?; + map.insert(name, tag); + } + Tag::Compound(map) + } + // A length-prefixed array of signed integers. The prefix is a + // signed integer (thus 4 bytes) and indicates the number of 4 byte + // integers. + 11 => { + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut ints = Vec::new(); + for _ in 0..length { + ints.push(stream.read_i32::().map_err(|_| Error::InvalidTag)?); + } + Tag::IntArray(ints) + } + // A length-prefixed array of signed longs. The prefix is a signed + // integer (thus 4 bytes) and indicates the number of 8 byte longs. + 12 => { + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut longs = Vec::new(); + for _ in 0..length { + longs.push(stream.read_i64::().map_err(|_| Error::InvalidTag)?); + } + Tag::LongArray(longs) + } + _ => return Err(Error::InvalidTagType(id)), + }; + Ok(tag) + } + + pub fn read(stream: &mut impl Read) -> Result { + // default to compound tag + Tag::read_known(stream, 10) + } +} diff --git a/azalea-nbt/src/encode.rs b/azalea-nbt/src/encode.rs new file mode 100644 index 00000000..86c0d868 --- /dev/null +++ b/azalea-nbt/src/encode.rs @@ -0,0 +1,117 @@ +use byteorder::{ReadBytesExt, BE}; +use error::Error; +use std::{collections::HashMap, io::Read}; +use tag::Tag; + +impl Tag { + fn write(&self, stream: &mut impl Read) -> Result<(), Error> { + println!("read_known: id={}", id); + let tag = match id { + // Signifies the end of a TAG_Compound. It is only ever used inside + // a TAG_Compound, and is not named despite being in a TAG_Compound + 0 => Tag::End, + // A single signed byte + 1 => Tag::Byte(stream.read_i8().map_err(|_| Error::InvalidTag)?), + // A single signed, big endian 16 bit integer + 2 => Tag::Short(stream.read_i16::().map_err(|_| Error::InvalidTag)?), + // A single signed, big endian 32 bit integer + 3 => Tag::Int(stream.read_i32::().map_err(|_| Error::InvalidTag)?), + // A single signed, big endian 64 bit integer + 4 => Tag::Long(stream.read_i64::().map_err(|_| Error::InvalidTag)?), + // A single, big endian IEEE-754 single-precision floating point + // number (NaN possible) + 5 => Tag::Float(stream.read_f32::().map_err(|_| Error::InvalidTag)?), + // A single, big endian IEEE-754 double-precision floating point + // number (NaN possible) + 6 => Tag::Double(stream.read_f64::().map_err(|_| Error::InvalidTag)?), + // A length-prefixed array of signed bytes. The prefix is a signed + // integer (thus 4 bytes) + 7 => { + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut bytes = Vec::new(); + for _ in 0..length { + bytes.push(stream.read_i8().map_err(|_| Error::InvalidTag)?); + } + Tag::ByteArray(bytes) + } + // A length-prefixed modified UTF-8 string. The prefix is an + // unsigned short (thus 2 bytes) signifying the length of the + // string in bytes + 8 => { + let length = stream.read_u16::().map_err(|_| Error::InvalidTag)?; + let mut bytes = Vec::new(); + for _ in 0..length { + bytes.push(stream.read_u8().map_err(|_| Error::InvalidTag)?); + } + Tag::String(String::from_utf8(bytes).map_err(|_| Error::InvalidTag)?) + } + // A list of nameless tags, all of the same type. The list is + // prefixed with the Type ID of the items it contains (thus 1 + // byte), and the length of the list as a signed integer (a further + // 4 bytes). If the length of the list is 0 or negative, the type + // may be 0 (TAG_End) but otherwise it must be any other type. (The + // notchian implementation uses TAG_End in that situation, but + // another reference implementation by Mojang uses 1 instead; + // parsers should accept any type if the length is <= 0). + 9 => { + let type_id = stream.read_u8().map_err(|_| Error::InvalidTag)?; + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut list = Vec::new(); + for _ in 0..length { + list.push(Tag::read_known(stream, type_id)?); + } + Tag::List(list) + } + // Effectively a list of a named tags. Order is not guaranteed. + 10 => { + println!("reading compound {{"); + let mut map = HashMap::new(); + loop { + let tag_id = stream.read_u8().unwrap_or(0); + println!("compound tag id: {}", tag_id); + if tag_id == 0 { + break; + } + let name = match Tag::read_known(stream, 8)? { + Tag::String(name) => name, + _ => panic!("Expected a string tag"), + }; + println!("compound name: {}", name); + let tag = Tag::read_known(stream, tag_id).map_err(|_| Error::InvalidTag)?; + println!("aight read tag: {:?}", tag); + map.insert(name, tag); + } + println!("}} compound map: {:?}", map); + Tag::Compound(map) + } + // A length-prefixed array of signed integers. The prefix is a + // signed integer (thus 4 bytes) and indicates the number of 4 byte + // integers. + 11 => { + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut ints = Vec::new(); + for _ in 0..length { + ints.push(stream.read_i32::().map_err(|_| Error::InvalidTag)?); + } + Tag::IntArray(ints) + } + // A length-prefixed array of signed longs. The prefix is a signed + // integer (thus 4 bytes) and indicates the number of 8 byte longs. + 12 => { + let length = stream.read_i32::().map_err(|_| Error::InvalidTag)?; + let mut longs = Vec::new(); + for _ in 0..length { + longs.push(stream.read_i64::().map_err(|_| Error::InvalidTag)?); + } + Tag::LongArray(longs) + } + _ => return Err(Error::InvalidTagType(id)), + }; + Ok(tag) + } + + pub fn read(stream: &mut impl Read) -> Result { + // default to compound tag + Tag::read_known(stream, 10) + } +} diff --git a/azalea-nbt/src/error.rs b/azalea-nbt/src/error.rs new file mode 100644 index 00000000..149a00e9 --- /dev/null +++ b/azalea-nbt/src/error.rs @@ -0,0 +1,5 @@ +#[derive(Debug)] +pub enum Error { + InvalidTagType(u8), + InvalidTag, +} diff --git a/azalea-nbt/src/lib.rs b/azalea-nbt/src/lib.rs new file mode 100644 index 00000000..adeb6e56 --- /dev/null +++ b/azalea-nbt/src/lib.rs @@ -0,0 +1,6 @@ +mod decode; +mod error; +mod tag; + +pub use error::Error; +pub use tag::Tag; diff --git a/azalea-nbt/src/tag.rs b/azalea-nbt/src/tag.rs new file mode 100644 index 00000000..1f496c9d --- /dev/null +++ b/azalea-nbt/src/tag.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +#[derive(Debug, PartialEq)] +pub enum Tag { + End, // 0 + Byte(i8), // 1 + Short(i16), // 2 + Int(i32), // 3 + Long(i64), // 4 + Float(f32), // 5 + Double(f64), // 6 + ByteArray(Vec), // 7 + String(String), // 8 + List(Vec), // 9 + Compound(HashMap), // 10 + IntArray(Vec), // 11 + LongArray(Vec), // 12 +} diff --git a/azalea-nbt/tests/bigtest.nbt b/azalea-nbt/tests/bigtest.nbt new file mode 100644 index 00000000..dc3769bc Binary files /dev/null and b/azalea-nbt/tests/bigtest.nbt differ diff --git a/azalea-nbt/tests/decode.rs b/azalea-nbt/tests/decode.rs new file mode 100644 index 00000000..f4060512 --- /dev/null +++ b/azalea-nbt/tests/decode.rs @@ -0,0 +1,19 @@ +use azalea_nbt::Tag; +use std::collections::HashMap; + +#[test] +fn test_hello_world() { + // read hello_world.nbt + let mut file = std::fs::File::open("tests/hello_world.nbt").unwrap(); + let tag = Tag::read(&mut file).unwrap(); + assert_eq!( + tag, + Tag::Compound(HashMap::from_iter(vec![( + "hello world".to_string(), + Tag::Compound(HashMap::from_iter(vec![( + "name".to_string(), + Tag::String("Bananrama".to_string()), + )])) + )])) + ); +} diff --git a/azalea-nbt/tests/hello_world.nbt b/azalea-nbt/tests/hello_world.nbt new file mode 100644 index 00000000..f3f5e21a Binary files /dev/null and b/azalea-nbt/tests/hello_world.nbt differ