Skip to main content

voidmc_codec/primitives/
nbt.rs

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