use super::DepotKey;
use crate::util::checksum::SteamAdler32;
use std::io::Read;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ChunkCompression {
VZstd,
VZlzma,
Lzma,
Zip,
None,
}
impl ChunkCompression {
pub fn detect(data: &[u8]) -> Self {
if data.len() < 2 {
return Self::None;
}
match &data[..2] {
[0x56, 0x53] => Self::VZstd, [0x56, 0x5A] => Self::VZlzma, [0x5D, _] => Self::Lzma,
[0x50, 0x4B] => Self::Zip, _ => Self::None,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ChunkError {
#[error("chunk data too short")]
TooShort,
#[error("size mismatch: expected {expected}, got {actual}")]
SizeMismatch { expected: u32, actual: u32 },
#[error("checksum mismatch: expected {expected:#010x}, got {actual:#010x}")]
ChecksumMismatch { expected: u32, actual: u32 },
#[error("empty archive")]
EmptyArchive,
#[error("crypto: {0}")]
Crypto(#[from] crate::error::CryptoError),
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("zip: {0}")]
Zip(String),
}
pub fn process_chunk(
data: &[u8],
depot_key: &DepotKey,
expected_size: u32,
expected_checksum: u32,
) -> Result<Vec<u8>, ChunkError> {
let _ = expected_checksum; if data.len() < 4 {
return Err(ChunkError::TooShort);
}
if data.len() < 32 {
return Err(ChunkError::TooShort);
}
let iv = crate::crypto::symmetric_decrypt_ecb_nopad(&data[..16], &depot_key.0)?;
let decrypted = crate::crypto::symmetric_decrypt_cbc(&data[16..], &depot_key.0, &iv)?;
tracing::debug!(
"chunk decrypted: {} bytes, first 20: {:02x?}, compression: {:?}",
decrypted.len(),
&decrypted[..decrypted.len().min(20)],
ChunkCompression::detect(&decrypted)
);
let decompressed = decompress(&decrypted, expected_size)?;
if decompressed.len() != expected_size as usize {
return Err(ChunkError::SizeMismatch {
expected: expected_size,
actual: decompressed.len() as u32,
});
}
let checksum = SteamAdler32::compute(&decompressed);
if checksum.0 != expected_checksum {
return Err(ChunkError::ChecksumMismatch {
expected: expected_checksum,
actual: checksum.0,
});
}
Ok(decompressed)
}
fn decompress(data: &[u8], expected_size: u32) -> Result<Vec<u8>, ChunkError> {
match ChunkCompression::detect(data) {
ChunkCompression::VZstd => {
const HEADER: usize = 4 + 4; const FOOTER: usize = 4 + 8 + 3; if data.len() < HEADER + FOOTER {
return Err(ChunkError::TooShort);
}
let compressed = &data[HEADER..data.len() - FOOTER];
let output = zstd::bulk::decompress(compressed, expected_size as usize)
.map_err(|e| ChunkError::Io(std::io::Error::other(e)))?;
Ok(output)
}
ChunkCompression::VZlzma => {
const HEADER: usize = 3 + 4; const PROPS: usize = 5; const FOOTER: usize = 4 + 4 + 2; if data.len() < HEADER + PROPS + FOOTER {
return Err(ChunkError::TooShort);
}
let props = &data[HEADER..HEADER + PROPS];
let lzma_data = &data[HEADER + PROPS..data.len() - FOOTER];
let mut lzma_stream = Vec::with_capacity(13 + lzma_data.len());
lzma_stream.extend_from_slice(props);
lzma_stream.extend_from_slice(&(expected_size as u64).to_le_bytes());
lzma_stream.extend_from_slice(lzma_data);
let mut output = Vec::with_capacity(expected_size as usize);
lzma_rs::lzma_decompress(&mut std::io::Cursor::new(&lzma_stream), &mut output)
.map_err(|e| ChunkError::Io(std::io::Error::other(e)))?;
Ok(output)
}
ChunkCompression::Lzma => {
let mut output = Vec::new();
lzma_rs::lzma_decompress(&mut std::io::Cursor::new(data), &mut output)
.map_err(|e| ChunkError::Io(std::io::Error::other(e)))?;
Ok(output)
}
ChunkCompression::Zip => {
let cursor = std::io::Cursor::new(data);
let mut archive =
zip::ZipArchive::new(cursor).map_err(|e| ChunkError::Zip(e.to_string()))?;
if archive.is_empty() {
return Err(ChunkError::EmptyArchive);
}
let mut file = archive
.by_index(0)
.map_err(|e| ChunkError::Zip(e.to_string()))?;
let mut output = Vec::new();
file.read_to_end(&mut output)?;
Ok(output)
}
ChunkCompression::None => Ok(data.to_vec()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_vzstd() {
assert_eq!(
ChunkCompression::detect(b"VSZa\x00\x00\x00\x00rest"),
ChunkCompression::VZstd
);
}
#[test]
fn detect_vzlzma() {
assert_eq!(
ChunkCompression::detect(b"VZa\x00\x00\x00\x00rest"),
ChunkCompression::VZlzma
);
}
#[test]
fn detect_lzma() {
assert_eq!(
ChunkCompression::detect(&[0x5D, 0x00, 0x00]),
ChunkCompression::Lzma
);
}
#[test]
fn detect_zip() {
assert_eq!(
ChunkCompression::detect(b"PK\x03\x04"),
ChunkCompression::Zip
);
}
#[test]
fn detect_none_for_unknown() {
assert_eq!(
ChunkCompression::detect(b"\x00\x00\x00\x00"),
ChunkCompression::None
);
}
#[test]
fn detect_none_for_short_input() {
assert_eq!(ChunkCompression::detect(b""), ChunkCompression::None);
assert_eq!(ChunkCompression::detect(b"V"), ChunkCompression::None);
}
#[test]
fn process_chunk_too_short() {
let key = DepotKey([0; 32]);
assert!(matches!(
process_chunk(b"short", &key, 0, 0),
Err(ChunkError::TooShort)
));
}
#[test]
fn process_chunk_uncompressed_roundtrip() {
let key = DepotKey([0xAA; 32]);
let plaintext = b"test chunk data!";
let checksum = crate::util::checksum::SteamAdler32::compute(plaintext);
let iv = [0x42u8; 16];
let encrypted_iv = crate::crypto::symmetric_encrypt_ecb_nopad(&iv, &key.0).unwrap();
let encrypted_body = crate::crypto::symmetric_encrypt_cbc(plaintext, &key.0, &iv).unwrap();
let mut chunk_data = Vec::new();
chunk_data.extend_from_slice(&encrypted_iv);
chunk_data.extend_from_slice(&encrypted_body);
let result = process_chunk(&chunk_data, &key, plaintext.len() as u32, checksum.0).unwrap();
assert_eq!(result, plaintext);
}
#[test]
fn process_chunk_bad_checksum() {
let key = DepotKey([0xAA; 32]);
let plaintext = b"test chunk data!";
let iv = [0x42u8; 16];
let encrypted_iv = crate::crypto::symmetric_encrypt_ecb_nopad(&iv, &key.0).unwrap();
let encrypted_body = crate::crypto::symmetric_encrypt_cbc(plaintext, &key.0, &iv).unwrap();
let mut chunk_data = Vec::new();
chunk_data.extend_from_slice(&encrypted_iv);
chunk_data.extend_from_slice(&encrypted_body);
let result = process_chunk(&chunk_data, &key, plaintext.len() as u32, 0xDEADBEEF);
assert!(matches!(result, Err(ChunkError::ChecksumMismatch { .. })));
}
#[test]
fn process_chunk_wrong_key_fails() {
let key = DepotKey([0xAA; 32]);
let wrong_key = DepotKey([0xBB; 32]);
let plaintext = b"test chunk data!";
let iv = [0x42u8; 16];
let encrypted_iv = crate::crypto::symmetric_encrypt_ecb_nopad(&iv, &key.0).unwrap();
let encrypted_body = crate::crypto::symmetric_encrypt_cbc(plaintext, &key.0, &iv).unwrap();
let mut chunk_data = Vec::new();
chunk_data.extend_from_slice(&encrypted_iv);
chunk_data.extend_from_slice(&encrypted_body);
let result = process_chunk(&chunk_data, &wrong_key, plaintext.len() as u32, 0);
assert!(result.is_err());
}
}