mirror of
https://github.com/Cactus-minecraft-server/nbt.git
synced 2025-12-07 02:30:37 +00:00
Add gzip compression
This commit is contained in:
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -4,3 +4,4 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
flate2 = "1.1.2"
|
||||
|
||||
40
src/io.rs
40
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<R: Read> {
|
||||
inner: R,
|
||||
}
|
||||
|
||||
// --- Reader gzip ---
|
||||
impl<R: Read> Reader<GzDecoder<R>> {
|
||||
pub fn from_gzip(inner: R) -> Self {
|
||||
Reader {
|
||||
inner: GzDecoder::new(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<W: Write> Writer<GzEncoder<W>> {
|
||||
pub fn to_gzip(inner: W) -> Self {
|
||||
Writer {
|
||||
inner: GzEncoder::new(inner, Compression::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<R: Read> Reader<R> {
|
||||
pub fn new(inner: R) -> Self {
|
||||
Reader { inner }
|
||||
@@ -134,13 +150,22 @@ impl<R: Read> Reader<R> {
|
||||
Ok(i64::from_be_bytes(buf))
|
||||
}
|
||||
fn read_f32(&mut self) -> Result<f32> {
|
||||
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<f64> {
|
||||
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<u16> {
|
||||
let mut b = [0u8; 2];
|
||||
self.inner.read_exact(&mut b)?;
|
||||
Ok(u16::from_be_bytes(b))
|
||||
}
|
||||
fn read_string(&mut self) -> Result<String> {
|
||||
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<W: Write> Writer<W> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
321
src/test.rs
321
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<u8> {
|
||||
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::<Vec<_>>();
|
||||
let la = (0..8).map(|i| (i as i64) * (1 << 33)).collect::<Vec<_>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user