pub fn decompress(data: &[u8]) -> Result<Vec<u8>, String> {
match detect_version(data) {
None => Ok(data.to_vec()),
Some(ver) => do_decompress(data, ver),
}
}
pub fn is_compressed(data: &[u8]) -> bool {
detect_version(data).is_some()
}
#[derive(Clone, Copy, PartialEq)]
enum Ver { V090, V091 }
const SIG90: &[u8] = &[
0x06,0x0E,0x1F,0x8B,0x0E,0x0C,0x00,0x8B,0xF1,0x4E,0x89,0xF7,0x8C,0xDB,
0x03,0x1E,0x0A,0x00,0x8E,0xC3,0xB4,0x00,0x31,0xED,
];
const SIG91: &[u8] = &[
0x06,0x0E,0x1F,0x8B,0x0E,0x0C,0x00,0x8B,0xF1,0x4E,0x89,0xF7,0x8C,0xDB,
0x03,0x1E,0x0A,0x00,0x8E,0xC3,0xFD,0xF3,0xA4,0x53,
];
const SIG91E_PREFIX: u8 = 0x50;
fn read_u16le(data: &[u8], off: usize) -> u16 {
u16::from_le_bytes([data[off], data[off + 1]])
}
fn write_u16le(out: &mut [u8], off: usize, v: u16) {
let b = v.to_le_bytes();
out[off] = b[0];
out[off + 1] = b[1];
}
fn entry_point(data: &[u8]) -> usize {
let header_paras = read_u16le(data, 0x08) as usize; let cs = read_u16le(data, 0x16) as usize; let ip = read_u16le(data, 0x14) as usize; ((header_paras + cs) << 4) + ip
}
fn detect_version(data: &[u8]) -> Option<Ver> {
if data.len() < 0x20 {
return None;
}
let magic = read_u16le(data, 0);
if magic != 0x5A4D && magic != 0x4D5A {
return None;
}
if read_u16le(data, 0x18) != 0x1C || read_u16le(data, 0x06) != 0 {
return None;
}
let ep = entry_point(data);
if ep + SIG91.len() > data.len() {
return None;
}
let sig = &data[ep..ep + SIG91.len()];
if sig == SIG90 {
return Some(Ver::V090);
}
if sig == SIG91 {
return Some(Ver::V091);
}
if data[ep] == SIG91E_PREFIX && ep + 1 + SIG91.len() <= data.len()
&& &data[ep+1..ep+1+SIG91.len()] == SIG91
{
return Some(Ver::V091);
}
None
}
struct Bits<'a> {
src: &'a [u8],
pos: usize,
buf: u16,
count: u8,
}
impl<'a> Bits<'a> {
fn new(src: &'a [u8], pos: usize) -> Self {
let buf = u16::from_le_bytes([src[pos], src[pos + 1]]);
Bits { src, pos: pos + 2, buf, count: 16 }
}
fn get(&mut self) -> u8 {
let b = (self.buf & 1) as u8;
self.buf >>= 1;
self.count -= 1;
if self.count == 0 {
self.buf = u16::from_le_bytes([self.src[self.pos], self.src[self.pos + 1]]);
self.pos += 2;
self.count = 16;
}
b
}
fn byte(&mut self) -> u8 {
let b = self.src[self.pos];
self.pos += 1;
b
}
}
fn do_decompress(data: &[u8], ver: Ver) -> Result<Vec<u8>, String> {
let ihead: Vec<u16> = (0..16).map(|i| read_u16le(data, i * 2)).collect();
let cs_off = ((ihead[0x04] + ihead[0x0b]) as usize) << 4;
if cs_off + 16 > data.len() {
return Err("Truncated LZEXE header".into());
}
let info: Vec<u16> = (0..8).map(|i| read_u16le(data, cs_off + i * 2)).collect();
let relocs = match ver {
Ver::V090 => decompress_reloc_90(data, cs_off)?,
Ver::V091 => decompress_reloc_91(data, cs_off)?,
};
let reloc_bytes = relocs.len() * 4;
let header_raw = 0x1C + reloc_bytes;
let header_size = (header_raw + 0x1FF) & !0x1FF;
let in_start = ((ihead[0x0b] as i32 - info[4] as i32 + ihead[0x04] as i32) << 4) as usize;
let mut image: Vec<u8> = Vec::with_capacity(200 * 1024);
let mut bits = Bits::new(data, in_start);
loop {
if bits.get() == 1 {
image.push(bits.byte());
continue;
}
let (span, len): (i32, usize) = if bits.get() == 0 {
let len_bits = (bits.get() as i32) << 1 | bits.get() as i32;
let len = (len_bits + 2) as usize;
let raw = (bits.byte() as u16) | 0xFF00u16;
(raw as i16 as i32, len)
} else {
let lo = bits.byte() as u32;
let hi = bits.byte() as u32;
let raw = (lo | (((hi & !0x07u32) << 5) | 0xE000u32)) as u16;
let span = raw as i16 as i32;
let mut len = (hi & 0x07) as usize + 2;
if len == 2 {
len = bits.byte() as usize;
if len == 0 { break; }
if len == 1 { continue; }
len += 1;
}
(span, len)
};
let pos = image.len();
for i in 0..len {
let src_idx = (pos as i32 + i as i32 + span) as usize;
let byte = *image.get(src_idx)
.ok_or_else(|| format!("Back-reference out of bounds: pos={pos} span={span} i={i}"))?;
image.push(byte);
}
}
let total_size = header_size + image.len();
let mut out = vec![0u8; total_size];
out[0] = 0x4D; out[1] = 0x5A;
let file_bytes = total_size;
write_u16le(&mut out, 0x02, (file_bytes & 0x1FF) as u16);
write_u16le(&mut out, 0x04, ((file_bytes + 0x1FF) >> 9) as u16);
write_u16le(&mut out, 0x06, relocs.len() as u16);
write_u16le(&mut out, 0x08, (header_size >> 4) as u16); write_u16le(&mut out, 0x18, 0x1C);
let dcmpsizepar = ((info[6] as u32 + 15) >> 4) as u16;
let n = info[5].wrapping_add(dcmpsizepar).wrapping_add(9);
let min_alloc = ihead[0x05].wrapping_sub(n);
let max_alloc = if ihead[0x06] == 0xFFFF {
0xFFFFu16
} else {
ihead[0x06].wrapping_sub(n)
};
write_u16le(&mut out, 0x0A, min_alloc);
write_u16le(&mut out, 0x0C, max_alloc);
write_u16le(&mut out, 0x0E, info[3]); write_u16le(&mut out, 0x10, info[2]); write_u16le(&mut out, 0x14, info[0]); write_u16le(&mut out, 0x16, info[1]);
for (i, &(off, seg)) in relocs.iter().enumerate() {
let base = 0x1C + i * 4;
write_u16le(&mut out, base, off);
write_u16le(&mut out, base + 2, seg);
}
out[header_size..].copy_from_slice(&image);
Ok(out)
}
fn decompress_reloc_91(data: &[u8], cs_off: usize) -> Result<Vec<(u16, u16)>, String> {
let mut pos = cs_off + 0x158;
let mut entries: Vec<(u16, u16)> = Vec::new();
let mut rel_seg = 0u32;
let mut rel_off = 0u32;
loop {
if pos >= data.len() {
return Err("Relocation table truncated".into());
}
let b = data[pos] as u32;
pos += 1;
let span: u32 = if b == 0 {
if pos + 2 > data.len() { return Err("Relocation table truncated".into()); }
let w = u16::from_le_bytes([data[pos], data[pos+1]]) as u32;
pos += 2;
match w {
0 => { rel_seg += 0x0FFF; continue; }
1 => break,
_ => w,
}
} else {
b
};
rel_off += span;
rel_seg += rel_off >> 4;
rel_off &= 0x0F;
entries.push((rel_off as u16, rel_seg as u16));
}
Ok(entries)
}
fn decompress_reloc_90(data: &[u8], cs_off: usize) -> Result<Vec<(u16, u16)>, String> {
let mut pos = cs_off + 0x19D;
let mut entries: Vec<(u16, u16)> = Vec::new();
let mut rel_seg = 0u32;
loop {
if pos + 2 > data.len() { return Err("Relocation table truncated".into()); }
let count = u16::from_le_bytes([data[pos], data[pos+1]]) as usize;
pos += 2;
for _ in 0..count {
if pos + 2 > data.len() { return Err("Relocation table truncated".into()); }
let rel_off = u16::from_le_bytes([data[pos], data[pos+1]]);
pos += 2;
entries.push((rel_off, rel_seg as u16));
}
rel_seg += 0x1000;
if rel_seg >= 0xF000 + 0x1000 { break; }
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uncompressed_passthrough() {
let data = vec![0x4D, 0x5A, 0x00, 0x01]; let out = decompress(&data).unwrap();
assert_eq!(out, data);
}
#[test]
fn non_exe_passthrough() {
let data = b"hello world".to_vec();
let out = decompress(&data).unwrap();
assert_eq!(out, data);
}
}