#[cfg(not(test))]
use crate::casc::Archive;
#[cfg(test)]
use crate::casc::mock::MockArchive as Archive;
use crate::targets::TargetMatcher;
use anyhow::{Result, anyhow};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
use std::sync::atomic::Ordering;
pub fn execute(
archive_dir: &Path,
targets: &[String],
output_dir: &Path,
flatten: bool,
) -> Result<i32> {
let archive = Archive::open(archive_dir).map_err(|e| anyhow!(e))?;
execute_internal(
&archive,
targets,
output_dir,
&mut io::stdout(),
&mut io::stderr(),
flatten,
)
}
fn execute_internal<W1: io::Write, W2: io::Write>(
archive: &Archive,
targets: &[String],
output_dir: &Path,
stdout: &mut W1,
stderr: &mut W2,
flatten: bool,
) -> Result<i32> {
let matcher = TargetMatcher::new(targets).map_err(|e| anyhow!(e))?;
let mut extractor = Extractor::new(archive, output_dir, flatten);
for path in archive.files() {
if crate::CANCELLED.load(Ordering::Relaxed) {
return Err(anyhow!(crate::AppError::Cancelled(
"Extraction"
)));
}
if matcher.is_match(&path) {
writeln!(stdout, "Extracting {}", path)?;
match extractor.extract_path(&path, stderr) {
Ok(_) => {}
Err(e) => {
if let Some(app_err) = e.downcast_ref::<crate::AppError>()
&& matches!(app_err, crate::AppError::Cancelled(_))
{
return Err(e);
}
writeln!(stderr, "{}", e)?;
}
}
}
}
if extractor.extracted_count == 0 && extractor.skipped_count == 0 && extractor.failed_count == 0
{
writeln!(stdout, "No matches.")?;
} else {
write!(stdout, "Extracted {} files", extractor.extracted_count)?;
if extractor.skipped_count > 0 || extractor.failed_count > 0 {
write!(stdout, " (")?;
if extractor.skipped_count > 0 {
write!(stdout, "{} skipped", extractor.skipped_count)?;
if extractor.failed_count > 0 {
write!(stdout, ", ")?;
}
}
if extractor.failed_count > 0 {
write!(stdout, "{} failed", extractor.failed_count)?;
}
write!(stdout, ")")?;
}
writeln!(stdout, ".")?;
}
if extractor.failed_count > 0 {
Ok(crate::exit_codes::ERROR)
} else if extractor.skipped_count > 0 {
Ok(crate::exit_codes::WARNING)
} else if extractor.extracted_count == 0 && !targets.is_empty() {
Ok(crate::exit_codes::NO_MATCHES)
} else {
Ok(crate::exit_codes::SUCCESS)
}
}
struct Extractor<'a> {
archive: &'a Archive,
output_dir: &'a Path,
flatten: bool,
extracted_count: usize,
skipped_count: usize,
failed_count: usize,
extracted_filenames: HashSet<String>,
buffer: [u8; 64 * 1024],
}
impl<'a> Extractor<'a> {
fn new(archive: &'a Archive, output_dir: &'a Path, flatten: bool) -> Self {
Self {
archive,
output_dir,
flatten,
extracted_count: 0,
skipped_count: 0,
failed_count: 0,
extracted_filenames: HashSet::new(),
buffer: [0u8; 64 * 1024],
}
}
fn extract_path<W: io::Write>(&mut self, path: &str, stderr: &mut W) -> Result<()> {
let local_path_str = if let Some(colon_idx) = path.find(':') {
&path[colon_idx + 1..]
} else {
path
};
let local_path_normalized = local_path_str.replace('\\', "/");
let local_path_relative = Path::new(&local_path_normalized);
let local_path = if self.flatten {
let filename = match local_path_relative.file_name().and_then(|f| f.to_str()) {
Some(f) => f,
None => {
writeln!(
stderr,
"ERROR: Failed to extract filename from path: {}",
path
)?;
self.failed_count += 1;
return Ok(());
}
};
if self.extracted_filenames.contains(filename) {
writeln!(stderr, "WARN: Skipped '{}' (Conflict/Exists)", path)?;
self.skipped_count += 1;
return Ok(());
}
self.extracted_filenames.insert(filename.to_string());
self.output_dir.join(filename)
} else {
self.output_dir.join(local_path_relative)
};
if let Some(parent) = local_path.parent()
&& let Err(e) = fs::create_dir_all(parent)
{
writeln!(
stderr,
"ERROR: Failed to create directory '{}': {}",
parent.display(),
e
)?;
self.failed_count += 1;
return Ok(());
}
match self.extract_file(path, &local_path) {
Ok(_) => {
self.extracted_count += 1;
Ok(())
}
Err(e) => {
if let Some(app_err) = e.downcast_ref::<crate::AppError>()
&& matches!(app_err, crate::AppError::Cancelled(_))
{
return Err(e);
}
self.failed_count += 1;
let _ = fs::remove_file(&local_path);
Err(e)
}
}
}
fn extract_file(&mut self, path: &str, local_path: &Path) -> Result<()> {
let mut archive_file = self.archive.open_file(path).map_err(|e| {
let code = self.archive.get_error();
anyhow!(
"Extraction failure (code: {}) for '{}' (open): {}",
code,
path,
e
)
})?;
let mut out_file = fs::File::create(local_path)
.map_err(|e| anyhow!("Extraction failure for '{}' (create): {}", path, e))?;
loop {
if crate::CANCELLED.load(Ordering::Relaxed) {
drop(out_file);
let _ = fs::remove_file(local_path);
return Err(anyhow!(crate::AppError::Cancelled(
"Extraction"
)));
}
let bytes_read = archive_file.read(&mut self.buffer).map_err(|e| {
let code = self.archive.get_error();
anyhow!(
"Extraction failure (code: {}) for '{}' (read): {}",
code,
path,
e
)
})?;
if bytes_read == 0 {
break; }
out_file
.write_all(&self.buffer[..bytes_read])
.map_err(|e| anyhow!("Extraction failure for '{}' (write): {}", path, e))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::casc::mock::{MockArchiveFile, TEST_MUTEX};
use crate::tests::CANCEL_MUTEX;
use mockall::predicate::eq;
use std::fs;
use std::path::Path;
use std::sync::Mutex;
fn mock_file(data: Vec<u8>) -> MockArchiveFile {
let mut f = MockArchiveFile::default();
let content = Mutex::new(Some(data));
f.expect_read().returning(move |buf| {
let mut lock = content.lock().unwrap();
if let Some(data) = lock.take() {
let len = std::cmp::min(data.len(), buf.len());
buf[..len].copy_from_slice(&data[..len]);
Ok(len)
} else {
Ok(0)
}
});
f
}
#[test]
fn test_execute_internal_happy_path() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["test.txt".to_string()].into_iter()));
archive
.expect_open_file()
.with(eq("test.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"hello".to_vec())));
let temp_dir = Path::new("test_extract_happy");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
let extracted_file = temp_dir.join("test.txt");
assert!(extracted_file.exists());
assert_eq!(fs::read_to_string(extracted_file).unwrap(), "hello");
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracting test.txt"));
assert!(output_str.contains("Extracted 1 files."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_with_prefix() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["data:folder/file.dat".to_string()].into_iter()));
archive
.expect_open_file()
.with(eq("data:folder/file.dat"))
.times(1)
.returning(|_| Ok(mock_file(vec![1, 2, 3])));
let temp_dir = Path::new("test_extract_prefix");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
let extracted_file = temp_dir.join("folder/file.dat");
assert!(extracted_file.exists());
assert_eq!(fs::read(extracted_file).unwrap(), vec![1, 2, 3]);
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracting data:folder/file.dat"));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_no_match() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["other.txt".to_string()].into_iter()));
let temp_dir = Path::new("test_extract_no_match");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&["matching.txt".to_string()],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::NO_MATCHES);
assert!(!temp_dir.join("other.txt").exists());
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("No matches."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_multiple_matches() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive.expect_files().times(1).returning(|| {
Box::new(
vec![
"a.txt".to_string(),
"b.txt".to_string(),
"c.dat".to_string(),
]
.into_iter(),
)
});
archive
.expect_open_file()
.with(eq("a.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"a".to_vec())));
archive
.expect_open_file()
.with(eq("b.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"b".to_vec())));
let temp_dir = Path::new("test_extract_multiple");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&["*.txt".to_string()],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
assert_eq!(fs::read_to_string(temp_dir.join("a.txt")).unwrap(), "a");
assert_eq!(fs::read_to_string(temp_dir.join("b.txt")).unwrap(), "b");
assert!(!temp_dir.join("c.dat").exists());
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracting a.txt"));
assert!(output_str.contains("Extracting b.txt"));
assert!(output_str.contains("Extracted 2 files."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_backslash_path() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["data\\sub\\file.txt".to_string()].into_iter()));
archive
.expect_open_file()
.with(eq("data\\sub\\file.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"content".to_vec())));
let temp_dir = Path::new("test_extract_backslash");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
let extracted_file = temp_dir.join("data/sub/file.txt");
assert!(extracted_file.exists());
assert_eq!(fs::read_to_string(extracted_file).unwrap(), "content");
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracting data\\sub\\file.txt"));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_open_failure() {
let _cancel_lock = CANCEL_MUTEX.lock().unwrap();
let _test_lock = TEST_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let path = Path::new("/dummy/path");
let ctx = Archive::open_context();
ctx.expect()
.with(eq(path))
.times(1)
.returning(|_| Err("Mock open failure".to_string()));
let res = execute(
path,
&[],
Path::new("."),
false,
);
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "Mock open failure");
}
#[test]
fn test_execute_internal_invalid_output_dir() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.returning(|| Box::new(vec!["test.txt".to_string()].into_iter()));
let temp_file = Path::new("test_extract_invalid_dir");
fs::write(temp_file, "not a directory").unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_file,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::ERROR);
let stderr_str = String::from_utf8(stderr).unwrap();
assert!(
stderr_str.contains("ERROR: Failed to create directory")
|| stderr_str.contains("Not a directory")
);
fs::remove_file(temp_file).unwrap();
}
#[test]
fn test_execute_internal_empty_output_dir() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.returning(|| Box::new(vec!["test.txt".to_string()].into_iter()));
archive
.expect_open_file()
.returning(|_| Ok(mock_file(b"hello".to_vec())));
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
Path::new(""),
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
assert!(Path::new("test.txt").exists());
fs::remove_file("test.txt").unwrap();
}
#[test]
fn test_execute_internal_extraction_failure_with_code() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["fail.txt".to_string()].into_iter()));
archive
.expect_open_file()
.with(eq("fail.txt"))
.times(1)
.returning(|_| Err("Mock open failure".to_string()));
archive.expect_get_error().times(1).returning(|| 12345);
let temp_dir = Path::new("test_extract_fail_code");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::ERROR);
let stderr_str = String::from_utf8(stderr).unwrap();
assert!(stderr_str.contains("Extraction failure (code: 12345) for 'fail.txt' (open)"));
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracted 0 files (1 failed)."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_mixed_run() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive.expect_files().times(1).returning(|| {
Box::new(
vec![
"a_success.txt".to_string(),
"b_skip/file.txt".to_string(),
"c_fail.txt".to_string(),
"d_collision/file.txt".to_string(),
]
.into_iter(),
)
});
archive
.expect_open_file()
.with(eq("a_success.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"ok".to_vec())));
archive
.expect_open_file()
.with(eq("b_skip/file.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"first".to_vec())));
archive
.expect_open_file()
.with(eq("d_collision/file.txt"))
.times(0);
archive
.expect_open_file()
.with(eq("c_fail.txt"))
.times(1)
.returning(|_| Err("Open error".to_string()));
archive.expect_get_error().returning(|| 555);
let temp_dir = Path::new("test_extract_mixed");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
true,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::ERROR);
let output_str = String::from_utf8(stdout).unwrap();
let error_str = String::from_utf8(stderr).unwrap();
assert!(output_str.contains("Extracting a_success.txt"));
assert!(output_str.contains("Extracting b_skip/file.txt"));
assert!(output_str.contains("Extracting c_fail.txt"));
assert!(output_str.contains("Extracting d_collision/file.txt"));
assert!(output_str.contains("Extracted 2 files (1 skipped, 1 failed)."));
assert!(error_str.contains("WARN: Skipped 'd_collision/file.txt' (Conflict/Exists)"));
assert!(error_str.contains("Extraction failure (code: 555) for 'c_fail.txt' (open)"));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_read_failure_mid_file() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["bad_read.bin".to_string()].into_iter()));
let mut mock_file = MockArchiveFile::default();
mock_file
.expect_read()
.times(1)
.returning(|_| Err(std::io::Error::other("Read error")));
let mock_file_opt = Mutex::new(Some(mock_file));
archive
.expect_open_file()
.with(eq("bad_read.bin"))
.times(1)
.returning(move |_| {
Ok(mock_file_opt
.lock()
.unwrap()
.take()
.expect("Called open_file twice"))
});
archive.expect_get_error().returning(|| 999);
let temp_dir = Path::new("test_extract_read_fail");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::ERROR);
let error_str = String::from_utf8(stderr).unwrap();
assert!(error_str.contains("Extraction failure (code: 999) for 'bad_read.bin' (read)"));
assert!(!temp_dir.join("bad_read.bin").exists());
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_write_failure() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["fail_write.txt".to_string()].into_iter()));
archive
.expect_open_file()
.returning(|_| Ok(mock_file(b"some data".to_vec())));
let temp_dir = Path::new("test_extract_write_fail");
fs::create_dir_all(temp_dir).unwrap();
let target_file = temp_dir.join("fail_write.txt");
fs::create_dir(&target_file).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::ERROR);
let error_str = String::from_utf8(stderr).unwrap();
assert!(error_str.contains("Extraction failure for 'fail_write.txt' (create)"));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_filename_failure() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["..".to_string()].into_iter()));
let temp_dir = Path::new("test_extract_filename_fail");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
true,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::ERROR);
let error_str = String::from_utf8(stderr).unwrap();
assert!(error_str.contains("ERROR: Failed to extract filename from path: .."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_skip_only_no_match() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive.expect_files().times(1).returning(|| {
Box::new(vec!["a/file.txt".to_string(), "b/file.txt".to_string()].into_iter())
});
archive
.expect_open_file()
.returning(|_| Ok(mock_file(b"data".to_vec())));
let temp_dir = Path::new("test_extract_skip_only");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&["*.txt".to_string()],
temp_dir,
&mut stdout,
&mut stderr,
true,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::WARNING);
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracted 1 files (1 skipped)."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_cancelled_before() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(true, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["test.txt".to_string()].into_iter()));
let temp_dir = Path::new("test_extract_cancel_before");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_err());
let err = res.unwrap_err();
if let Some(app_err) = err.downcast_ref::<crate::AppError>() {
match app_err {
crate::AppError::Cancelled(op) => assert_eq!(*op, "Extraction"),
}
} else {
panic!("Expected AppError::Cancelled, got: {:?}", err);
}
assert!(stdout.is_empty());
assert!(!temp_dir.join("test.txt").exists());
crate::CANCELLED.store(false, Ordering::SeqCst);
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_cancelled_mid_file() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive
.expect_files()
.times(1)
.returning(|| Box::new(vec!["bigfile.bin".to_string()].into_iter()));
let mut mock_file = MockArchiveFile::default();
mock_file.expect_read().times(1).returning(|buf| {
let data = vec![0u8; 1024];
buf[..1024].copy_from_slice(&data);
crate::CANCELLED.store(true, Ordering::SeqCst);
Ok(1024)
});
let mock_file_opt = Mutex::new(Some(mock_file));
archive
.expect_open_file()
.with(eq("bigfile.bin"))
.times(1)
.returning(move |_| {
Ok(mock_file_opt
.lock()
.unwrap()
.take()
.expect("Called open_file twice"))
});
let temp_dir = Path::new("test_extract_cancel_mid");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_err());
let err = res.unwrap_err();
if let Some(app_err) = err.downcast_ref::<crate::AppError>() {
match app_err {
crate::AppError::Cancelled(op) => assert_eq!(*op, "Extraction"),
}
} else {
panic!("Expected AppError::Cancelled, got: {:?}", err);
}
assert!(!temp_dir.join("bigfile.bin").exists());
crate::CANCELLED.store(false, Ordering::SeqCst);
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_flatten_no_collision() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive.expect_files().times(1).returning(|| {
Box::new(
vec![
"a/test.txt".to_string(),
"b/other.txt".to_string(),
"c/d/deep.txt".to_string(),
]
.into_iter(),
)
});
archive
.expect_open_file()
.with(eq("a/test.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"a".to_vec())));
archive
.expect_open_file()
.with(eq("b/other.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"b".to_vec())));
archive
.expect_open_file()
.with(eq("c/d/deep.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"deep".to_vec())));
let temp_dir = Path::new("test_extract_flatten_no_collision");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
true,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
assert_eq!(fs::read_to_string(temp_dir.join("test.txt")).unwrap(), "a");
assert_eq!(fs::read_to_string(temp_dir.join("other.txt")).unwrap(), "b");
assert_eq!(
fs::read_to_string(temp_dir.join("deep.txt")).unwrap(),
"deep"
);
assert!(!temp_dir.join("a").exists());
assert!(!temp_dir.join("b").exists());
assert!(!temp_dir.join("c").exists());
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_flatten_collision() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive.expect_files().times(1).returning(|| {
Box::new(vec!["a/file.txt".to_string(), "b/file.txt".to_string()].into_iter())
});
archive
.expect_open_file()
.with(eq("a/file.txt"))
.times(1)
.returning(|_| Ok(mock_file(b"first".to_vec())));
archive.expect_open_file().with(eq("b/file.txt")).times(0);
let temp_dir = Path::new("test_extract_flatten_collision");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
true,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::WARNING);
assert_eq!(
fs::read_to_string(temp_dir.join("file.txt")).unwrap(),
"first"
);
let stderr_str = String::from_utf8(stderr).unwrap();
assert!(stderr_str.contains("WARN: Skipped 'b/file.txt' (Conflict/Exists)"));
let output_str = String::from_utf8(stdout).unwrap();
assert!(output_str.contains("Extracting a/file.txt"));
assert!(output_str.contains("Extracted 1 files (1 skipped)."));
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_execute_internal_complex_structure() {
let _lock = CANCEL_MUTEX.lock().unwrap();
crate::CANCELLED.store(false, Ordering::SeqCst);
let mut archive = Archive::default();
archive.expect_files().times(1).returning(|| {
Box::new(
vec![
"a/file.txt".to_string(),
"b/file.txt".to_string(),
"c/d/file.txt".to_string(),
"e/d/file.txt".to_string(),
]
.into_iter(),
)
});
archive
.expect_open_file()
.with(eq("a/file.txt"))
.returning(|_| Ok(mock_file(b"a".to_vec())));
archive
.expect_open_file()
.with(eq("b/file.txt"))
.returning(|_| Ok(mock_file(b"b".to_vec())));
archive
.expect_open_file()
.with(eq("c/d/file.txt"))
.returning(|_| Ok(mock_file(b"cd".to_vec())));
archive
.expect_open_file()
.with(eq("e/d/file.txt"))
.returning(|_| Ok(mock_file(b"ed".to_vec())));
let temp_dir = Path::new("test_extract_complex");
fs::create_dir_all(temp_dir).unwrap();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let res = execute_internal(
&archive,
&[],
temp_dir,
&mut stdout,
&mut stderr,
false,
);
assert!(res.is_ok());
assert_eq!(res.unwrap(), crate::exit_codes::SUCCESS);
assert_eq!(
fs::read_to_string(temp_dir.join("a/file.txt")).unwrap(),
"a"
);
assert_eq!(
fs::read_to_string(temp_dir.join("b/file.txt")).unwrap(),
"b"
);
assert_eq!(
fs::read_to_string(temp_dir.join("c/d/file.txt")).unwrap(),
"cd"
);
assert_eq!(
fs::read_to_string(temp_dir.join("e/d/file.txt")).unwrap(),
"ed"
);
fs::remove_dir_all(temp_dir).unwrap();
}
}