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.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
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]]
|
[[package]]
|
||||||
name = "nbt"
|
name = "nbt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
]
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
flate2 = "1.1.2"
|
||||||
|
|||||||
40
src/io.rs
40
src/io.rs
@@ -4,12 +4,28 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Tag, TagId};
|
use crate::{Tag, TagId};
|
||||||
|
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
|
||||||
|
|
||||||
/// Binary reader for NBT format
|
/// Binary reader for NBT format
|
||||||
pub struct Reader<R: Read> {
|
pub struct Reader<R: Read> {
|
||||||
inner: R,
|
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> {
|
impl<R: Read> Reader<R> {
|
||||||
pub fn new(inner: R) -> Self {
|
pub fn new(inner: R) -> Self {
|
||||||
Reader { inner }
|
Reader { inner }
|
||||||
@@ -134,13 +150,22 @@ impl<R: Read> Reader<R> {
|
|||||||
Ok(i64::from_be_bytes(buf))
|
Ok(i64::from_be_bytes(buf))
|
||||||
}
|
}
|
||||||
fn read_f32(&mut self) -> Result<f32> {
|
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> {
|
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> {
|
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];
|
let mut buf = vec![0u8; len];
|
||||||
self.inner.read_exact(&mut buf)?;
|
self.inner.read_exact(&mut buf)?;
|
||||||
String::from_utf8(buf).map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
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())
|
self.inner.write_all(&v.to_be_bytes())
|
||||||
}
|
}
|
||||||
fn write_f32(&mut self, v: f32) -> Result<()> {
|
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<()> {
|
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<()> {
|
fn write_string(&mut self, s: &str) -> Result<()> {
|
||||||
let bytes = s.as_bytes();
|
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)
|
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();
|
let decoded = read_nbt(&buf[..]).unwrap();
|
||||||
assert_eq!(decoded, root);
|
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