Skip to main content

casc_lib/blte/
decoder.rs

1//! Top-level BLTE stream decoder.
2//!
3//! Validates the `"BLTE"` magic, parses the chunk table (if present), and
4//! iterates over each block - delegating to the [`compression`](crate::blte::compression)
5//! module for per-block decompression and decryption. Supports both single-block
6//! (header size = 0) and multi-block layouts.
7
8use super::compression::decode_block_with_keys;
9use crate::blte::encryption::TactKeyStore;
10use crate::error::{CascError, Result};
11use crate::util::io::{read_be_u24, read_be_u32};
12
13/// A parsed BLTE chunk descriptor.
14#[derive(Debug)]
15struct ChunkInfo {
16    compressed_size: u32,
17    #[allow(dead_code)]
18    decompressed_size: u32,
19    #[allow(dead_code)]
20    hash: [u8; 16],
21}
22
23/// Decode a BLTE-encoded payload into raw file content.
24pub fn decode_blte(data: &[u8]) -> Result<Vec<u8>> {
25    decode_blte_with_keys(data, None)
26}
27
28/// Decode a BLTE-encoded payload with optional encryption support.
29///
30/// When `keystore` is `Some`, encrypted (mode E) blocks are decrypted
31/// using the keys in the store. Pass `None` for backwards-compatible
32/// behaviour where encrypted blocks return an error.
33pub fn decode_blte_with_keys(data: &[u8], keystore: Option<&TactKeyStore>) -> Result<Vec<u8>> {
34    if data.len() < 8 {
35        return Err(CascError::InvalidFormat("BLTE data too short".into()));
36    }
37    if &data[0..4] != b"BLTE" {
38        return Err(CascError::InvalidMagic {
39            expected: "BLTE".into(),
40            found: String::from_utf8_lossy(&data[0..4]).into(),
41        });
42    }
43
44    let header_size = read_be_u32(&data[4..8]);
45
46    if header_size == 0 {
47        // Single-block mode: everything after the 8-byte header is one block
48        if data.len() <= 8 {
49            return Ok(Vec::new());
50        }
51        return decode_block_with_keys(&data[8..], keystore);
52    }
53
54    // Multi-block mode: parse chunk table
55    if data.len() < 12 {
56        return Err(CascError::InvalidFormat(
57            "BLTE chunk table too short".into(),
58        ));
59    }
60
61    let table_format = data[8];
62    if table_format != 0x0F {
63        return Err(CascError::InvalidFormat(format!(
64            "unsupported BLTE table format: 0x{:02X}",
65            table_format
66        )));
67    }
68
69    let num_blocks = read_be_u24(&data[9..12]) as usize;
70
71    // Parse block descriptors (24 bytes each, starting at offset 12)
72    let descriptors_start = 12;
73    let descriptors_end = descriptors_start + num_blocks * 24;
74    if data.len() < descriptors_end {
75        return Err(CascError::InvalidFormat(
76            "BLTE block descriptors truncated".into(),
77        ));
78    }
79
80    let mut chunks = Vec::with_capacity(num_blocks);
81    for i in 0..num_blocks {
82        let base = descriptors_start + i * 24;
83        let compressed_size = read_be_u32(&data[base..]);
84        let decompressed_size = read_be_u32(&data[base + 4..]);
85        let mut hash = [0u8; 16];
86        hash.copy_from_slice(&data[base + 8..base + 24]);
87        chunks.push(ChunkInfo {
88            compressed_size,
89            decompressed_size,
90            hash,
91        });
92    }
93
94    // Decode blocks sequentially
95    let mut data_pos = header_size as usize;
96    let mut output = Vec::new();
97    for chunk in &chunks {
98        let block_end = data_pos + chunk.compressed_size as usize;
99        if data.len() < block_end {
100            return Err(CascError::InvalidFormat("BLTE block data truncated".into()));
101        }
102        let block = &data[data_pos..block_end];
103        let decoded = decode_block_with_keys(block, keystore)?;
104        output.extend_from_slice(&decoded);
105        data_pos = block_end;
106    }
107
108    Ok(output)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use flate2::Compression;
115    use flate2::write::ZlibEncoder;
116    use std::io::Write;
117
118    fn zlib_compress(data: &[u8]) -> Vec<u8> {
119        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
120        encoder.write_all(data).unwrap();
121        encoder.finish().unwrap()
122    }
123
124    fn make_block_descriptor(compressed_size: u32, decompressed_size: u32) -> Vec<u8> {
125        let mut desc = Vec::new();
126        desc.extend_from_slice(&compressed_size.to_be_bytes());
127        desc.extend_from_slice(&decompressed_size.to_be_bytes());
128        desc.extend_from_slice(&[0u8; 16]); // hash (zeroed for tests)
129        desc
130    }
131
132    #[test]
133    fn blte_validates_magic() {
134        let data = b"XBLT\x00\x00\x00\x00";
135        let err = decode_blte(data).unwrap_err();
136        assert!(matches!(err, CascError::InvalidMagic { .. }));
137    }
138
139    #[test]
140    fn blte_too_short() {
141        assert!(decode_blte(&[0x42, 0x4C, 0x54]).is_err());
142    }
143
144    #[test]
145    fn blte_single_block_raw() {
146        let mut data = Vec::new();
147        data.extend_from_slice(b"BLTE");
148        data.extend_from_slice(&0u32.to_be_bytes());
149        data.push(b'N');
150        data.extend_from_slice(b"hello");
151        assert_eq!(decode_blte(&data).unwrap(), b"hello");
152    }
153
154    #[test]
155    fn blte_single_block_zlib() {
156        let original = b"hello world compressed!";
157        let compressed = zlib_compress(original);
158
159        let mut data = Vec::new();
160        data.extend_from_slice(b"BLTE");
161        data.extend_from_slice(&0u32.to_be_bytes());
162        data.push(b'Z');
163        data.extend_from_slice(&compressed);
164
165        assert_eq!(decode_blte(&data).unwrap(), original);
166    }
167
168    #[test]
169    fn blte_single_block_empty() {
170        let mut data = Vec::new();
171        data.extend_from_slice(b"BLTE");
172        data.extend_from_slice(&0u32.to_be_bytes());
173        assert_eq!(decode_blte(&data).unwrap(), Vec::<u8>::new());
174    }
175
176    #[test]
177    fn blte_multi_block_two_raw() {
178        let block1_data = b"Nhello"; // N + "hello"
179        let block2_data = b"N world"; // N + " world"
180
181        let desc1 = make_block_descriptor(block1_data.len() as u32, 5);
182        let desc2 = make_block_descriptor(block2_data.len() as u32, 6);
183
184        // headerSize = 4 (magic) + 4 (headerSize) + 1 (tableFormat) + 3 (numBlocks) + 2*24
185        let header_size: u32 = 8 + 1 + 3 + 2 * 24; // = 60
186
187        let mut data = Vec::new();
188        data.extend_from_slice(b"BLTE");
189        data.extend_from_slice(&header_size.to_be_bytes());
190        data.push(0x0F); // tableFormat
191        data.push(0x00);
192        data.push(0x00);
193        data.push(0x02); // numBlocks = 2 as u24 BE
194        data.extend_from_slice(&desc1);
195        data.extend_from_slice(&desc2);
196        assert_eq!(data.len(), header_size as usize);
197        data.extend_from_slice(block1_data);
198        data.extend_from_slice(block2_data);
199
200        let result = decode_blte(&data).unwrap();
201        assert_eq!(result, b"hello world");
202    }
203
204    #[test]
205    fn blte_multi_block_mixed_nz() {
206        let raw_content = b"raw part";
207        let zlib_content = b"compressed part";
208        let compressed = zlib_compress(zlib_content);
209
210        let block1 = {
211            let mut b = vec![b'N'];
212            b.extend_from_slice(raw_content);
213            b
214        };
215        let block2 = {
216            let mut b = vec![b'Z'];
217            b.extend_from_slice(&compressed);
218            b
219        };
220
221        let desc1 = make_block_descriptor(block1.len() as u32, raw_content.len() as u32);
222        let desc2 = make_block_descriptor(block2.len() as u32, zlib_content.len() as u32);
223
224        let header_size: u32 = 8 + 1 + 3 + 2 * 24;
225
226        let mut data = Vec::new();
227        data.extend_from_slice(b"BLTE");
228        data.extend_from_slice(&header_size.to_be_bytes());
229        data.push(0x0F);
230        data.push(0x00);
231        data.push(0x00);
232        data.push(0x02);
233        data.extend_from_slice(&desc1);
234        data.extend_from_slice(&desc2);
235        data.extend_from_slice(&block1);
236        data.extend_from_slice(&block2);
237
238        let result = decode_blte(&data).unwrap();
239        let expected: Vec<u8> = [raw_content.as_ref(), zlib_content.as_ref()].concat();
240        assert_eq!(result, expected);
241    }
242
243    #[test]
244    fn blte_multi_block_truncated_data() {
245        let header_size: u32 = 8 + 1 + 3 + 24;
246        let mut data = Vec::new();
247        data.extend_from_slice(b"BLTE");
248        data.extend_from_slice(&header_size.to_be_bytes());
249        data.push(0x0F);
250        data.push(0x00);
251        data.push(0x00);
252        data.push(0x01);
253        data.extend_from_slice(&make_block_descriptor(100, 100)); // claims 100 bytes
254        // But we only provide 5 bytes of block data
255        data.extend_from_slice(&[b'N', 1, 2, 3, 4]);
256
257        assert!(decode_blte(&data).is_err());
258    }
259
260    #[test]
261    fn blte_unsupported_table_format() {
262        let mut data = Vec::new();
263        data.extend_from_slice(b"BLTE");
264        data.extend_from_slice(&100u32.to_be_bytes());
265        data.push(0x10); // unsupported format
266        data.extend_from_slice(&[0; 100]);
267
268        assert!(decode_blte(&data).is_err());
269    }
270
271    #[test]
272    fn blte_with_keys_none_works_for_non_encrypted() {
273        let mut data = Vec::new();
274        data.extend_from_slice(b"BLTE");
275        data.extend_from_slice(&0u32.to_be_bytes());
276        data.push(b'N');
277        data.extend_from_slice(b"test");
278        assert_eq!(decode_blte_with_keys(&data, None).unwrap(), b"test");
279    }
280
281    #[test]
282    fn blte_encrypted_block_without_keystore_errors() {
283        // Build a single-block BLTE with mode E - should fail without keystore
284        let mut data = Vec::new();
285        data.extend_from_slice(b"BLTE");
286        data.extend_from_slice(&0u32.to_be_bytes()); // single block
287        data.push(b'E'); // encrypted
288        data.push(1u8); // key_count
289        data.push(8u8); // key_name_size
290        data.extend_from_slice(&0xDEADu64.to_le_bytes()); // unknown key
291        data.extend_from_slice(&4u32.to_le_bytes()); // iv_size
292        data.extend_from_slice(&[0; 4]); // iv
293        data.push(b'S'); // salsa20
294        data.extend_from_slice(b"fake_encrypted_data");
295
296        // Without keystore -> error
297        assert!(decode_blte(&data).is_err());
298
299        // With empty keystore -> EncryptionKeyMissing
300        let ks = TactKeyStore::new();
301        let result = decode_blte_with_keys(&data, Some(&ks));
302        assert!(result.is_err());
303    }
304
305    #[test]
306    fn blte_encrypted_single_block_round_trip() {
307        let key_name: u64 = 0xFA505078126ACB3E;
308        let ks = TactKeyStore::with_known_keys();
309        let key = ks.get(key_name).unwrap();
310
311        // Inner content: mode N + "decrypted!"
312        let plaintext = b"Ndecrypted!";
313        let iv_bytes = [0x10, 0x20, 0x30, 0x40];
314
315        // Encrypt plaintext with Salsa20
316        let mut encrypted_payload = plaintext.to_vec();
317        {
318            use salsa20::Salsa20;
319            use salsa20::cipher::{KeyIvInit, StreamCipher};
320            let mut full_key = [0u8; 32];
321            full_key[..16].copy_from_slice(key);
322            full_key[16..].copy_from_slice(key);
323            let mut nonce = [0u8; 8];
324            nonce[..4].copy_from_slice(&iv_bytes);
325            let mut cipher = Salsa20::new(&full_key.into(), &nonce.into());
326            cipher.apply_keystream(&mut encrypted_payload);
327        }
328
329        // Build E-mode encryption header
330        let mut e_block = Vec::new();
331        e_block.push(1u8); // key_count
332        e_block.push(8u8); // key_name_size
333        e_block.extend_from_slice(&key_name.to_le_bytes());
334        e_block.extend_from_slice(&4u32.to_le_bytes()); // iv_size
335        e_block.extend_from_slice(&iv_bytes);
336        e_block.push(b'S'); // salsa20
337        e_block.extend_from_slice(&encrypted_payload);
338
339        // Wrap in BLTE single-block format
340        let mut data = Vec::new();
341        data.extend_from_slice(b"BLTE");
342        data.extend_from_slice(&0u32.to_be_bytes()); // single block
343        data.push(b'E');
344        data.extend_from_slice(&e_block);
345
346        let result = decode_blte_with_keys(&data, Some(&ks)).unwrap();
347        assert_eq!(result, b"decrypted!");
348    }
349}