use std::io::{Cursor, Read};
use flate2::read::{DeflateDecoder, GzDecoder};
pub type DecompressResult<T> = std::result::Result<T, DecompressError>;
#[derive(Debug)]
pub enum DecompressError {
InvalidLzmaHeader,
LzmaError(String),
DeflateError(String),
BufferTooSmall,
}
impl std::fmt::Display for DecompressError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidLzmaHeader => write!(f, "Invalid LZMA header"),
Self::LzmaError(msg) => write!(f, "LZMA decompression error: {msg}"),
Self::DeflateError(msg) => write!(f, "Deflate decompression error: {msg}"),
Self::BufferTooSmall => write!(f, "Input buffer too small"),
}
}
}
impl std::error::Error for DecompressError {}
const MAX_DECOMPRESSED_SIZE: u64 = 512 * 1024 * 1024;
const LZMA_HEADER_LAYOUTS: [usize; 2] = [
13, 9, ];
fn valid_lzma_props(data: &[u8]) -> bool {
let Some(&props_byte) = data.first() else {
return false;
};
if props_byte > 224 {
return false;
}
let Some(dict_bytes) = data.get(1..5).and_then(|s| <[u8; 4]>::try_from(s).ok()) else {
return false;
};
let dict_size = u32::from_le_bytes(dict_bytes);
(1024u32..=1024u32.saturating_mul(1024).saturating_mul(1024)).contains(&dict_size)
}
fn declared_size(data: &[u8], header_len: usize) -> Option<Option<u64>> {
let size = match header_len {
13 => u64::from_le_bytes(data.get(5..13)?.try_into().ok()?),
_ => u64::from(u32::from_le_bytes(data.get(5..9)?.try_into().ok()?)),
};
if size == u64::MAX || size == u64::from(u32::MAX) {
return Some(None);
}
if size == 0 || size > MAX_DECOMPRESSED_SIZE {
return None;
}
Some(Some(size))
}
#[must_use]
pub fn is_confuserex_lzma(data: &[u8]) -> bool {
if data.len() < 13 {
return false;
}
if !valid_lzma_props(data) {
return false;
}
LZMA_HEADER_LAYOUTS
.iter()
.any(|&len| declared_size(data, len).is_some())
}
pub fn decompress_confuserex_lzma(data: &[u8]) -> DecompressResult<Vec<u8>> {
if data.len() < 9 {
return Err(DecompressError::BufferTooSmall);
}
if !valid_lzma_props(data) {
return Err(DecompressError::InvalidLzmaHeader);
}
let props = data.get(0..5).ok_or(DecompressError::BufferTooSmall)?;
let mut last_err: Option<DecompressError> = None;
for &header_len in &LZMA_HEADER_LAYOUTS {
let Some(size) = declared_size(data, header_len) else {
continue;
};
let Some(compressed) = data.get(header_len..) else {
continue;
};
if compressed.is_empty() {
continue;
}
let mut lzma_stream = Vec::with_capacity(compressed.len().saturating_add(13));
lzma_stream.extend_from_slice(props);
lzma_stream.extend_from_slice(&size.unwrap_or(u64::MAX).to_le_bytes());
lzma_stream.extend_from_slice(compressed);
let mut cursor = Cursor::new(&lzma_stream);
let mut decompressed = Vec::new();
match lzma_rs::lzma_decompress(&mut cursor, &mut decompressed) {
Ok(()) => {
if size.is_none_or(|expected| decompressed.len() as u64 == expected) {
return Ok(decompressed);
}
last_err = Some(DecompressError::InvalidLzmaHeader);
}
Err(e) => last_err = Some(DecompressError::LzmaError(e.to_string())),
}
}
Err(last_err.unwrap_or(DecompressError::InvalidLzmaHeader))
}
pub fn decompress_deflate(data: &[u8]) -> DecompressResult<Vec<u8>> {
let mut decoder = DeflateDecoder::new(data);
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.map_err(|e| DecompressError::DeflateError(e.to_string()))?;
Ok(decompressed)
}
pub fn decompress_gzip(data: &[u8]) -> DecompressResult<Vec<u8>> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.map_err(|e| DecompressError::DeflateError(e.to_string()))?;
Ok(decompressed)
}
#[cfg(test)]
mod tests {
use std::io::Write;
use flate2::{
write::{DeflateEncoder, GzEncoder},
Compression,
};
use super::*;
#[test]
fn test_is_confuserex_lzma_valid() {
let valid_header = [
0x5D, 0x00, 0x00, 0x10, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
assert!(is_confuserex_lzma(&valid_header));
}
#[test]
fn test_is_confuserex_lzma_invalid_props() {
let invalid_props = [
0xFF, 0x00, 0x00, 0x10, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(!is_confuserex_lzma(&invalid_props));
}
#[test]
fn test_is_confuserex_lzma_too_small() {
let too_small = [0x5D, 0x00, 0x00, 0x10, 0x00, 0x64, 0x00, 0x00, 0x00];
assert!(!is_confuserex_lzma(&too_small));
}
const FORK_BLOB: [u8; 64] = [
0x5D, 0x00, 0x00, 0x80, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x35, 0x03, 0xA1, 0xBC, 0x67, 0x7D, 0x8E, 0xD0, 0x35, 0x60, 0x52, 0x59,
0x6E, 0x4A, 0xF6, 0x76, 0x12, 0xF7, 0xD1, 0x80, 0xD2, 0xA5, 0xEA, 0x78, 0xC3, 0x73, 0x0E,
0x4B, 0x7C, 0xD8, 0x8E, 0xF4, 0xE6, 0x1C, 0x93, 0x81, 0x81, 0x68, 0xCC, 0xEC, 0x3A, 0x04,
0x8E, 0x00, 0x00, 0x00, 0x00, 0x00,
];
#[test]
fn test_confuserex_lzma_standard_13_byte_header() {
assert!(is_confuserex_lzma(&FORK_BLOB));
let out = decompress_confuserex_lzma(&FORK_BLOB).unwrap();
assert_eq!(out.len(), 44, "must honour the declared size");
assert!(out.windows(8).any(|w| w == b"Result: "));
assert!(out.windows(27).any(|w| w == b"Hello From ConfuserEx test."));
}
#[test]
fn test_confuserex_lzma_accepts_payload_larger_than_output() {
assert!(FORK_BLOB.len() - 13 > 44);
assert!(is_confuserex_lzma(&FORK_BLOB));
}
#[test]
fn test_confuserex_lzma_stock_9_byte_header() {
let original = b"ConfuserEx constants blob, repeated repeated repeated repeated.";
let mut alone = Vec::new();
lzma_rs::lzma_compress(&mut Cursor::new(&original[..]), &mut alone).unwrap();
let mut stock = Vec::with_capacity(alone.len());
stock.extend_from_slice(&alone[0..5]);
stock.extend_from_slice(&(original.len() as u32).to_le_bytes());
stock.extend_from_slice(&alone[13..]);
assert!(is_confuserex_lzma(&stock));
assert_eq!(decompress_confuserex_lzma(&stock).unwrap(), original);
}
#[test]
fn test_confuserex_lzma_rejects_non_lzma() {
let junk = [
0x5D, 0x11, 0x22, 0x33, 0x44, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
assert!(!is_confuserex_lzma(&junk));
assert!(decompress_confuserex_lzma(&junk).is_err());
}
#[test]
fn test_decompress_deflate() {
let original = b"Hello, World! This is a test of deflate compression.";
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder.write_all(original).unwrap();
let compressed = encoder.finish().unwrap();
let decompressed = decompress_deflate(&compressed).unwrap();
assert_eq!(&decompressed, original);
}
#[test]
fn test_decompress_gzip() {
let original = b"Hello, World! This is a test of gzip compression.";
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(original).unwrap();
let compressed = encoder.finish().unwrap();
let decompressed = decompress_gzip(&compressed).unwrap();
assert_eq!(&decompressed, original);
}
}