use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use crate::{Error, Result};
pub trait OodleDecompressor: Send + Sync {
fn decompress_block(&self, compressed: &[u8], decompressed_size: usize) -> Result<Vec<u8>>;
fn name(&self) -> &'static str;
fn is_full_support(&self) -> bool {
false
}
}
pub struct OozextractBackend {
extractor: Mutex<oozextract::Extractor>,
}
impl OozextractBackend {
pub fn new() -> Self {
Self {
extractor: Mutex::new(oozextract::Extractor::new()),
}
}
}
impl Default for OozextractBackend {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for OozextractBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OozextractBackend").finish_non_exhaustive()
}
}
impl OodleDecompressor for OozextractBackend {
fn decompress_block(&self, compressed: &[u8], decompressed_size: usize) -> Result<Vec<u8>> {
let mut output = vec![0u8; decompressed_size];
let mut extractor = self.extractor.lock().unwrap();
let actual = extractor
.read_from_slice(compressed, &mut output)
.map_err(|e| Error::Oodle(format!("oozextract: {:?}", e)))?;
if actual != decompressed_size {
return Err(Error::DecompressionSize {
expected: decompressed_size,
actual,
});
}
Ok(output)
}
fn name(&self) -> &'static str {
"oozextract"
}
fn is_full_support(&self) -> bool {
false
}
}
#[cfg(target_os = "windows")]
pub struct NativeBackend {
decompress_fn: OodleLzDecompress,
}
#[cfg(target_os = "windows")]
type OodleLzDecompress = unsafe extern "C" fn(
comp_buf: *const u8,
comp_len: isize,
raw_buf: *mut u8,
raw_len: isize,
fuzz_safe: i32,
check_crc: i32,
verbosity: i32,
dec_buf_base: *mut u8,
dec_buf_size: isize,
fp_callback: *mut std::ffi::c_void,
callback_user_data: *mut std::ffi::c_void,
decoder_memory: *mut u8,
decoder_memory_size: isize,
thread_phase: i32,
) -> isize;
#[cfg(target_os = "windows")]
impl NativeBackend {
pub fn load<P: AsRef<Path>>(dll_path: P) -> Result<Self> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let path = dll_path.as_ref();
let wide_path: Vec<u16> = OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
let handle = winapi::um::libloaderapi::LoadLibraryW(wide_path.as_ptr());
if handle.is_null() {
return Err(Error::Oodle(format!(
"Failed to load Oodle DLL: {}",
path.display()
)));
}
let proc_name = b"OodleLZ_Decompress\0";
let proc =
winapi::um::libloaderapi::GetProcAddress(handle, proc_name.as_ptr() as *const i8);
if proc.is_null() {
return Err(Error::Oodle(
"Failed to find OodleLZ_Decompress in DLL".to_string(),
));
}
Ok(Self {
decompress_fn: std::mem::transmute(proc),
})
}
}
}
#[cfg(target_os = "windows")]
impl OodleDecompressor for NativeBackend {
fn decompress_block(&self, compressed: &[u8], decompressed_size: usize) -> Result<Vec<u8>> {
let mut output = vec![0u8; decompressed_size];
let result = unsafe {
(self.decompress_fn)(
compressed.as_ptr(),
compressed.len() as isize,
output.as_mut_ptr(),
decompressed_size as isize,
1, 0, 0, std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
0,
)
};
if result < 0 {
return Err(Error::Oodle(format!(
"OodleLZ_Decompress failed with code {}",
result
)));
}
if result as usize != decompressed_size {
return Err(Error::DecompressionSize {
expected: decompressed_size,
actual: result as usize,
});
}
Ok(output)
}
fn name(&self) -> &'static str {
"native"
}
fn is_full_support(&self) -> bool {
true
}
}
pub struct ExecBackend {
command: String,
}
impl ExecBackend {
pub fn new<S: Into<String>>(command: S) -> Self {
Self {
command: command.into(),
}
}
}
impl std::fmt::Debug for ExecBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecBackend")
.field("command", &self.command)
.finish()
}
}
impl OodleDecompressor for ExecBackend {
fn decompress_block(&self, compressed: &[u8], decompressed_size: usize) -> Result<Vec<u8>> {
let parts: Vec<&str> = self.command.split_whitespace().collect();
let (program, prefix_args) = parts
.split_first()
.ok_or_else(|| Error::Oodle("Empty exec command".into()))?;
let mut child = Command::new(program)
.args(prefix_args)
.arg("decompress")
.arg(decompressed_size.to_string())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
Error::Oodle(format!("Failed to spawn command '{}': {}", self.command, e))
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(compressed)
.map_err(|e| Error::Oodle(format!("Failed to write to command stdin: {}", e)))?;
}
let output = child
.wait_with_output()
.map_err(|e| Error::Oodle(format!("Failed to wait for command: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::Oodle(format!(
"Command '{}' failed with exit code {:?}: {}",
self.command,
output.status.code(),
stderr.trim()
)));
}
if output.stdout.len() != decompressed_size {
return Err(Error::DecompressionSize {
expected: decompressed_size,
actual: output.stdout.len(),
});
}
Ok(output.stdout)
}
fn name(&self) -> &'static str {
"exec"
}
fn is_full_support(&self) -> bool {
true
}
}
pub fn default_backend() -> Box<dyn OodleDecompressor> {
Box::new(OozextractBackend::new())
}
#[cfg(target_os = "windows")]
pub fn native_backend<P: AsRef<Path>>(dll_path: P) -> Result<Box<dyn OodleDecompressor>> {
Ok(Box::new(NativeBackend::load(dll_path)?))
}
#[cfg(not(target_os = "windows"))]
pub fn native_backend<P: AsRef<Path>>(_dll_path: P) -> Result<Box<dyn OodleDecompressor>> {
Err(Error::Oodle(
"Native Oodle DLL loading requires Windows. On Linux/macOS, use \
--oodle-exec with a decompression helper (add --oodle-fifo for Wine)"
.to_string(),
))
}
pub fn exec_backend<S: Into<String>>(command: S) -> Box<dyn OodleDecompressor> {
Box::new(ExecBackend::new(command))
}
#[cfg(unix)]
pub struct FifoExecBackend {
command: String,
_fifo_dir: tempfile::TempDir,
input_fifo: std::path::PathBuf,
output_fifo: std::path::PathBuf,
}
#[cfg(unix)]
impl FifoExecBackend {
pub fn new<S: Into<String>>(command: S) -> Result<Self> {
let fifo_dir = tempfile::tempdir()
.map_err(|e| Error::Oodle(format!("Failed to create FIFO directory: {}", e)))?;
let input_fifo = fifo_dir.path().join("input");
let output_fifo = fifo_dir.path().join("output");
mkfifo(&input_fifo)?;
mkfifo(&output_fifo)?;
Ok(Self {
command: command.into(),
_fifo_dir: fifo_dir,
input_fifo,
output_fifo,
})
}
}
#[cfg(unix)]
impl std::fmt::Debug for FifoExecBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FifoExecBackend")
.field("command", &self.command)
.field("input_fifo", &self.input_fifo)
.field("output_fifo", &self.output_fifo)
.finish()
}
}
#[cfg(unix)]
impl FifoExecBackend {
fn spawn_helper(&self, decompressed_size: usize) -> Result<std::process::Child> {
let parts: Vec<&str> = self.command.split_whitespace().collect();
let (program, prefix_args) = parts
.split_first()
.ok_or_else(|| Error::Oodle("Empty exec command".into()))?;
Command::new(program)
.args(prefix_args)
.arg("decompress")
.arg(decompressed_size.to_string())
.arg(&self.input_fifo)
.arg(&self.output_fifo)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| Error::Oodle(format!("Failed to spawn command '{}': {}", self.command, e)))
}
fn check_exit(&self, child: &mut std::process::Child) -> Result<()> {
let status = child
.wait()
.map_err(|e| Error::Oodle(format!("Failed to wait for command: {}", e)))?;
if !status.success() {
let stderr_output = child
.stderr
.take()
.map(|mut s| {
let mut buf = String::new();
std::io::Read::read_to_string(&mut s, &mut buf).ok();
buf
})
.unwrap_or_default();
return Err(Error::Oodle(format!(
"Command '{}' failed with exit code {:?}: {}",
self.command,
status.code(),
stderr_output.trim()
)));
}
Ok(())
}
}
#[cfg(unix)]
impl OodleDecompressor for FifoExecBackend {
fn decompress_block(&self, compressed: &[u8], decompressed_size: usize) -> Result<Vec<u8>> {
let mut child = self.spawn_helper(decompressed_size)?;
let input_path = self.input_fifo.clone();
let compressed_data = compressed.to_vec();
let writer = std::thread::spawn(move || -> Result<()> {
let mut file = std::fs::File::create(&input_path)
.map_err(|e| Error::Oodle(format!("Failed to open input FIFO: {}", e)))?;
file.write_all(&compressed_data)
.map_err(|e| Error::Oodle(format!("Failed to write to input FIFO: {}", e)))?;
Ok(())
});
let output_path = self.output_fifo.clone();
let reader =
std::thread::spawn(move || -> std::io::Result<Vec<u8>> { std::fs::read(&output_path) });
let exit_result = self.check_exit(&mut child);
if exit_result.is_err() {
let _ = std::fs::OpenOptions::new()
.write(true)
.open(&self.output_fifo);
}
let output = reader
.join()
.map_err(|_| Error::Oodle("Reader thread panicked".into()))?
.map_err(|e| Error::Oodle(format!("Failed to read from output FIFO: {}", e)))?;
writer
.join()
.map_err(|_| Error::Oodle("Writer thread panicked".into()))??;
exit_result?;
if output.len() != decompressed_size {
return Err(Error::DecompressionSize {
expected: decompressed_size,
actual: output.len(),
});
}
Ok(output)
}
fn name(&self) -> &'static str {
"fifo-exec"
}
fn is_full_support(&self) -> bool {
true
}
}
#[cfg(unix)]
pub fn fifo_exec_backend<S: Into<String>>(command: S) -> Result<Box<dyn OodleDecompressor>> {
Ok(Box::new(FifoExecBackend::new(command)?))
}
#[cfg(unix)]
fn mkfifo(path: &std::path::Path) -> Result<()> {
let c_path = std::ffi::CString::new(
path.to_str()
.ok_or_else(|| Error::Oodle("non-UTF8 FIFO path".into()))?,
)
.map_err(|e| Error::Oodle(format!("invalid FIFO path: {}", e)))?;
let result = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) };
if result != 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_oozextract_backend_name() {
let backend = OozextractBackend::new();
assert_eq!(backend.name(), "oozextract");
assert!(!backend.is_full_support());
}
#[test]
fn test_default_backend() {
let backend = default_backend();
assert_eq!(backend.name(), "oozextract");
}
#[test]
fn test_exec_backend_empty_command() {
let backend = ExecBackend::new("");
let result = backend.decompress_block(&[0u8; 4], 4);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Empty exec command"), "got: {}", err);
}
#[test]
fn test_exec_backend_single_word_command() {
let backend = ExecBackend::new("nonexistent_oodle_helper");
let result = backend.decompress_block(&[0u8; 4], 4);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Failed to spawn"), "got: {}", err);
}
#[test]
fn test_exec_backend_multi_word_command() {
let backend = ExecBackend::new("nonexistent_wine nonexistent_helper.exe --dll foo.dll");
let result = backend.decompress_block(&[0u8; 4], 4);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Failed to spawn"), "got: {}", err);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn test_native_backend_not_available_on_non_windows() {
match native_backend("/path/to/oodle.dll") {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("--oodle-exec"),
"error should suggest --oodle-exec, got: {}",
msg
);
}
Ok(_) => panic!("native_backend should return Err on non-Windows"),
}
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_backend_creates_fifos() {
let backend = FifoExecBackend::new("nonexistent_helper").unwrap();
assert!(backend.input_fifo.exists());
assert!(backend.output_fifo.exists());
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_backend_name() {
let backend = FifoExecBackend::new("test_helper").unwrap();
assert_eq!(backend.name(), "fifo-exec");
assert!(backend.is_full_support());
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_backend_cleanup_on_drop() {
let (input_path, output_path);
{
let backend = FifoExecBackend::new("test_helper").unwrap();
input_path = backend.input_fifo.clone();
output_path = backend.output_fifo.clone();
assert!(input_path.exists());
assert!(output_path.exists());
}
assert!(!input_path.exists());
assert!(!output_path.exists());
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_backend_empty_command() {
let backend = FifoExecBackend::new("").unwrap();
let result = backend.decompress_block(&[0u8; 4], 4);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Empty exec command"), "got: {}", err);
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_backend_nonexistent_command() {
let backend = FifoExecBackend::new("nonexistent_fifo_helper").unwrap();
let result = backend.decompress_block(&[0u8; 4], 4);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Failed to spawn"), "got: {}", err);
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_backend_end_to_end() {
use std::os::unix::fs::PermissionsExt;
let script_dir = tempfile::tempdir().unwrap();
let script_path = script_dir.path().join("test_helper.sh");
std::fs::write(&script_path, "#!/bin/sh\ncat < \"$3\" > \"$4\"\n").unwrap();
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap();
let backend = FifoExecBackend::new(script_path.to_str().unwrap()).unwrap();
let input_data = b"hello world test data";
let result = backend.decompress_block(input_data, input_data.len());
match result {
Ok(output) => assert_eq!(output, input_data),
Err(e) => panic!("FIFO decompression failed: {}", e),
}
}
#[cfg(unix)]
#[test]
fn test_fifo_exec_factory() {
match fifo_exec_backend("test_helper") {
Ok(backend) => assert_eq!(backend.name(), "fifo-exec"),
Err(e) => panic!("fifo_exec_backend factory failed: {}", e),
}
}
}