From a580e421f057dfff7e63784d81d9fc9e34976240 Mon Sep 17 00:00:00 2001 From: Spectre Date: Mon, 19 May 2025 14:43:14 +0200 Subject: [PATCH] a bit more --- src/io.rs | 255 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 89 ++++++++++++++- src/test.rs | 311 +++++++++++++++++++++++++++------------------------- 3 files changed, 503 insertions(+), 152 deletions(-) create mode 100644 src/io.rs diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..877927b --- /dev/null +++ b/src/io.rs @@ -0,0 +1,255 @@ +// src/io.rs + +use std::{ + collections::HashMap, + io::{Error, ErrorKind, Read, Result, Write}, +}; + +use crate::{Tag, TagId}; + +/// Binary reader for NBT format +pub struct Reader { + inner: R, +} + +impl Reader { + pub fn new(inner: R) -> Self { + Reader { inner } + } + + /// Read a full tag (ID + name + payload) + pub fn read_tag(&mut self) -> Result { + let id = self.read_u8()?; + if id == 0 { + return Ok(Tag::End); + } + let name = Some(self.read_string()?); + self.read_payload(id, name) + } + + fn read_payload(&mut self, id: TagId, name: Option) -> Result { + match id { + 1 => Ok(Tag::Byte { + name, + value: self.read_i8()?, + }), + 2 => Ok(Tag::Short { + name, + value: self.read_i16()?, + }), + 3 => Ok(Tag::Int { + name, + value: self.read_i32()?, + }), + 4 => Ok(Tag::Long { + name, + value: self.read_i64()?, + }), + 5 => Ok(Tag::Float { + name, + value: self.read_f32()?, + }), + 6 => Ok(Tag::Double { + name, + value: self.read_f64()?, + }), + 7 => { + let len = self.read_i32()? as usize; + let mut buf = vec![0u8; len]; + self.inner.read_exact(&mut buf)?; + Ok(Tag::ByteArray { name, value: buf }) + } + 8 => { + let s = self.read_string()?; + Ok(Tag::String { name, value: s }) + } + 9 => { + let elem_id = self.read_u8()?; + let len = self.read_i32()? as usize; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(self.read_payload(elem_id, None)?); + } + Ok(Tag::List { + name, + element_id: elem_id, + elements, + }) + } + 10 => { + let mut entries = HashMap::new(); + loop { + let id = self.read_u8()?; + if id == 0 { + break; + } + let key = self.read_string()?; + let tag = self.read_payload(id, Some(key.clone()))?; + entries.insert(key, tag); + } + Ok(Tag::Compound { name, entries }) + } + 11 => { + let len = self.read_i32()? as usize; + let mut v = Vec::with_capacity(len); + for _ in 0..len { + v.push(self.read_i32()?); + } + Ok(Tag::IntArray { name, value: v }) + } + 12 => { + let len = self.read_i32()? as usize; + let mut v = Vec::with_capacity(len); + for _ in 0..len { + v.push(self.read_i64()?); + } + Ok(Tag::LongArray { name, value: v }) + } + other => Err(Error::new( + ErrorKind::InvalidData, + format!("Unknown tag id {}", other), + )), + } + } + + fn read_u8(&mut self) -> Result { + let mut buf = [0u8; 1]; + self.inner.read_exact(&mut buf)?; + Ok(buf[0]) + } + fn read_i8(&mut self) -> Result { + Ok(self.read_u8()? as i8) + } + fn read_i16(&mut self) -> Result { + let mut buf = [0u8; 2]; + self.inner.read_exact(&mut buf)?; + Ok(i16::from_be_bytes(buf)) + } + fn read_i32(&mut self) -> Result { + let mut buf = [0u8; 4]; + self.inner.read_exact(&mut buf)?; + Ok(i32::from_be_bytes(buf)) + } + fn read_i64(&mut self) -> Result { + let mut buf = [0u8; 8]; + self.inner.read_exact(&mut buf)?; + Ok(i64::from_be_bytes(buf)) + } + fn read_f32(&mut self) -> Result { + Ok(f32::from_bits(self.read_i32()? as u32)) + } + fn read_f64(&mut self) -> Result { + Ok(f64::from_bits(self.read_i64()? as u64)) + } + fn read_string(&mut self) -> Result { + let len = self.read_i16()? 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)) + } +} + +/// Binary writer for NBT format +pub struct Writer { + inner: W, +} + +impl Writer { + pub fn new(inner: W) -> Self { + Writer { inner } + } + + /// Write a full tag (ID + name + payload) + pub fn write_tag(&mut self, tag: &Tag) -> Result<()> { + let id = tag.id(); + self.write_u8(id)?; + if id != 0 { + if let Some(name) = tag.name() { + self.write_string(name)?; + } else { + self.write_string("")?; + } + self.write_payload(tag)?; + } + Ok(()) + } + + fn write_payload(&mut self, tag: &Tag) -> Result<()> { + match tag { + Tag::End => {} + Tag::Byte { value, .. } => self.write_i8(*value)?, + Tag::Short { value, .. } => self.write_i16(*value)?, + Tag::Int { value, .. } => self.write_i32(*value)?, + Tag::Long { value, .. } => self.write_i64(*value)?, + Tag::Float { value, .. } => self.write_f32(*value)?, + Tag::Double { value, .. } => self.write_f64(*value)?, + Tag::ByteArray { value, .. } => { + self.write_i32(value.len() as i32)?; + self.inner.write_all(value)?; + } + Tag::String { value, .. } => { + self.write_string(value)?; + } + Tag::List { + element_id, + elements, + .. + } => { + self.write_u8(*element_id)?; + self.write_i32(elements.len() as i32)?; + for elem in elements { + self.write_payload(elem)?; // lists omit names + } + } + Tag::Compound { entries, .. } => { + for (key, entry) in entries { + let id = entry.id(); + self.write_u8(id)?; + self.write_string(key)?; + self.write_payload(entry)?; + } + self.write_u8(0)?; // TAG_End + } + Tag::IntArray { value, .. } => { + self.write_i32(value.len() as i32)?; + for &i in value { + self.write_i32(i)?; + } + } + Tag::LongArray { value, .. } => { + self.write_i32(value.len() as i32)?; + for &l in value { + self.write_i64(l)?; + } + } + } + Ok(()) + } + + fn write_u8(&mut self, v: u8) -> Result<()> { + self.inner.write_all(&[v]) + } + fn write_i8(&mut self, v: i8) -> Result<()> { + self.write_u8(v as u8) + } + fn write_i16(&mut self, v: i16) -> Result<()> { + self.inner.write_all(&v.to_be_bytes()) + } + fn write_i32(&mut self, v: i32) -> Result<()> { + self.inner.write_all(&v.to_be_bytes()) + } + fn write_i64(&mut self, v: i64) -> Result<()> { + self.inner.write_all(&v.to_be_bytes()) + } + fn write_f32(&mut self, v: f32) -> Result<()> { + self.write_i32(v.to_bits() as i32) + } + fn write_f64(&mut self, v: f64) -> Result<()> { + self.write_i64(v.to_bits() as i64) + } + fn write_string(&mut self, s: &str) -> Result<()> { + let bytes = s.as_bytes(); + self.write_i16(bytes.len() as i16)?; + self.inner.write_all(bytes) + } +} diff --git a/src/lib.rs b/src/lib.rs index 13c9016..7925435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,15 @@ +// src/lib.rs + use std::collections::HashMap; +pub mod io; mod test; -// Id of nbt +pub use io::{Reader, Writer}; + +/// Identifier for an NBT tag type pub type TagId = u8; -// All the types of nbt -#[derive(PartialEq,Clone,Debug,)] +/// All NBT tag types +#[derive(PartialEq, Clone, Debug)] pub enum Tag { End, Byte { @@ -95,7 +100,7 @@ impl Tag { value: v, } } - pub fn new_ByteArray(name: impl Into, v: Vec) -> Tag { + pub fn new_byte_array(name: impl Into, v: Vec) -> Tag { Tag::ByteArray { name: Some(name.into()), value: v, @@ -134,3 +139,79 @@ impl Tag { } } +impl Tag { + /// Insert a sub-tag into a Compound + pub fn insert(&mut self, key: String, tag: Tag) { + if let Tag::Compound { entries, .. } = self { + entries.insert(key, tag); + } else { + panic!("insert() called on non-Compound"); + } + } + + /// Retrieve a sub-tag from a Compound + pub fn get(&self, key: &str) -> Option<&Tag> { + if let Tag::Compound { entries, .. } = self { + entries.get(key) + } else { + None + } + } + + /// Add an element to a List + pub fn push(&mut self, tag: Tag) { + if let Tag::List { elements, .. } = self { + elements.push(tag); + } else { + panic!("push() called on non-List"); + } + } + + /// Numeric ID of this tag (0 = End, 1 = Byte, …, 12 = LongArray) + pub fn id(&self) -> TagId { + match self { + Tag::End => 0, + Tag::Byte { .. } => 1, + Tag::Short { .. } => 2, + Tag::Int { .. } => 3, + Tag::Long { .. } => 4, + Tag::Float { .. } => 5, + Tag::Double { .. } => 6, + Tag::ByteArray { .. } => 7, + Tag::String { .. } => 8, + Tag::List { .. } => 9, + Tag::Compound { .. } => 10, + Tag::IntArray { .. } => 11, + Tag::LongArray { .. } => 12, + } + } + + /// Name of this tag (None for End or list elements) + pub fn name(&self) -> Option<&str> { + match self { + Tag::End => None, + Tag::Byte { name, .. } + | Tag::Short { name, .. } + | Tag::Int { name, .. } + | Tag::Long { name, .. } + | Tag::Float { name, .. } + | Tag::Double { name, .. } + | Tag::ByteArray { name, .. } + | Tag::String { name, .. } + | Tag::List { name, .. } + | Tag::Compound { name, .. } + | Tag::IntArray { name, .. } + | Tag::LongArray { name, .. } => name.as_deref(), + } + } +} + +/// Read an NBT Tag from any reader +pub fn read_nbt(reader: R) -> std::io::Result { + Reader::new(reader).read_tag() +} + +/// Write an NBT Tag to any writer +pub fn write_nbt(tag: &Tag, writer: W) -> std::io::Result<()> { + Writer::new(writer).write_tag(tag) +} diff --git a/src/test.rs b/src/test.rs index 4dfd8f6..2712316 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,153 +1,168 @@ +// src/test.rs -#[cfg(test)] -mod tests { - use crate::Tag; - #[test] - fn new_byte_tag() { - let tag = Tag::new_byte("health", 20); - match tag { - Tag::Byte { name, value } => { - assert_eq!(name.unwrap(), "health"); - assert_eq!(value, 20); - } - _ => panic!("Expected Byte tag"), - } - } +use crate::{Tag, read_nbt, write_nbt}; - #[test] - fn new_short_tag() { - let tag = Tag::new_short("optLevel", 3); - match tag { - Tag::Short { name, value } => { - assert_eq!(name.unwrap(), "optLevel"); - assert_eq!(value, 3); - } - _ => panic!("Expected Short tag"), - } - } - - #[test] - fn new_int_tag() { - let tag = Tag::new_int("xPos", 100); - match tag { - Tag::Int { name, value } => { - assert_eq!(name.unwrap(), "xPos"); - assert_eq!(value, 100); - } - _ => panic!("Expected Int tag"), - } - } - - #[test] - fn new_long_tag() { - let tag = Tag::new_long("timestamp", 1_620_000_000_000); - match tag { - Tag::Long { name, value } => { - assert_eq!(name.unwrap(), "timestamp"); - assert_eq!(value, 1_620_000_000_000); - } - _ => panic!("Expected Long tag"), - } - } - - #[test] - fn new_float_tag() { - let tag = Tag::new_float("speed", 0.5); - match tag { - Tag::Float { name, value } => { - assert_eq!(name.unwrap(), "speed"); - assert!((value - 0.5).abs() < f32::EPSILON); - } - _ => panic!("Expected Float tag"), - } - } - - #[test] - fn new_double_tag() { - let tag = Tag::new_double("gravity", 9.81); - match tag { - Tag::Double { name, value } => { - assert_eq!(name.unwrap(), "gravity"); - assert!((value - 9.81).abs() < f64::EPSILON); - } - _ => panic!("Expected Double tag"), - } - } - - #[test] - fn new_byte_array_tag() { - let data = vec![1u8, 2, 3, 4]; - let tag = Tag::new_ByteArray("blocks", data.clone()); - match tag { - Tag::ByteArray { name, value } => { - assert_eq!(name.unwrap(), "blocks"); - assert_eq!(value, data); - } - _ => panic!("Expected ByteArray tag"), - } - } - - #[test] - fn new_string_tag() { - let tag = Tag::new_string("name", "Steve"); - match tag { - Tag::String { name, value } => { - assert_eq!(name.unwrap(), "name"); - assert_eq!(value, "Steve"); - } - _ => panic!("Expected String tag"), - } - } - - #[test] - fn new_list_tag() { - let elements = vec![Tag::new_int("", 1), Tag::new_int("", 2)]; - let tag = Tag::new_list("nums", 3, elements.clone()); - match tag { - Tag::List { name, element_id, elements: elems } => { - assert_eq!(name.unwrap(), "nums"); - assert_eq!(element_id, 3); - assert_eq!(elems, elements); - } - _ => panic!("Expected List tag"), - } - } - - #[test] - fn new_compound_tag() { - let tag = Tag::new_compound("Level"); - match tag { - Tag::Compound { name, entries } => { - assert_eq!(name.unwrap(), "Level"); - assert!(entries.is_empty()); - } - _ => panic!("Expected Compound tag"), - } - } - - #[test] - fn new_int_array_tag() { - let data = vec![1i32, 2, 3]; - let tag = Tag::new_int_array("palette", data.clone()); - match tag { - Tag::IntArray { name, value } => { - assert_eq!(name.unwrap(), "palette"); - assert_eq!(value, data); - } - _ => panic!("Expected IntArray tag"), - } - } - - #[test] - fn new_long_array_tag() { - let data = vec![100i64, 200]; - let tag = Tag::new_long_array("timestamps", data.clone()); - match tag { - Tag::LongArray { name, value } => { - assert_eq!(name.unwrap(), "timestamps"); - assert_eq!(value, data); - } - _ => panic!("Expected LongArray tag"), +#[test] +fn new_byte_tag() { + let tag = Tag::new_byte("health", 20); + match tag { + Tag::Byte { name, value } => { + assert_eq!(name.unwrap(), "health"); + assert_eq!(value, 20); } + _ => panic!("Expected Byte tag"), } } + +#[test] +fn new_short_tag() { + let tag = Tag::new_short("optLevel", 3); + match tag { + Tag::Short { name, value } => { + assert_eq!(name.unwrap(), "optLevel"); + assert_eq!(value, 3); + } + _ => panic!("Expected Short tag"), + } +} + +#[test] +fn new_int_tag() { + let tag = Tag::new_int("xPos", 100); + match tag { + Tag::Int { name, value } => { + assert_eq!(name.unwrap(), "xPos"); + assert_eq!(value, 100); + } + _ => panic!("Expected Int tag"), + } +} + +#[test] +fn new_long_tag() { + let tag = Tag::new_long("timestamp", 1_620_000_000_000); + match tag { + Tag::Long { name, value } => { + assert_eq!(name.unwrap(), "timestamp"); + assert_eq!(value, 1_620_000_000_000); + } + _ => panic!("Expected Long tag"), + } +} + +#[test] +fn new_float_tag() { + let tag = Tag::new_float("speed", 0.5); + match tag { + Tag::Float { name, value } => { + assert_eq!(name.unwrap(), "speed"); + assert!((value - 0.5).abs() < f32::EPSILON); + } + _ => panic!("Expected Float tag"), + } +} + +#[test] +fn new_double_tag() { + let tag = Tag::new_double("gravity", 9.81); + match tag { + Tag::Double { name, value } => { + assert_eq!(name.unwrap(), "gravity"); + assert!((value - 9.81).abs() < f64::EPSILON); + } + _ => panic!("Expected Double tag"), + } +} + +#[test] +fn new_byte_array_tag() { + let data = vec![1u8, 2, 3, 4]; + let tag = Tag::new_byte_array("blocks", data.clone()); + match tag { + Tag::ByteArray { name, value } => { + assert_eq!(name.unwrap(), "blocks"); + assert_eq!(value, data); + } + _ => panic!("Expected ByteArray tag"), + } +} + +#[test] +fn new_string_tag() { + let tag = Tag::new_string("name", "Steve"); + match tag { + Tag::String { name, value } => { + assert_eq!(name.unwrap(), "name"); + assert_eq!(value, "Steve"); + } + _ => panic!("Expected String tag"), + } +} + +#[test] +fn new_list_tag() { + let elements = vec![Tag::new_int("", 1), Tag::new_int("", 2)]; + let tag = Tag::new_list("nums", 3, elements.clone()); + match tag { + Tag::List { + name, + element_id, + elements: elems, + } => { + assert_eq!(name.unwrap(), "nums"); + assert_eq!(element_id, 3); + assert_eq!(elems, elements); + } + _ => panic!("Expected List tag"), + } +} + +#[test] +fn new_compound_tag() { + let tag = Tag::new_compound("Level"); + match tag { + Tag::Compound { name, entries } => { + assert_eq!(name.unwrap(), "Level"); + assert!(entries.is_empty()); + } + _ => panic!("Expected Compound tag"), + } +} + +#[test] +fn new_int_array_tag() { + let data = vec![1i32, 2, 3]; + let tag = Tag::new_int_array("palette", data.clone()); + match tag { + Tag::IntArray { name, value } => { + assert_eq!(name.unwrap(), "palette"); + assert_eq!(value, data); + } + _ => panic!("Expected IntArray tag"), + } +} + +#[test] +fn new_long_array_tag() { + let data = vec![100i64, 200]; + let tag = Tag::new_long_array("timestamps", data.clone()); + match tag { + Tag::LongArray { name, value } => { + assert_eq!(name.unwrap(), "timestamps"); + assert_eq!(value, data); + } + _ => panic!("Expected LongArray tag"), + } +} + +#[test] +fn roundtrip_compound() { + let mut root = Tag::new_compound("Test"); + root.insert("value".to_string(), Tag::new_int("value", 42)); + + let mut buf = Vec::new(); + write_nbt(&root, &mut buf).unwrap(); + + let decoded = read_nbt(&buf[..]).unwrap(); + assert_eq!(decoded, root); +}