From 76e1985fc4ab21c43e17fef685b17a567b1073a5 Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 18 Dec 2021 17:02:23 -0600 Subject: [PATCH] reading nbt --- Cargo.lock | 38 ++++++++-- Cargo.toml | 1 + azalea-nbt/Cargo.toml | 11 +++ azalea-nbt/README.md | 3 + azalea-nbt/src/decode.rs | 111 +++++++++++++++++++++++++++++ azalea-nbt/src/encode.rs | 117 +++++++++++++++++++++++++++++++ azalea-nbt/src/error.rs | 5 ++ azalea-nbt/src/lib.rs | 6 ++ azalea-nbt/src/tag.rs | 18 +++++ azalea-nbt/tests/bigtest.nbt | Bin 0 -> 507 bytes azalea-nbt/tests/decode.rs | 19 +++++ azalea-nbt/tests/hello_world.nbt | Bin 0 -> 33 bytes 12 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 azalea-nbt/Cargo.toml create mode 100644 azalea-nbt/README.md create mode 100644 azalea-nbt/src/decode.rs create mode 100644 azalea-nbt/src/encode.rs create mode 100644 azalea-nbt/src/error.rs create mode 100644 azalea-nbt/src/lib.rs create mode 100644 azalea-nbt/src/tag.rs create mode 100644 azalea-nbt/tests/bigtest.nbt create mode 100644 azalea-nbt/tests/decode.rs create mode 100644 azalea-nbt/tests/hello_world.nbt 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 0000000000000000000000000000000000000000..dc3769bcd06cca599974396faaf5ce42ef4143b3 GIT binary patch literal 507 zcmVFFmV&Xw5M#`%wS=YH%|a20v4|EdnhhG* zs4<4PE5m{=yJmN=@zhIuYI4wZzg#H1XOZQ zEDm_fu}zJ5^y6@1J_vgq$EA}P4}wSC?t}tjwW6xWcy?S@%cxZk8_3okYL$kD4Xu7y zdyj+9gHMBR&jS!{TaG@yr8rDv{SfNfbfzOf+-5Fm;kDDdbNY4*DccL+@8~@qI9u-# z2v+spUEd2p;9j@-WVZwWj6qCu#t2nR(;zN=q`=6+5VN}8SPN65?nI7712C~CQ;baU z=@g?=jD_LZpX0OgM1iGzGu_y`$EtM`Unm?1*DldnKd&7dU?ExG`tb$+!Or}hy#T!N zK*{)pLO@3Tp6lullR9XJV7u!wH=`&Dj=S~HX=BPx+v)7)<|{kBCB9@y2|cR2l>Hcf z=+X|_Zxu|jXg(|9o1BE1yo3b_Wmy(Q0RJy2t}pV!vZO8@Aa`0jxs1x^Dc?~+$riQ6 zZJ{cyEX6T@x_Xk1UY%d~5(Oh0(e}4@s?C*hyq@1!T}zj)k{7u|(16JLKEJcvRLci- xZlkt#S(1~f+)+@OYs@v~8vj=#2tv#08`gNj?Ed_E`+a!Rgx}Qir27a4007^G{X+l% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f3f5e21aacfc8ce832a459c6c6d827127109cb5b GIT binary patch literal 33 ocmd;L;Lb?R$;nqJ&o9bJ;b36NOUzAW;B-pNOUx@u%uQqf0G#LvsQ>@~ literal 0 HcmV?d00001