mod casclib;
use self::casclib::{CascFindData, CascLib, DefaultCascLib, Handle};
use std::path::Path;
use std::ptr;
pub struct Archive<L: CascLib = DefaultCascLib> {
handle: Handle,
lib: L,
}
impl Archive<DefaultCascLib> {
#[allow(dead_code)]
pub fn open(path: &Path) -> Result<Self, String> {
Self::open_with_lib(path, DefaultCascLib)
}
}
impl<L: CascLib> Archive<L> {
pub fn open_with_lib<P: AsRef<Path>>(path: P, lib: L) -> Result<Self, String> {
let path_str = path
.as_ref()
.to_str()
.ok_or_else(|| "Invalid path encoding".to_string())?;
let c_path = std::ffi::CString::new(path_str).map_err(|e| e.to_string())?;
let mut handle: Handle = ptr::null_mut();
unsafe {
if lib.casc_open_storage(c_path.as_ptr(), 0, &mut handle) {
Ok(Archive { handle, lib })
} else {
Err(format!(
"Failed to open CASC storage at {:?}",
path.as_ref()
))
}
}
}
pub fn files(&self) -> ArchiveFileIterator<'_, L> {
ArchiveFileIterator::new(self.handle, &self.lib)
}
pub fn open_file(&self, name: &str) -> Result<ArchiveFile<'_, L>, String> {
let c_name = std::ffi::CString::new(name).map_err(|e| e.to_string())?;
let mut file_handle: Handle = ptr::null_mut();
unsafe {
if self.lib.casc_open_file(
self.handle,
c_name.as_ptr(),
0,
0,
&mut file_handle,
) {
Ok(ArchiveFile {
handle: file_handle,
lib: &self.lib,
})
} else {
Err(format!("Failed to open file '{}' in archive", name))
}
}
}
pub fn get_error(&self) -> u32 {
unsafe { self.lib.get_casc_error() }
}
}
impl<L: CascLib> Drop for Archive<L> {
fn drop(&mut self) {
unsafe {
if !self.handle.is_null() {
self.lib.casc_close_storage(self.handle);
}
}
}
}
#[cfg(test)]
pub mod mock {
use std::path::Path;
use std::sync::Mutex;
pub static TEST_MUTEX: Mutex<()> = Mutex::new(());
mockall::mock! {
pub ArchiveFile {}
impl std::io::Read for ArchiveFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
}
}
mockall::mock! {
pub Archive {
pub fn open(path: &Path) -> Result<Self, String>;
pub fn files<'a>(&'a self) -> Box<dyn Iterator<Item = String> + 'a>;
pub fn open_file(&self, name: &str) -> Result<MockArchiveFile, String>;
pub fn get_error(&self) -> u32;
}
}
}
pub struct ArchiveFile<'a, L: CascLib = DefaultCascLib> {
handle: Handle,
lib: &'a L,
}
impl<'a, L: CascLib> ArchiveFile<'a, L> {}
impl<'a, L: CascLib> std::io::Read for ArchiveFile<'a, L> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut bytes_read: u32 = 0;
unsafe {
if self.lib.casc_read_file(
self.handle,
buf.as_mut_ptr() as *mut std::ffi::c_void,
buf.len() as u32,
&mut bytes_read,
) {
Ok(bytes_read as usize)
} else {
Err(std::io::Error::other("Failed to read from CASC file"))
}
}
}
}
impl<'a, L: CascLib> Drop for ArchiveFile<'a, L> {
fn drop(&mut self) {
unsafe {
if !self.handle.is_null() {
self.lib.casc_close_file(self.handle);
}
}
}
}
pub struct ArchiveFileIterator<'a, L: CascLib = DefaultCascLib> {
find_handle: Handle,
find_data: CascFindData,
first: bool,
done: bool,
lib: &'a L,
}
impl<'a, L: CascLib> ArchiveFileIterator<'a, L> {
fn new(storage_handle: Handle, lib: &'a L) -> Self {
let mut find_data: CascFindData = unsafe { std::mem::zeroed() };
let mask = std::ffi::CString::new("*").unwrap();
let find_handle = unsafe {
lib.casc_find_first_file(
storage_handle,
mask.as_ptr(),
&mut find_data,
std::ptr::null(),
)
};
if find_handle.is_null() {
ArchiveFileIterator {
find_handle: std::ptr::null_mut(),
find_data,
first: false,
done: true,
lib,
}
} else {
ArchiveFileIterator {
find_handle,
find_data,
first: true,
done: false,
lib,
}
}
}
fn extract_name(&self) -> String {
unsafe {
std::ffi::CStr::from_ptr(self.find_data.szFileName.as_ptr())
.to_string_lossy()
.into_owned()
}
}
}
impl<'a, L: CascLib> Iterator for ArchiveFileIterator<'a, L> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
if self.first {
self.first = false;
return Some(self.extract_name());
}
unsafe {
if self
.lib
.casc_find_next_file(self.find_handle, &mut self.find_data)
{
Some(self.extract_name())
} else {
self.done = true;
None
}
}
}
}
impl<'a, L: CascLib> Drop for ArchiveFileIterator<'a, L> {
fn drop(&mut self) {
unsafe {
if !self.find_handle.is_null() {
self.lib.casc_find_close(self.find_handle);
}
}
}
}
#[cfg(test)]
mod tests {
use super::casclib::MockCascLib;
use crate::casc::Archive;
#[test]
fn test_open_non_existent_path() {
let mut lib = MockCascLib::new();
lib.expect_casc_open_storage().times(1).return_const(false);
let res = Archive::open_with_lib("/non/existent/path", lib);
match res {
Err(e) => assert!(e.contains("Failed to open CASC storage")),
Ok(_) => panic!("Should have failed"),
}
}
#[test]
fn test_iterate_empty_list() {
let mut lib = MockCascLib::new();
lib.mock_open();
lib.mock_file_list(vec![]);
lib.expect_casc_close_storage().times(1).return_const(true);
let archive = Archive::open_with_lib("/dummy/path", lib).unwrap();
let mut files = archive.files();
assert_eq!(files.next(), None);
}
#[test]
fn test_iterate_one_file() {
let mut lib = MockCascLib::new();
lib.mock_open();
lib.mock_file_list(vec!["file1.txt"]);
lib.expect_casc_close_storage().times(1).return_const(true);
lib.expect_casc_find_close().times(1).return_const(true);
let archive = Archive::open_with_lib("/dummy/path", lib).unwrap();
let mut files = archive.files();
assert_eq!(files.next(), Some("file1.txt".to_string()));
assert_eq!(files.next(), None);
}
#[test]
fn test_iterate_many_files() {
let mut lib = MockCascLib::new();
lib.mock_open();
lib.mock_file_list(vec!["file1.txt", "dir/file2.dat", "another.txt"]);
lib.expect_casc_close_storage().times(1).return_const(true);
lib.expect_casc_find_close().times(1).return_const(true);
let archive = Archive::open_with_lib("/dummy/path", lib).unwrap();
let files = archive.files();
let extracted: Vec<String> = files.collect();
assert_eq!(
extracted,
vec![
"file1.txt".to_string(),
"dir/file2.dat".to_string(),
"another.txt".to_string()
]
);
}
#[test]
fn test_read_file_success() {
use super::casclib::MockCascLib;
use std::io::Read;
let mut lib = MockCascLib::new();
lib.mock_open();
let content = b"Hello, CASC!".to_vec();
lib.mock_file_read(
"test.txt",
content.clone(),
100,
);
lib.expect_casc_close_storage().times(1).return_const(true);
let archive = Archive::open_with_lib("/dummy/path", lib).unwrap();
let mut file = archive.open_file("test.txt").unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(buf, content);
}
#[test]
fn test_read_file_chunks() {
use super::casclib::MockCascLib;
use std::io::Read;
let mut lib = MockCascLib::new();
lib.mock_open();
let content = vec![0u8; 100];
lib.mock_file_read(
"large.bin",
content.clone(),
101,
);
lib.expect_casc_close_storage().times(1).return_const(true);
let archive = Archive::open_with_lib("/dummy/path", lib).unwrap();
let mut file = archive.open_file("large.bin").unwrap();
let mut buf = [0u8; 30];
assert_eq!(file.read(&mut buf).unwrap(), 30);
assert_eq!(file.read(&mut buf).unwrap(), 30);
assert_eq!(file.read(&mut buf).unwrap(), 30);
assert_eq!(file.read(&mut buf).unwrap(), 10);
assert_eq!(file.read(&mut buf).unwrap(), 0);
}
#[test]
fn test_archive_get_error() {
use super::casclib::{Handle, MockCascLib};
let mut lib = MockCascLib::default();
lib.expect_get_casc_error().return_const(12345u32);
lib.expect_casc_close_storage()
.withf(|&h| h == 1 as Handle)
.times(1)
.return_const(true);
let archive = Archive {
handle: 1 as Handle,
lib,
};
assert_eq!(archive.get_error(), 12345);
}
}