use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::read::Entry;
use crate::{Error, Result};
pub trait ExtractDestination: Send {
fn create_writer(&mut self, entry: &Entry) -> Result<Box<dyn Write + Send>>;
fn on_complete(&mut self, entry: &Entry, success: bool) -> Result<()>;
fn on_directory(&mut self, entry: &Entry) -> Result<()> {
let _ = entry;
Ok(())
}
fn on_start(&mut self, total_entries: usize) -> Result<()> {
let _ = total_entries;
Ok(())
}
fn on_finish(&mut self, success: bool) -> Result<()> {
let _ = success;
Ok(())
}
}
pub struct FilesystemDestination {
output_dir: PathBuf,
preserve_permissions: bool,
current_path: Option<PathBuf>,
current_entry_index: usize,
}
impl FilesystemDestination {
pub fn new(output_dir: impl AsRef<Path>) -> Self {
Self {
output_dir: output_dir.as_ref().to_path_buf(),
preserve_permissions: true,
current_path: None,
current_entry_index: 0,
}
}
pub fn preserve_permissions(mut self, preserve: bool) -> Self {
self.preserve_permissions = preserve;
self
}
fn resolve_path(&self, entry: &Entry) -> Result<PathBuf> {
let entry_path = Path::new(entry.path.as_str());
let resolved = self.output_dir.join(entry_path);
let canonical_output = self
.output_dir
.canonicalize()
.unwrap_or_else(|_| self.output_dir.clone());
let mut check_path = resolved.clone();
while !check_path.exists() {
if let Some(parent) = check_path.parent() {
check_path = parent.to_path_buf();
} else {
break;
}
}
if check_path.exists() {
let canonical_resolved = check_path.canonicalize().map_err(Error::Io)?;
if !canonical_resolved.starts_with(&canonical_output) {
return Err(Error::PathTraversal {
entry_index: self.current_entry_index,
path: entry.path.as_str().to_string(),
});
}
}
Ok(resolved)
}
}
impl ExtractDestination for FilesystemDestination {
fn create_writer(&mut self, entry: &Entry) -> Result<Box<dyn Write + Send>> {
self.current_entry_index = entry.index;
let path = self.resolve_path(entry)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(Error::Io)?;
}
let file = File::create(&path).map_err(Error::Io)?;
self.current_path = Some(path);
Ok(Box::new(file))
}
fn on_complete(&mut self, entry: &Entry, success: bool) -> Result<()> {
if !success {
if let Some(path) = self.current_path.take() {
if let Err(e) = fs::remove_file(&path) {
log::warn!(
"Failed to clean up partial file '{}': {}",
path.display(),
e
);
}
}
return Ok(());
}
#[cfg(unix)]
{
let path = match self.current_path.take() {
Some(p) => p,
None => return Ok(()),
};
if self.preserve_permissions {
if let Some(mode) = entry.unix_mode() {
use std::os::unix::fs::PermissionsExt;
if let Err(e) = fs::set_permissions(&path, fs::Permissions::from_mode(mode)) {
log::warn!("Failed to set permissions on '{}': {}", path.display(), e);
}
}
}
}
#[cfg(not(unix))]
{
self.current_path.take();
let _ = entry;
}
Ok(())
}
fn on_directory(&mut self, entry: &Entry) -> Result<()> {
self.current_entry_index = entry.index;
let path = self.resolve_path(entry)?;
fs::create_dir_all(&path).map_err(Error::Io)?;
Ok(())
}
}
pub struct MemoryDestination {
files: HashMap<String, Vec<u8>>,
current_path: Option<String>,
current_buffer: Option<Arc<Mutex<Vec<u8>>>>,
}
impl MemoryDestination {
pub fn new() -> Self {
Self {
files: HashMap::new(),
current_path: None,
current_buffer: None,
}
}
pub fn files(&self) -> &HashMap<String, Vec<u8>> {
&self.files
}
pub fn into_files(self) -> HashMap<String, Vec<u8>> {
self.files
}
pub fn get(&self, path: &str) -> Option<&[u8]> {
self.files.get(path).map(|v| v.as_slice())
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
impl Default for MemoryDestination {
fn default() -> Self {
Self::new()
}
}
struct SharedBufferWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl SharedBufferWriter {
fn new(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
Self { buffer }
}
}
impl Write for SharedBufferWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut guard = self
.buffer
.lock()
.map_err(|_| io::Error::other("mutex poisoned"))?;
guard.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl ExtractDestination for MemoryDestination {
fn create_writer(&mut self, entry: &Entry) -> Result<Box<dyn Write + Send>> {
self.current_path = Some(entry.path.as_str().to_string());
let buffer = Arc::new(Mutex::new(Vec::with_capacity(entry.size as usize)));
self.current_buffer = Some(Arc::clone(&buffer));
Ok(Box::new(SharedBufferWriter::new(buffer)))
}
fn on_complete(&mut self, _entry: &Entry, success: bool) -> Result<()> {
let path = self.current_path.take();
let buffer = self.current_buffer.take();
if success {
if let (Some(path), Some(buffer)) = (path, buffer) {
let data = Arc::try_unwrap(buffer)
.map(|mutex| mutex.into_inner().unwrap_or_default())
.unwrap_or_else(|arc| {
arc.lock().map(|guard| guard.clone()).unwrap_or_default()
});
self.files.insert(path, data);
}
}
Ok(())
}
}
pub struct NullDestination {
entries_processed: usize,
bytes_discarded: u64,
}
impl NullDestination {
pub fn new() -> Self {
Self {
entries_processed: 0,
bytes_discarded: 0,
}
}
pub fn entries_processed(&self) -> usize {
self.entries_processed
}
pub fn bytes_discarded(&self) -> u64 {
self.bytes_discarded
}
}
impl Default for NullDestination {
fn default() -> Self {
Self::new()
}
}
struct NullWriter {
bytes_written: u64,
}
impl NullWriter {
fn new() -> Self {
Self { bytes_written: 0 }
}
}
impl Write for NullWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.bytes_written += buf.len() as u64;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl ExtractDestination for NullDestination {
fn create_writer(&mut self, _entry: &Entry) -> Result<Box<dyn Write + Send>> {
Ok(Box::new(NullWriter::new()))
}
fn on_complete(&mut self, entry: &Entry, success: bool) -> Result<()> {
if success {
self.entries_processed += 1;
self.bytes_discarded += entry.size;
}
Ok(())
}
fn on_directory(&mut self, _entry: &Entry) -> Result<()> {
self.entries_processed += 1;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ArchivePath;
fn make_entry(path: &str, is_dir: bool, size: u64) -> Entry {
Entry {
path: ArchivePath::new(path).unwrap(),
is_directory: is_dir,
size,
crc32: None,
crc64: None,
modification_time: None,
creation_time: None,
access_time: None,
attributes: None,
is_encrypted: false,
is_symlink: false,
is_anti: false,
ownership: None,
index: 0,
folder_index: None,
stream_index: None,
}
}
#[test]
fn test_null_destination() {
let mut dest = NullDestination::new();
let entry = make_entry("test.txt", false, 100);
let mut writer = dest.create_writer(&entry).unwrap();
writer.write_all(b"test data").unwrap();
dest.on_complete(&entry, true).unwrap();
assert_eq!(dest.entries_processed(), 1);
assert_eq!(dest.bytes_discarded(), 100);
}
#[test]
fn test_memory_destination() {
let dest = MemoryDestination::new();
assert!(dest.is_empty());
assert_eq!(dest.len(), 0);
}
#[test]
fn test_filesystem_destination_creation() {
let dest = FilesystemDestination::new("/tmp/output");
assert_eq!(dest.output_dir, PathBuf::from("/tmp/output"));
}
}