use std::{
ffi::OsString,
fs::{self, File},
io::{Read, Seek, Write},
path::{Path, PathBuf},
time::Instant,
};
use zarust::{ArchiveReader, EntryKind, NodeHandle, ROOT_NODE};
use crate::cli::{
Failure, Outcome,
args::Options,
path,
ui::{Reporter, format_duration},
walk::Summary,
};
const WRITE_BUFFER: usize = 64 * 1024;
pub fn run(
reporter: &mut Reporter,
archive: &Path,
output: Option<PathBuf>,
options: &Options,
) -> Outcome {
let output = output.unwrap_or_else(|| default_output(archive));
if output.exists() && !output.is_dir() {
return Err(Failure::new(format!(
"`{}` exists and is not a directory",
path::display(&output)
)));
}
if !options.force
&& output
.read_dir()
.is_ok_and(|mut listing| listing.next().is_some())
{
return Err(Failure::new(format!(
"`{}` is not empty (use --force to extract into it)",
path::display(&output)
)));
}
let started = Instant::now();
let mut reader = super::open_archive(archive)?;
if options.check {
verify_first(reporter, &mut reader, archive)?;
}
let summary = Summary::of(&reader, ROOT_NODE)?;
fs::create_dir_all(&output).map_err(|error| Failure::path("create", &output, error))?;
reporter.start_bar("extracting", summary.bytes);
let mut buffer = vec![0_u8; WRITE_BUFFER];
let mut path_buffer = output.clone();
let result = extract_directory(
reporter,
&mut reader,
ROOT_NODE,
&mut path_buffer,
&mut buffer,
);
reporter.finish_bar();
result?;
reporter.success(
"extracted",
&output,
&format!(
"{} in {}",
summary.describe(),
format_duration(Some(started.elapsed()))
),
)
}
fn verify_first<R: Read + Seek>(
reporter: &mut Reporter,
reader: &mut ArchiveReader<R>,
archive: &Path,
) -> Outcome {
reporter.start_bar("checking", reader.archive_size());
let intact = reader.verify_integrity_with(|read| reporter.advance(read));
reporter.finish_bar();
if intact.map_err(|error| Failure::path("read", archive, error))? {
return Ok(());
}
Err(Failure::new(format!(
"`{}` failed its integrity check; refusing to extract",
path::display(archive)
)))
}
fn default_output(archive: &Path) -> PathBuf {
let mut name = archive.file_stem().unwrap_or_default().to_os_string();
if name.is_empty() {
name = OsString::from("archive");
}
name.push("_extracted");
path::sibling(archive, name)
}
fn extract_directory<R: Read + Seek>(
reporter: &mut Reporter,
reader: &mut ArchiveReader<R>,
directory: NodeHandle,
output: &mut PathBuf,
buffer: &mut [u8],
) -> Result<(), Failure> {
let children = reader
.directory_entries(directory)?
.into_iter()
.map(|entry| Ok((path::from_archive(entry.name)?, entry.kind, entry.handle)))
.collect::<Result<Vec<_>, Failure>>()?;
for (name, kind, handle) in children {
output.push(&name);
let result = match kind {
EntryKind::Directory => fs::create_dir_all(&output)
.map_err(|error| Failure::path("create", output, error))
.and_then(|()| extract_directory(reporter, reader, handle, output, buffer)),
EntryKind::File => {
if reporter.is_verbose() {
reporter.detail(format_args!("{}", path::display(output)))?;
}
extract_file(reporter, reader, handle, output, buffer)
}
};
output.pop();
result?;
}
Ok(())
}
fn extract_file<R: Read + Seek>(
reporter: &mut Reporter,
reader: &mut ArchiveReader<R>,
handle: NodeHandle,
path: &Path,
buffer: &mut [u8],
) -> Result<(), Failure> {
let expected = reader.file_size(handle)?;
let mut file = File::create(path).map_err(|error| Failure::path("create", path, error))?;
let mut written = 0_u64;
loop {
let read = reader
.read_file(handle, written, buffer)
.map_err(|error| Failure::path("read", path, error))?;
if read == 0 {
break;
}
file.write_all(&buffer[..read])
.map_err(|error| Failure::path("write", path, error))?;
written += read as u64;
reporter.advance(read as u64);
}
if written != expected {
return Err(Failure::new(format!(
"`{}` ended after {written} of {expected} bytes",
super::path::display(path)
)));
}
Ok(())
}