Skip to main content

casc_lib/blte/
compression.rs

1//! Block-level compression and mode dispatch for BLTE.
2//!
3//! Each BLTE block starts with a one-byte mode prefix:
4//!
5//! - `N` (0x4E) - raw / uncompressed data, returned as-is.
6//! - `Z` (0x5A) - zlib-compressed data (RFC 1950).
7//! - `4` (0x34) - LZ4 block compression with sub-block framing.
8//! - `E` (0x45) - encrypted block; after decryption the inner payload is
9//!   recursively decoded (its first byte is the inner compression mode).
10//! - `F` (0x46) - recursive BLTE frame; the payload is a complete nested
11//!   BLTE stream that is decoded recursively.
12
13use super::encryption::{TactKeyStore, decrypt_block as decrypt_encrypted_block};
14use crate::error::{CascError, Result};
15
16/// Decode a BLTE block with optional encryption support.
17///
18/// `block` includes the mode byte as the first byte. When a mode-E
19/// (encrypted) block is encountered the `keystore` is used to look up
20/// the decryption key. Pass `None` if encryption support is not needed
21/// - encrypted blocks will return an error in that case.
22pub fn decode_block_with_keys(block: &[u8], keystore: Option<&TactKeyStore>) -> Result<Vec<u8>> {
23    if block.is_empty() {
24        return Ok(Vec::new());
25    }
26    match block[0] {
27        b'N' => decode_raw(&block[1..]),
28        b'Z' => decode_zlib(&block[1..]),
29        b'4' => decode_lz4(&block[1..]),
30        b'E' => decode_encrypted(&block[1..], keystore),
31        b'F' => decode_frame(&block[1..], keystore),
32        mode => Err(CascError::InvalidFormat(format!(
33            "unknown BLTE mode: 0x{:02X}",
34            mode
35        ))),
36    }
37}
38
39/// Decode a BLTE block based on its mode byte (no encryption support).
40///
41/// `block` includes the mode byte as the first byte.
42/// Encrypted blocks (mode E) will always return an error.
43pub fn decode_block(block: &[u8]) -> Result<Vec<u8>> {
44    decode_block_with_keys(block, None)
45}
46
47fn decode_encrypted(data: &[u8], keystore: Option<&TactKeyStore>) -> Result<Vec<u8>> {
48    let keystore = keystore.ok_or_else(|| {
49        CascError::EncryptionKeyMissing("no keystore provided for encrypted block".into())
50    })?;
51    // decrypt_encrypted_block returns decrypted data where the first byte is the inner mode
52    let decrypted = decrypt_encrypted_block(data, keystore)?;
53    // Recursively decode the inner block (which starts with a mode byte: N, Z, 4, etc.)
54    decode_block_with_keys(&decrypted, Some(keystore))
55}
56
57fn decode_frame(data: &[u8], keystore: Option<&TactKeyStore>) -> Result<Vec<u8>> {
58    // Mode F wraps a complete nested BLTE stream (starting with "BLTE" magic).
59    super::decoder::decode_blte_with_keys(data, keystore)
60}
61
62fn decode_raw(data: &[u8]) -> Result<Vec<u8>> {
63    Ok(data.to_vec())
64}
65
66fn decode_zlib(data: &[u8]) -> Result<Vec<u8>> {
67    use flate2::read::ZlibDecoder;
68    use std::io::Read;
69
70    let mut decoder = ZlibDecoder::new(data);
71    let mut output = Vec::new();
72    decoder
73        .read_to_end(&mut output)
74        .map_err(|e| CascError::DecompressionFailed(format!("zlib: {}", e)))?;
75    Ok(output)
76}
77
78fn decode_lz4(data: &[u8]) -> Result<Vec<u8>> {
79    let mut output = Vec::new();
80    let mut offset = 0;
81
82    while offset < data.len() {
83        // Each sub-block: u32 LE decompressed_size + u32 LE compressed_size + payload
84        if offset + 8 > data.len() {
85            return Err(CascError::InvalidFormat(
86                "LZ4: truncated sub-block header".into(),
87            ));
88        }
89
90        let decompressed_size =
91            u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
92        offset += 4;
93
94        let compressed_size =
95            u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
96        offset += 4;
97
98        if compressed_size >= decompressed_size {
99            // Stored uncompressed - read decompressed_size raw bytes
100            if offset + decompressed_size > data.len() {
101                return Err(CascError::InvalidFormat(
102                    "LZ4: truncated uncompressed sub-block payload".into(),
103                ));
104            }
105            output.extend_from_slice(&data[offset..offset + decompressed_size]);
106            offset += decompressed_size;
107        } else {
108            // LZ4 block compressed
109            if offset + compressed_size > data.len() {
110                return Err(CascError::InvalidFormat(
111                    "LZ4: truncated compressed sub-block payload".into(),
112                ));
113            }
114            let decompressed =
115                lz4_flex::decompress(&data[offset..offset + compressed_size], decompressed_size)
116                    .map_err(|e| CascError::DecompressionFailed(format!("LZ4: {}", e)))?;
117            output.extend_from_slice(&decompressed);
118            offset += compressed_size;
119        }
120    }
121
122    Ok(output)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use flate2::Compression;
129    use flate2::write::ZlibEncoder;
130    use std::io::Write;
131
132    fn zlib_compress(data: &[u8]) -> Vec<u8> {
133        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
134        encoder.write_all(data).unwrap();
135        encoder.finish().unwrap()
136    }
137
138    #[test]
139    fn mode_n_passthrough() {
140        let mut block = vec![b'N'];
141        block.extend_from_slice(b"hello world");
142        let result = decode_block(&block).unwrap();
143        assert_eq!(result, b"hello world");
144    }
145
146    #[test]
147    fn mode_n_empty_payload() {
148        let block = vec![b'N'];
149        let result = decode_block(&block).unwrap();
150        assert!(result.is_empty());
151    }
152
153    #[test]
154    fn mode_z_decompresses() {
155        let original = b"hello world compressed with zlib!";
156        let compressed = zlib_compress(original);
157        let mut block = vec![b'Z'];
158        block.extend_from_slice(&compressed);
159        let result = decode_block(&block).unwrap();
160        assert_eq!(result, original);
161    }
162
163    #[test]
164    fn mode_z_large_data() {
165        let original: Vec<u8> = (0..10000).map(|i| (i % 256) as u8).collect();
166        let compressed = zlib_compress(&original);
167        let mut block = vec![b'Z'];
168        block.extend_from_slice(&compressed);
169        let result = decode_block(&block).unwrap();
170        assert_eq!(result, original);
171    }
172
173    #[test]
174    fn mode_z_invalid_data() {
175        let block = vec![b'Z', 0xFF, 0xFE, 0xFD];
176        assert!(decode_block(&block).is_err());
177    }
178
179    #[test]
180    fn mode_e_without_keystore_errors() {
181        let block = vec![b'E', 0x00];
182        assert!(decode_block(&block).is_err());
183        assert!(decode_block_with_keys(&block, None).is_err());
184    }
185
186    #[test]
187    fn mode_e_with_empty_keystore_errors() {
188        use crate::blte::encryption::TactKeyStore;
189        // Build a minimal encrypted block with a key that won't be in the store
190        let mut block = vec![b'E'];
191        block.push(1u8); // key_count
192        block.push(8u8); // key_name_size
193        block.extend_from_slice(&0xDEADu64.to_le_bytes());
194        block.extend_from_slice(&4u32.to_le_bytes()); // iv_size
195        block.extend_from_slice(&[0; 4]); // iv
196        block.push(b'S'); // salsa20
197        block.extend_from_slice(b"fake_encrypted_data");
198
199        let ks = TactKeyStore::new();
200        let result = decode_block_with_keys(&block, Some(&ks));
201        assert!(result.is_err());
202    }
203
204    #[test]
205    fn mode_e_decrypt_and_decompress_raw() {
206        use crate::blte::encryption::TactKeyStore;
207
208        let key_name: u64 = 0xFA505078126ACB3E;
209        let ks = TactKeyStore::with_known_keys();
210        let key = ks.get(key_name).unwrap();
211
212        // Inner content: mode N + "hello"
213        let plaintext = b"Nhello";
214        let iv_bytes = [0x10, 0x20, 0x30, 0x40];
215
216        // Encrypt the plaintext with Salsa20
217        let mut encrypted_payload = plaintext.to_vec();
218        {
219            use salsa20::Salsa20;
220            use salsa20::cipher::{KeyIvInit, StreamCipher};
221            let mut full_key = [0u8; 32];
222            full_key[..16].copy_from_slice(key);
223            full_key[16..].copy_from_slice(key);
224            let mut nonce = [0u8; 8];
225            nonce[..4].copy_from_slice(&iv_bytes);
226            let mut cipher = Salsa20::new(&full_key.into(), &nonce.into());
227            cipher.apply_keystream(&mut encrypted_payload);
228        }
229
230        // Build the full E-mode block: E + encryption header + encrypted payload
231        let mut block = vec![b'E'];
232        block.push(1u8);
233        block.push(8u8);
234        block.extend_from_slice(&key_name.to_le_bytes());
235        block.extend_from_slice(&4u32.to_le_bytes());
236        block.extend_from_slice(&iv_bytes);
237        block.push(b'S');
238        block.extend_from_slice(&encrypted_payload);
239
240        let result = decode_block_with_keys(&block, Some(&ks)).unwrap();
241        assert_eq!(result, b"hello");
242    }
243
244    #[test]
245    fn mode_f_recursive_blte() {
246        // Mode F wraps a complete nested BLTE stream
247        let mut inner = Vec::new();
248        inner.extend_from_slice(b"BLTE");
249        inner.extend_from_slice(&0u32.to_be_bytes()); // single-block
250        inner.push(b'N');
251        inner.extend_from_slice(b"nested content");
252
253        let mut block = vec![b'F'];
254        block.extend_from_slice(&inner);
255
256        let result = decode_block(&block).unwrap();
257        assert_eq!(result, b"nested content");
258    }
259
260    #[test]
261    fn mode_f_nested_zlib() {
262        let original = b"nested zlib payload";
263        let compressed = zlib_compress(original);
264
265        let mut inner = Vec::new();
266        inner.extend_from_slice(b"BLTE");
267        inner.extend_from_slice(&0u32.to_be_bytes());
268        inner.push(b'Z');
269        inner.extend_from_slice(&compressed);
270
271        let mut block = vec![b'F'];
272        block.extend_from_slice(&inner);
273
274        let result = decode_block(&block).unwrap();
275        assert_eq!(result, original);
276    }
277
278    #[test]
279    fn mode_f_invalid_inner_errors() {
280        // Payload without a valid nested BLTE magic must fail
281        let block = vec![b'F', b'X', b'X', b'X', b'X', 0, 0, 0, 0];
282        assert!(decode_block(&block).is_err());
283    }
284
285    #[test]
286    fn mode_unknown_returns_error() {
287        let block = vec![b'X', 0x00];
288        let err = decode_block(&block).unwrap_err();
289        assert!(err.to_string().contains("unknown BLTE mode"));
290    }
291
292    #[test]
293    fn empty_block() {
294        let result = decode_block(&[]).unwrap();
295        assert!(result.is_empty());
296    }
297
298    #[test]
299    fn mode_4_single_subblock_compressed() {
300        // Use highly repetitive data so LZ4 actually compresses it smaller
301        let original: Vec<u8> = b"AAAA".repeat(256);
302        let compressed = lz4_flex::compress(&original);
303        assert!(
304            compressed.len() < original.len(),
305            "test data must actually compress smaller"
306        );
307
308        let decompressed_size = original.len() as u32;
309        let compressed_size = compressed.len() as u32;
310
311        let mut block = vec![b'4'];
312        block.extend_from_slice(&decompressed_size.to_le_bytes());
313        block.extend_from_slice(&compressed_size.to_le_bytes());
314        block.extend_from_slice(&compressed);
315
316        let result = decode_block(&block).unwrap();
317        assert_eq!(result, original);
318    }
319
320    #[test]
321    fn mode_4_single_subblock_uncompressed() {
322        // When compressed_size >= decompressed_size, data is stored raw
323        let original = b"raw data";
324        let decompressed_size = original.len() as u32;
325        let compressed_size = decompressed_size; // equal means uncompressed
326
327        let mut block = vec![b'4'];
328        block.extend_from_slice(&decompressed_size.to_le_bytes());
329        block.extend_from_slice(&compressed_size.to_le_bytes());
330        block.extend_from_slice(original);
331
332        let result = decode_block(&block).unwrap();
333        assert_eq!(result, original);
334    }
335
336    #[test]
337    fn mode_4_multiple_subblocks() {
338        // Use repetitive data so LZ4 compresses smaller than original
339        let part1: Vec<u8> = b"BBBB".repeat(200);
340        let part2: Vec<u8> = b"CCCC".repeat(300);
341        let compressed1 = lz4_flex::compress(&part1);
342        let compressed2 = lz4_flex::compress(&part2);
343        assert!(compressed1.len() < part1.len());
344        assert!(compressed2.len() < part2.len());
345
346        let mut block = vec![b'4'];
347        // Sub-block 1
348        block.extend_from_slice(&(part1.len() as u32).to_le_bytes());
349        block.extend_from_slice(&(compressed1.len() as u32).to_le_bytes());
350        block.extend_from_slice(&compressed1);
351        // Sub-block 2
352        block.extend_from_slice(&(part2.len() as u32).to_le_bytes());
353        block.extend_from_slice(&(compressed2.len() as u32).to_le_bytes());
354        block.extend_from_slice(&compressed2);
355
356        let result = decode_block(&block).unwrap();
357        let expected: Vec<u8> = [part1.as_slice(), part2.as_slice()].concat();
358        assert_eq!(result, expected);
359    }
360
361    #[test]
362    fn mode_4_empty_returns_empty() {
363        let block = vec![b'4'];
364        let result = decode_block(&block).unwrap();
365        assert!(result.is_empty());
366    }
367
368    #[test]
369    fn mode_4_truncated_header_errors() {
370        // Only 4 bytes after mode (need 8 for decompressed_size + compressed_size)
371        let block = vec![b'4', 0x10, 0x00, 0x00, 0x00];
372        assert!(decode_block(&block).is_err());
373    }
374}