diff --git a/Cargo.lock b/Cargo.lock index 140aa01..e3a1a14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,49 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "nbt" version = "0.1.0" +dependencies = [ + "flate2", +] diff --git a/Cargo.toml b/Cargo.toml index a81a699..9659195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +flate2 = "1.1.2" diff --git a/src/io.rs b/src/io.rs index 77f4b4a..c7194d4 100644 --- a/src/io.rs +++ b/src/io.rs @@ -4,12 +4,28 @@ use std::{ }; use crate::{Tag, TagId}; +use flate2::{Compression, read::GzDecoder, write::GzEncoder}; /// Binary reader for NBT format pub struct Reader { inner: R, } +// --- Reader gzip --- +impl Reader> { + pub fn from_gzip(inner: R) -> Self { + Reader { + inner: GzDecoder::new(inner), + } + } +} +impl Writer> { + pub fn to_gzip(inner: W) -> Self { + Writer { + inner: GzEncoder::new(inner, Compression::default()), + } + } +} impl Reader { pub fn new(inner: R) -> Self { Reader { inner } @@ -134,13 +150,22 @@ impl Reader { Ok(i64::from_be_bytes(buf)) } fn read_f32(&mut self) -> Result { - Ok(f32::from_bits(self.read_i32()? as u32)) + let mut b = [0u8; 4]; + self.inner.read_exact(&mut b)?; + Ok(f32::from_bits(u32::from_be_bytes(b))) } fn read_f64(&mut self) -> Result { - Ok(f64::from_bits(self.read_i64()? as u64)) + let mut b = [0u8; 8]; + self.inner.read_exact(&mut b)?; + Ok(f64::from_bits(u64::from_be_bytes(b))) + } + fn read_u16(&mut self) -> Result { + let mut b = [0u8; 2]; + self.inner.read_exact(&mut b)?; + Ok(u16::from_be_bytes(b)) } fn read_string(&mut self) -> Result { - let len = self.read_i16()? as usize; + let len = self.read_u16()? as usize; let mut buf = vec![0u8; len]; self.inner.read_exact(&mut buf)?; String::from_utf8(buf).map_err(|e| Error::new(ErrorKind::InvalidData, e)) @@ -240,14 +265,17 @@ impl Writer { self.inner.write_all(&v.to_be_bytes()) } fn write_f32(&mut self, v: f32) -> Result<()> { - self.write_i32(v.to_bits() as i32) + self.inner.write_all(&v.to_bits().to_be_bytes()) } fn write_f64(&mut self, v: f64) -> Result<()> { - self.write_i64(v.to_bits() as i64) + self.inner.write_all(&v.to_bits().to_be_bytes()) } fn write_string(&mut self, s: &str) -> Result<()> { let bytes = s.as_bytes(); - self.write_i16(bytes.len() as i16)?; + if bytes.len() > u16::MAX as usize { + return Err(Error::new(ErrorKind::InvalidInput, "string too long")); + } + self.inner.write_all(&(bytes.len() as u16).to_be_bytes())?; self.inner.write_all(bytes) } } diff --git a/src/test.rs b/src/test.rs index e216afb..d7dc072 100644 --- a/src/test.rs +++ b/src/test.rs @@ -164,3 +164,324 @@ fn roundtrip_compound() { let decoded = read_nbt(&buf[..]).unwrap(); assert_eq!(decoded, root); } + +// --------------------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use std::io::{Cursor, Read}; + + use flate2::bufread::GzDecoder; + + use crate::{Reader, Tag, Writer}; + + fn sample_level_compound() -> Tag { + use std::collections::HashMap; + let mut entries = HashMap::new(); + entries.insert( + "DataVersion".into(), + Tag::Int { + name: Some("DataVersion".into()), + value: 3837, + }, + ); + entries.insert( + "LevelName".into(), + Tag::String { + name: Some("LevelName".into()), + value: "test".into(), + }, + ); + entries.insert( + "GameType".into(), + Tag::Int { + name: Some("GameType".into()), + value: 0, + }, + ); + entries.insert( + "Difficulty".into(), + Tag::Byte { + name: Some("Difficulty".into()), + value: 2, + }, + ); + entries.insert( + "hardcore".into(), + Tag::Byte { + name: Some("hardcore".into()), + value: 0, + }, + ); + entries.insert( + "allowCommands".into(), + Tag::Byte { + name: Some("allowCommands".into()), + value: 1, + }, + ); + entries.insert( + "SpawnX".into(), + Tag::Int { + name: Some("SpawnX".into()), + value: 0, + }, + ); + entries.insert( + "SpawnY".into(), + Tag::Int { + name: Some("SpawnY".into()), + value: 64, + }, + ); + entries.insert( + "SpawnZ".into(), + Tag::Int { + name: Some("SpawnZ".into()), + value: 0, + }, + ); + + let list = Tag::List { + name: Some("TestList".into()), + element_id: 3, + elements: vec![ + Tag::Int { + name: None, + value: 1, + }, + Tag::Int { + name: None, + value: 2, + }, + Tag::Int { + name: None, + value: 3, + }, + ], + }; + entries.insert("TestList".into(), list); + Tag::Compound { + name: Some("Data".into()), + entries, + } + } + + fn write_uncompressed(tag: &Tag) -> Vec { + let mut buf = Cursor::new(Vec::new()); + let mut w = Writer::new(&mut buf); + w.write_tag(tag).unwrap(); + buf.into_inner() + } + + fn read_uncompressed(bytes: &[u8]) -> Tag { + let mut r = Reader::new(Cursor::new(bytes)); + r.read_tag().unwrap() + } + + #[test] + fn roundtrip_uncompressed() { + let root = sample_level_compound(); + let bytes = write_uncompressed(&root); + let back = read_uncompressed(&bytes); + + match back { + Tag::Compound { name, .. } => assert_eq!(name.as_deref(), Some("Data")), + _ => panic!("root not a Compound"), + } + } + + #[test] + fn list_elements_have_no_names() { + let root = sample_level_compound(); + let bytes = write_uncompressed(&root); + let back = read_uncompressed(&bytes); + let Tag::Compound { entries, .. } = back else { + panic!("not compound"); + }; + let Tag::List { + element_id, + elements, + .. + } = entries.get("TestList").expect("missing TestList") + else { + panic!("not list"); + }; + assert_eq!(*element_id, 3); + assert_eq!(elements.len(), 3); + for e in elements { + match e { + Tag::Int { name, value } => { + assert!(name.is_none()); + assert!(*value >= 1 && *value <= 3); + } + _ => panic!("list element not Int"), + } + } + } + + #[test] + fn string_length_u16_boundary() { + let s = "a".repeat(300); + let tag = Tag::String { + name: Some("S".into()), + value: s.clone(), + }; + let root = Tag::Compound { + name: Some("Data".into()), + entries: { + let mut m = std::collections::HashMap::new(); + m.insert("S".into(), tag); + m + }, + }; + let bytes = write_uncompressed(&root); + let back = read_uncompressed(&bytes); + let Tag::Compound { entries, .. } = back else { + panic!("not compound"); + }; + let Tag::String { value, .. } = entries.get("S").expect("missing S") else { + panic!("not string"); + }; + assert_eq!(value, &s); + } + + #[test] + fn roundtrip_gzip() { + let root = sample_level_compound(); + + let mut out = Vec::new(); + { + let cursor = Cursor::new(&mut out); + let mut w = Writer::to_gzip(cursor); + w.write_tag(&root).unwrap(); + } + + assert!(out.len() >= 2); + assert_eq!(out[0], 0x1f); + assert_eq!(out[1], 0x8b); + + let mut r = Reader::from_gzip(Cursor::new(&out)); + let back = r.read_tag().unwrap(); + match back { + Tag::Compound { name, .. } => assert_eq!(name.as_deref(), Some("Data")), + _ => panic!("root not a Compound"), + } + } + + #[test] + fn error_on_unknown_tag_id() { + let mut raw = Vec::new(); + raw.push(99u8); + raw.extend_from_slice(&0u16.to_be_bytes()); + let mut r = Reader::new(Cursor::new(raw)); + let err = r.read_tag().err().expect("should error"); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn compound_end_marker_required() { + let mut raw = Vec::new(); + raw.push(10u8); + raw.extend_from_slice(&(4u16.to_be_bytes())); + raw.extend_from_slice(b"Data"); + + let mut r = Reader::new(Cursor::new(raw)); + let res = r.read_tag(); + assert!(res.is_err(), "Reader should fail on missing TAG_End"); + } + + #[test] + fn float_and_double_are_be() { + let mut entries = std::collections::HashMap::new(); + entries.insert( + "F".into(), + Tag::Float { + name: Some("F".into()), + value: 1234.5f32, + }, + ); + entries.insert( + "D".into(), + Tag::Double { + name: Some("D".into()), + value: -0.25f64, + }, + ); + let root = Tag::Compound { + name: Some("Data".into()), + entries, + }; + + let bytes = write_uncompressed(&root); + + let back = read_uncompressed(&bytes); + let Tag::Compound { entries, .. } = back else { + panic!("not compound"); + }; + let Tag::Float { value: f, .. } = entries.get("F").unwrap() else { + panic!("F missing"); + }; + let Tag::Double { value: d, .. } = entries.get("D").unwrap() else { + panic!("D missing"); + }; + assert!((*f - 1234.5).abs() < 1e-4); + assert!((*d + 0.25).abs() < 1e-12); + } + + #[test] + fn int_long_arrays_roundtrip() { + let ia = (0..16).map(|i| i - 8).collect::>(); + let la = (0..8).map(|i| (i as i64) * (1 << 33)).collect::>(); + let mut entries = std::collections::HashMap::new(); + entries.insert( + "IA".into(), + Tag::IntArray { + name: Some("IA".into()), + value: ia.clone(), + }, + ); + entries.insert( + "LA".into(), + Tag::LongArray { + name: Some("LA".into()), + value: la.clone(), + }, + ); + let root = Tag::Compound { + name: Some("Data".into()), + entries, + }; + + let bytes = write_uncompressed(&root); + let back = read_uncompressed(&bytes); + + let Tag::Compound { entries, .. } = back else { + panic!("not compound"); + }; + let Tag::IntArray { value: ia2, .. } = entries.get("IA").unwrap() else { + panic!("IA"); + }; + let Tag::LongArray { value: la2, .. } = entries.get("LA").unwrap() else { + panic!("LA"); + }; + assert_eq!(ia2, &ia); + assert_eq!(la2, &la); + } + + #[test] + fn gzip_stream_is_valid_by_gzdecoder() { + let root = sample_level_compound(); + let mut out = Vec::new(); + { + let cursor = Cursor::new(&mut out); + let mut w = Writer::to_gzip(cursor); + w.write_tag(&root).unwrap(); + } + let mut dec = GzDecoder::new(Cursor::new(&out)); + let mut raw = Vec::new(); + dec.read_to_end(&mut raw).unwrap(); + assert!(!raw.is_empty()); + + assert_eq!(raw[0], 10u8); + } +}