From fe80ef32f96fac4c87b53f0f540227cc01dd1cc8 Mon Sep 17 00:00:00 2001 From: jsenior10 Date: Thu, 12 Feb 2026 22:50:21 +0000 Subject: [PATCH 1/4] compressed stuff --- Cargo.lock | 7 +++++ Cargo.toml | 1 + src/util/xex.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index bc2825d..8f12704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,7 @@ dependencies = [ "indexmap", "itertools", "log", + "lzxd", "memchr", "memmap2", "mimalloc", @@ -801,6 +802,12 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lzxd" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9" + [[package]] name = "matchers" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e01c22b..4db3a3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } xxhash-rust = { version = "0.8", features = ["xxh3"] } zerocopy = { version = "0.8", features = ["derive"] } pdb = "0.8.0" +lzxd = "0.2.6" [target.'cfg(target_env = "musl")'.dependencies] mimalloc = "0.1" diff --git a/src/util/xex.rs b/src/util/xex.rs index 1ebbe6a..c89387e 100644 --- a/src/util/xex.rs +++ b/src/util/xex.rs @@ -1,3 +1,4 @@ +use std::io::Cursor; use std::{ borrow::Cow, cmp::min, @@ -8,6 +9,8 @@ use std::{ }; use anyhow::{anyhow, bail, ensure, Result}; +use byteorder::{BigEndian, ReadBytesExt}; +use lzxd::Lzxd; use memchr::memmem; use num_enum::{IntoPrimitive, TryFromPrimitive}; use object::{ @@ -647,7 +650,74 @@ impl XexInfo { pe_image = compressed.to_vec(); } XexCompression::Compressed => { - bail!("This xex is compressed using LZX, which is not currently supported."); + let comp = bff.normal.as_ref().unwrap(); + let window_size = comp.window_size as usize; + //let lzx_window = match window_size { + // 32768 => lzxd::WindowSize::KB32, + // 65536 => lzxd::WindowSize::KB64, + // 131072 => lzxd::WindowSize::KB128, + // 262144 => lzxd::WindowSize::KB256, + // 524288 => lzxd::WindowSize::KB512, + // 1048576 => lzxd::WindowSize::MB1, + // 2097152 => lzxd::WindowSize::MB2, + // _ => bail!("LZX: bad window size: {}", window_size), + //}; + let lzx_window = lzxd::WindowSize::KB32; + let mut lzxd_state = Lzxd::new(lzx_window); + let mut current_block_size = comp.block_size as usize; + + while current_block_size != 0 { + if pos_in + current_block_size > compressed.len() { + bail!("LZX: block needs {} bytes at 0x{:X} but only {} remain", current_block_size, pos_in, compressed.len() - pos_in); + } + let block = &compressed[pos_in..pos_in + current_block_size]; + pos_in += current_block_size; + if block.len() < 24 { + bail!("LZX: block too small for header: {} bytes", block.len()); + } + let next_block_size = u32::from_be_bytes([ + block[0], block[1], block[2], block[3], + ]) as usize; + let mut off = 24usize; + while off + 2 <= block.len() { + let chunk_len = u16::from_be_bytes([ + block[off], block[off + 1], + ]) as usize; + off += 2; + + if chunk_len == 0 { + break; + } + + if off + chunk_len > block.len() { + bail!("LZX: sub-chunk at offset {} wants {} bytes but only {} remain", off, chunk_len, block.len() - off); + } + let chunk_data = &block[off..off + chunk_len]; + off += chunk_len; + let expected = std::cmp::min(window_size, pe_image.len().saturating_sub(pos_out), ); + if expected == 0 { + break; + } + let decompressed = lzxd_state.decompress_next(chunk_data, expected).map_err(|e| anyhow::anyhow!( + "LZX: decompress failed at pos_out=0x{:X} \ + (chunk_len={}, expected={}, block_off={}): {:?}", + pos_out, chunk_len, expected, off - chunk_len, e))?; + + if decompressed.is_empty() { + bail!("LZX: decompression returned zero bytes at pos_out=0x{:X}", pos_out); + } + + let copy_len = std::cmp::min(decompressed.len(), pe_image.len() - pos_out); + pe_image[pos_out..pos_out + copy_len] + .copy_from_slice(&decompressed[..copy_len]); + pos_out += copy_len; + } + current_block_size = next_block_size; + } + if pos_out == 0 { + bail!("LZX: produced zero output bytes"); + } + //bail!("This xex is compressed using LZX, which is not currently supported."); // this is actually pretty hard to implement, it involves use of the NormalCompression we retrieved earlier, // plus the use of microsoft's LZX decompression algorithms // here are some references if you try to attempt this From b42ce3c23c09f2cb9c319e206e04dab129ca0427 Mon Sep 17 00:00:00 2001 From: jsenior10 Date: Fri, 13 Feb 2026 00:14:21 +0000 Subject: [PATCH 2/4] not needed assuming window size is the same for all lzx compressed xexs --- src/util/xex.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/util/xex.rs b/src/util/xex.rs index c89387e..c356858 100644 --- a/src/util/xex.rs +++ b/src/util/xex.rs @@ -652,16 +652,6 @@ impl XexInfo { XexCompression::Compressed => { let comp = bff.normal.as_ref().unwrap(); let window_size = comp.window_size as usize; - //let lzx_window = match window_size { - // 32768 => lzxd::WindowSize::KB32, - // 65536 => lzxd::WindowSize::KB64, - // 131072 => lzxd::WindowSize::KB128, - // 262144 => lzxd::WindowSize::KB256, - // 524288 => lzxd::WindowSize::KB512, - // 1048576 => lzxd::WindowSize::MB1, - // 2097152 => lzxd::WindowSize::MB2, - // _ => bail!("LZX: bad window size: {}", window_size), - //}; let lzx_window = lzxd::WindowSize::KB32; let mut lzxd_state = Lzxd::new(lzx_window); let mut current_block_size = comp.block_size as usize; From a0671c057e692a6c4ca305431ace9af003dc1d0a Mon Sep 17 00:00:00 2001 From: jsenior10 Date: Fri, 13 Feb 2026 00:17:33 +0000 Subject: [PATCH 3/4] remove func call scopes as not needed --- src/util/xex.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/xex.rs b/src/util/xex.rs index c356858..3fc2610 100644 --- a/src/util/xex.rs +++ b/src/util/xex.rs @@ -684,7 +684,7 @@ impl XexInfo { } let chunk_data = &block[off..off + chunk_len]; off += chunk_len; - let expected = std::cmp::min(window_size, pe_image.len().saturating_sub(pos_out), ); + let expected = min(window_size, pe_image.len().saturating_sub(pos_out), ); if expected == 0 { break; } @@ -697,7 +697,7 @@ impl XexInfo { bail!("LZX: decompression returned zero bytes at pos_out=0x{:X}", pos_out); } - let copy_len = std::cmp::min(decompressed.len(), pe_image.len() - pos_out); + let copy_len = min(decompressed.len(), pe_image.len() - pos_out); pe_image[pos_out..pos_out + copy_len] .copy_from_slice(&decompressed[..copy_len]); pos_out += copy_len; From 80fadbc03885e443b715259df5768c8f05a6e875 Mon Sep 17 00:00:00 2001 From: jsenior10 Date: Fri, 13 Feb 2026 02:02:09 +0000 Subject: [PATCH 4/4] remove unused include --- src/util/xex.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/util/xex.rs b/src/util/xex.rs index 3fc2610..2492399 100644 --- a/src/util/xex.rs +++ b/src/util/xex.rs @@ -1,4 +1,3 @@ -use std::io::Cursor; use std::{ borrow::Cow, cmp::min,