use std::sync::Mutex;
use no_std_io2::io::Read;
use crate::error::BackhandError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct LzmaParams {
lc: u32,
lp: u32,
pb: u32,
dict_size: u32,
offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LzmaFormat {
Unknown,
Standard,
Adaptive(LzmaParams),
Undecodable,
}
pub(crate) struct LzmaCache(Mutex<LzmaFormat>);
impl LzmaCache {
pub(crate) const fn new() -> Self {
Self(Mutex::new(LzmaFormat::Unknown))
}
fn get(&self) -> LzmaFormat {
self.0.lock().map(|format| *format).unwrap_or(LzmaFormat::Unknown)
}
fn set(&self, format: LzmaFormat) {
if let Ok(mut cached) = self.0.lock() {
*cached = format;
}
}
}
impl Clone for LzmaCache {
fn clone(&self) -> Self {
Self::new()
}
}
impl core::fmt::Debug for LzmaCache {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("LzmaCache").field(&self.get()).finish()
}
}
impl Default for LzmaCache {
fn default() -> Self {
Self::new()
}
}
pub(crate) const MAX_BLOCK_SIZE: usize = 1 << 20;
pub(crate) const DEFAULT_BLOCK_SIZE: usize = 1 << 17;
const LZMA_MAX_LC_PLUS_LP: u32 = 4;
const LZMA_MAX_LC: u32 = 4;
const LZMA_MAX_LP: u32 = 4;
const LZMA_MAX_PB: u32 = 4;
const LZMA_MAX_OFFSET: usize = 10;
const LZMA_DEFAULT_PARAMS: LzmaParams =
LzmaParams { lc: 3, lp: 0, pb: 2, dict_size: DICT_SIZE_DEFAULT, offset: 0 };
const DICT_SIZE_DEFAULT: u32 = 0x800000;
const DICT_SIZE_FALLBACKS: [u32; 2] = [0x100000, 0x400000];
pub(crate) fn decompress_adaptive(
bytes: &[u8],
out: &mut Vec<u8>,
cache: &LzmaCache,
max_out: usize,
) -> Result<(), BackhandError> {
if bytes.is_empty() {
return Ok(());
}
match cache.get() {
LzmaFormat::Adaptive(params) => {
if let Ok(result) = try_lzma_with_params(bytes, params, max_out) {
out.extend_from_slice(&result);
return Ok(());
}
trace!("cached LZMA parameters failed, searching again");
}
LzmaFormat::Standard => {
if try_standard_lzma(bytes, out) {
return Ok(());
}
trace!("cached standard LZMA failed, searching again");
}
LzmaFormat::Undecodable => {
return Err(BackhandError::UnsupportedCompression(err_text!(
"no LZMA parameters decompress this image"
)));
}
LzmaFormat::Unknown => {}
}
if try_standard_lzma(bytes, out) {
trace!("standard LZMA decompressed the block");
cache.set(LzmaFormat::Standard);
return Ok(());
}
if let Some((result, params)) = search_lzma_params(bytes, max_out) {
trace!("found LZMA parameters {:?}", params);
cache.set(LzmaFormat::Adaptive(params));
out.extend_from_slice(&result);
return Ok(());
}
cache.set(LzmaFormat::Undecodable);
Err(BackhandError::UnsupportedCompression(err_text!(
"no LZMA parameters decompress this image"
)))
}
fn try_standard_lzma(bytes: &[u8], out: &mut Vec<u8>) -> bool {
if let Ok(mut reader) = lzma_rust2::LzmaReader::new_mem_limit(bytes, u32::MAX, None) {
if reader.read_to_end(out).is_ok() {
return true;
}
out.clear();
}
false
}
fn try_lzma_with_params(
bytes: &[u8],
params: LzmaParams,
max_out: usize,
) -> Result<Vec<u8>, BackhandError> {
if params.offset >= bytes.len() {
return Err(BackhandError::UnsupportedCompression(err_text!("invalid offset")));
}
let dict_size = if params.dict_size == 0xFFFFFFFF || params.dict_size == 0 {
DICT_SIZE_DEFAULT
} else {
params.dict_size
};
lzma_adaptive_sys::decompress_lzma(
bytes,
params.lc,
params.lp,
params.pb,
dict_size,
params.offset,
max_out,
)
.map_err(|code| {
BackhandError::UnsupportedCompression(err_text!("LZMA decompression failed: {code}"))
})
}
fn search_lzma_params(bytes: &[u8], max_out: usize) -> Option<(Vec<u8>, LzmaParams)> {
trace!("searching for LZMA parameters");
if let Ok(result) = try_lzma_with_params(bytes, LZMA_DEFAULT_PARAMS, max_out) {
return Some((result, LZMA_DEFAULT_PARAMS));
}
for offset in 0..=LZMA_MAX_OFFSET {
match bytes.get(offset) {
Some(0) => {}
_ => continue,
}
for lc in 0..=LZMA_MAX_LC {
for lp in 0..=LZMA_MAX_LP {
if lc + lp > LZMA_MAX_LC_PLUS_LP {
continue;
}
for pb in 0..=LZMA_MAX_PB {
for dict_size in core::iter::once(DICT_SIZE_DEFAULT).chain(DICT_SIZE_FALLBACKS)
{
let params = LzmaParams { lc, lp, pb, dict_size, offset };
if params == LZMA_DEFAULT_PARAMS {
continue;
}
if let Ok(result) = try_lzma_with_params(bytes, params, max_out) {
return Some((result, params));
}
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const MAX_OUT: usize = 0x2000;
#[test]
fn empty_input_writes_nothing() {
let cache = LzmaCache::new();
let mut out = Vec::new();
decompress_adaptive(&[], &mut out, &cache, MAX_OUT).unwrap();
assert!(out.is_empty());
assert_eq!(cache.get(), LzmaFormat::Unknown);
}
#[test]
fn garbage_input_reports_unsupported() {
let cache = LzmaCache::new();
let mut out = Vec::new();
let error = decompress_adaptive(&[0xff; 64], &mut out, &cache, MAX_OUT).unwrap_err();
assert!(matches!(error, BackhandError::UnsupportedCompression(_)));
assert!(out.is_empty());
}
#[test]
fn undecodable_block_is_not_searched_twice() {
let cache = LzmaCache::new();
let mut out = Vec::new();
let _ = decompress_adaptive(&[0xff; 64], &mut out, &cache, MAX_OUT);
assert_eq!(cache.get(), LzmaFormat::Undecodable);
let error = decompress_adaptive(&[0xff; 64], &mut out, &cache, MAX_OUT).unwrap_err();
assert!(matches!(error, BackhandError::UnsupportedCompression(_)));
}
#[test]
fn caches_are_independent() {
let first = LzmaCache::new();
let second = LzmaCache::new();
first.set(LzmaFormat::Standard);
assert_eq!(first.get(), LzmaFormat::Standard);
assert_eq!(second.get(), LzmaFormat::Unknown);
}
#[test]
fn clone_starts_empty() {
let cache = LzmaCache::new();
cache.set(LzmaFormat::Adaptive(LZMA_DEFAULT_PARAMS));
assert_eq!(cache.clone().get(), LzmaFormat::Unknown);
assert_eq!(cache.get(), LzmaFormat::Adaptive(LZMA_DEFAULT_PARAMS));
}
#[test]
fn search_skips_offsets_that_cannot_start_a_stream() {
assert!(search_lzma_params(&[0xab; 32], MAX_OUT).is_none());
}
#[test]
fn search_ends_when_every_offset_looks_valid() {
assert!(search_lzma_params(&[0x00; 128], MAX_OUT).is_none());
}
#[test]
fn truncated_input_does_not_panic() {
for len in 0..LZMA_MAX_OFFSET + 2 {
let cache = LzmaCache::new();
let mut out = Vec::new();
let _ = decompress_adaptive(&vec![0x00; len], &mut out, &cache, MAX_OUT);
}
}
}