use std::{
ffi::{CStr, CString},
mem,
os::raw::{c_char, c_int, c_longlong, c_uchar, c_void},
path::Path,
result,
};
use thiserror::Error;
unsafe extern "C" {
fn chm_open(filename: *const c_char) -> *mut ChmFile;
fn chm_close(file: *mut ChmFile);
fn chm_enumerate(file: *mut ChmFile, what: c_int, callback: ChmEnumerateCallback, context: *mut c_void) -> c_int;
fn chm_resolve_object(file: *mut ChmFile, path: *const c_char, ui: *mut ChmUnitInfo) -> c_int;
fn chm_retrieve_object(
file: *mut ChmFile,
ui: *const ChmUnitInfo,
buf: *mut c_uchar,
addr: c_longlong,
len: c_longlong,
) -> c_longlong;
}
#[repr(C)]
pub struct ChmFile {
_private: [u8; 0],
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct ChmUnitInfo {
pub start: c_longlong,
pub length: c_longlong,
pub space: c_int,
pub flags: c_int,
pub path: [c_char; 512],
}
pub type ChmEnumerateCallback = extern "C" fn(*mut ChmFile, *mut ChmUnitInfo, *mut c_void) -> c_int;
pub const CHM_ENUMERATE_ALL: c_int = 3;
pub const CHM_ENUMERATOR_CONTINUE: c_int = 1;
pub const CHM_ENUMERATOR_SUCCESS: c_int = 0;
pub const CHM_RESOLVE_SUCCESS: c_int = 0;
#[derive(Debug, Error)]
pub enum ChmError {
#[error("Invalid path for CHM file: {0}")]
InvalidPath(String),
#[error("Failed to open CHM file: {0}")]
OpenFailed(String),
#[error("CHM enumeration failed")]
EnumerateFailed,
#[error("Failed to resolve CHM object: {0}")]
ResolveFailed(String),
#[error("Failed to read complete CHM file (expected {expected} bytes, got {actual})")]
ShortRead { expected: i64, actual: i64 },
#[error("CHM object length overflows usize: {0}")]
LengthOverflow(i64),
}
pub type Result<T> = result::Result<T, ChmError>;
#[derive(Debug)]
pub struct ChmHandle {
handle: *mut ChmFile,
}
impl ChmHandle {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_str = path.as_ref().to_string_lossy().to_string();
let c_path = CString::new(path_str.as_str()).map_err(|_| ChmError::InvalidPath(path_str.clone()))?;
unsafe {
let handle = chm_open(c_path.as_ptr());
if handle.is_null() {
return Err(ChmError::OpenFailed(path_str));
}
Ok(Self { handle })
}
}
pub fn enumerate<F>(&mut self, what: c_int, mut callback: F) -> Result<()>
where
F: FnMut(&ChmUnitInfo) -> bool,
{
extern "C" fn trampoline<F>(_file: *mut ChmFile, ui: *mut ChmUnitInfo, context: *mut c_void) -> c_int
where
F: FnMut(&ChmUnitInfo) -> bool,
{
unsafe {
let cb: &mut F = &mut *context.cast::<F>();
if cb(&*ui) { CHM_ENUMERATOR_CONTINUE } else { CHM_ENUMERATOR_SUCCESS }
}
}
unsafe {
let context = (&raw mut callback).cast::<c_void>();
let result = chm_enumerate(self.handle, what, trampoline::<F>, context);
if result != 0 { Ok(()) } else { Err(ChmError::EnumerateFailed) }
}
}
pub fn read_file(&mut self, path: &str) -> Result<Vec<u8>> {
let c_path = CString::new(path).map_err(|_| ChmError::InvalidPath(path.to_string()))?;
unsafe {
let mut ui: ChmUnitInfo = mem::zeroed();
if chm_resolve_object(self.handle, c_path.as_ptr(), &raw mut ui) != CHM_RESOLVE_SUCCESS {
return Err(ChmError::ResolveFailed(path.to_string()));
}
if ui.length == 0 {
return Ok(Vec::new());
}
let len = usize::try_from(ui.length).map_err(|_| ChmError::LengthOverflow(ui.length))?;
let mut buffer = vec![0u8; len];
let bytes_read = chm_retrieve_object(self.handle, &raw const ui, buffer.as_mut_ptr(), 0, ui.length);
if bytes_read != ui.length {
return Err(ChmError::ShortRead { expected: ui.length, actual: bytes_read });
}
Ok(buffer)
}
}
}
impl Drop for ChmHandle {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe {
chm_close(self.handle);
}
}
}
}
unsafe impl Send for ChmHandle {}
unsafe impl Sync for ChmHandle {}
#[must_use]
pub fn unit_info_path(ui: &ChmUnitInfo) -> String {
unsafe { CStr::from_ptr(ui.path.as_ptr()).to_string_lossy().into_owned() }
}