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