1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
use crate::{Decode, DecodeError, Encode};
use ussr_nbt::owned::Nbt;
/// Custom reader that prepends the NBT protocol header for the root compound tag
/// For Minecraft 1.21.4+ (Protocol >= 764): 0x0A (compound) + 0x00 0x00 (empty root name) + payload
struct NbtReader<'a> {
read_bytes: usize,
inner: &'a [u8],
}
impl<'a> NbtReader<'a> {
fn new(inner: &'a [u8]) -> Self {
Self {
read_bytes: 0,
inner,
}
}
}
impl<'a> std::io::Read for NbtReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let len = buf.len();
if len == 0 {
return Ok(0);
}
// Send the protocol header bytes: 0x0A (tag) + 0x00 0x00 (empty root name)
if self.read_bytes == 0 {
buf[0] = 0x0A; // Compound tag
self.read_bytes = 1;
return Ok(1);
}
if self.read_bytes == 1 {
buf[0] = 0x00; // Name length high byte
self.read_bytes = 2;
return Ok(1);
}
if self.read_bytes == 2 {
buf[0] = 0x00; // Name length low byte
self.read_bytes = 3;
// Also consume one dummy byte from inner since NBT expects to read the name
let _ = self.inner.read(&mut [0])?;
return Ok(1);
}
// After header, read the payload directly from inner
match self.inner.read(buf) {
Ok(n) => {
self.read_bytes += n;
Ok(n)
}
Err(e) => Err(e),
}
}
}
impl Encode for Nbt {
fn encode(&self, buf: &mut Vec<u8>) {
let mut temp_buf = Vec::new();
if self.write(&mut temp_buf).is_ok() {
// Write the compound tag
buf.push(0x0A);
// NBT library includes: tag (1) + name_len (2) + name (0 for empty root) + payload
// We skip 3 bytes for the tag + empty name_len, then write the rest
if temp_buf.len() > 3 {
buf.extend_from_slice(&temp_buf[3..]);
}
}
}
}
impl Decode for Nbt {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
// Wrap the buffer with the NBT reader that prepends the header
let mut reader = NbtReader::new(buf);
match Nbt::read(&mut reader) {
Ok(nbt) => {
// The reader has a read_bytes counter that tells us how much was consumed
// including the 3-byte header (0x0A + 0x00 0x00) it injected
let consumed = reader.read_bytes - 3; // Subtract 3 for the injected header
*buf = &buf[consumed..];
Ok(nbt)
}
Err(_) => Err(DecodeError::InvalidLength),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nbt_encode_produces_output() {
// Create a simple NBT by reading from known data
// The NBT library writes: TAG_ID (0x0A) + Name Length (0x00 0x00) + Payload
let nbt_data = vec![
0x0A, // Compound tag
0x00, 0x00, // Empty name
0x00, // End tag
];
let mut slice = nbt_data.as_slice();
let nbt = Nbt::read(&mut slice).expect("Failed to read NBT");
// Now test our encode for 1.21.4+ format
let mut buf = Vec::new();
nbt.encode(&mut buf);
// For 1.21.4+ (Protocol >= 764):
// Encoding produces: 0x0A tag + payload (no name)
// So: 0x0A + buffer[3..] (skips tag + name)
assert_eq!(buf.len(), 2); // 0x0A + 0x00
assert_eq!(buf[0], 0x0A); // Compound tag
assert_eq!(buf[1], 0x00); // End tag (start of payload)
}
#[test]
fn test_nbt_decode_with_reader() {
// For 1.21.4+ format: the incoming data is just the payload (no name)
// Reader prepends 0x0A tag, then reads the payload
let nbt_data = vec![
0x00, // End tag (minimal payload)
];
let mut slice = nbt_data.as_slice();
let result = Nbt::decode(&mut slice);
// Should either succeed or gracefully fail - the key is it doesn't panic
let _ = result;
}
#[test]
fn test_nbt_truncated_data() {
let mut slice = &[][..];
let result = Nbt::decode(&mut slice);
// Should handle truncated data gracefully
assert!(result.is_err());
}
#[test]
fn test_nbt_empty_buffer() {
let buf = Vec::new();
let mut slice = buf.as_slice();
let result = Nbt::decode(&mut slice);
assert!(result.is_err());
}
#[test]
fn test_nbt_roundtrip_simple() {
use ussr_nbt::owned::Tag;
// Create a simple NBT structure like in configuration.rs
let original_nbt = Nbt {
name: "".into(),
compound: vec![("field".into(), Tag::String("value".into()))].into(),
};
// Encode the NBT
let mut encoded = Vec::new();
original_nbt.encode(&mut encoded);
assert!(!encoded.is_empty());
assert_eq!(encoded[0], 0x0A, "First byte should be compound tag");
// Decode the NBT
let mut buf_slice = encoded.as_slice();
let decoded_nbt = Nbt::decode(&mut buf_slice).expect("Failed to decode NBT");
// Check the decoded NBT has the same structure
assert_eq!(decoded_nbt.name, original_nbt.name);
assert_eq!(
decoded_nbt.compound.tags.len(),
original_nbt.compound.tags.len(),
"Should have same number of fields"
);
}
#[test]
fn test_nbt_roundtrip_multiple_tags() {
use ussr_nbt::owned::Tag;
// Create a more complex NBT with multiple field types (like real Minecraft configs)
let original_nbt = Nbt {
name: "".into(),
compound: vec![
(
"wild_texture".into(),
Tag::String("minecraft:entity/wolf/wolf_ashen".into()),
),
("flag".into(), Tag::Byte(1)),
]
.into(),
};
// Encode
let mut encoded = Vec::new();
original_nbt.encode(&mut encoded);
assert_eq!(encoded[0], 0x0A);
// Decode
let mut buf_slice = encoded.as_slice();
let decoded = Nbt::decode(&mut buf_slice).expect("Failed to decode multi-tag NBT");
// Verify structure preserved
assert_eq!(decoded.name, original_nbt.name);
assert_eq!(
decoded.compound.tags.len(),
original_nbt.compound.tags.len()
);
}
}