use crate::commands::CompressionLevel;
use oxiarc_archive::{
ArchiveFormat, LzhCompressionLevel, LzhReader, LzhWriter, TarReader, TarWriter,
ZipCompressionLevel, ZipReader, ZipWriter,
};
use oxiarc_core::EntryType;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, BufWriter, Seek, SeekFrom};
use std::path::{Path, PathBuf};
type AddEntry = (String, bool, Vec<u8>);
pub fn cmd_add(
archive: &Path,
files: &[PathBuf],
compression: CompressionLevel,
verbose: bool,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if !archive.exists() {
return Err(format!("archive not found: {}", archive.display()).into());
}
let file = File::open(archive)?;
let mut reader = BufReader::new(file);
let (format, _magic) = ArchiveFormat::detect(&mut reader)?;
reader.seek(SeekFrom::Start(0))?;
drop(reader);
match format {
ArchiveFormat::Zip => add_to_zip(archive, files, compression, verbose, dry_run),
ArchiveFormat::Tar => add_to_tar(archive, files, verbose, dry_run),
ArchiveFormat::Lzh => add_to_lzh(archive, files, verbose, dry_run),
other => {
eprintln!(
"error: `oxiarc add` does not support the {} format (only ZIP, TAR, and LZH are appendable).",
other
);
std::process::exit(2);
}
}
}
fn collect_input_entries(files: &[PathBuf]) -> Result<Vec<AddEntry>, Box<dyn std::error::Error>> {
let mut out: Vec<AddEntry> = Vec::new();
for path in files {
if !path.exists() {
return Err(format!("input not found: {}", path.display()).into());
}
collect_one(path, path, &mut out)?;
}
Ok(out)
}
fn collect_one(
path: &Path,
base: &Path,
out: &mut Vec<AddEntry>,
) -> Result<(), Box<dyn std::error::Error>> {
let rel = path
.strip_prefix(base.parent().unwrap_or(base))
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/");
if path.is_dir() {
out.push((rel.clone(), true, Vec::new()));
for child in std::fs::read_dir(path)? {
let child = child?;
collect_one(&child.path(), base, out)?;
}
} else {
let data = std::fs::read(path)?;
out.push((rel, false, data));
}
Ok(())
}
fn temp_path_for(archive: &Path) -> PathBuf {
let mut fname = archive
.file_name()
.map(|s| s.to_os_string())
.unwrap_or_else(|| std::ffi::OsString::from("archive"));
fname.push(format!(".{}.tmp", std::process::id()));
archive.with_file_name(fname)
}
fn add_to_zip(
archive: &Path,
files: &[PathBuf],
compression: CompressionLevel,
verbose: bool,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(archive)?;
let reader = BufReader::new(file);
let mut zip = ZipReader::new(reader)?;
let existing: Vec<_> = zip.entries().to_vec();
let mut existing_data: Vec<AddEntry> = Vec::with_capacity(existing.len());
for entry in &existing {
if entry.is_dir() {
existing_data.push((entry.name.clone(), true, Vec::new()));
} else {
let data = zip.extract(entry)?;
existing_data.push((entry.name.clone(), false, data));
}
}
drop(zip);
let new_entries = collect_input_entries(files)?;
if dry_run {
println!("[DRY RUN] Would update ZIP archive: {}", archive.display());
println!("[DRY RUN] Existing entries: {}", existing_data.len());
for (name, is_dir, data) in &new_entries {
if *is_dir {
println!("[DRY RUN] + (dir) {}", name);
} else {
println!("[DRY RUN] + {} ({} bytes)", name, data.len());
}
}
println!("[DRY RUN] No archive was modified.");
return Ok(());
}
let tmp = temp_path_for(archive);
{
let out = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp)?;
let writer = BufWriter::new(out);
let mut zw = ZipWriter::new(writer);
let level = match compression {
CompressionLevel::Store => ZipCompressionLevel::Store,
CompressionLevel::Fast => ZipCompressionLevel::Fast,
CompressionLevel::Normal => ZipCompressionLevel::Normal,
CompressionLevel::Best => ZipCompressionLevel::Best,
};
zw.set_compression(level);
for (name, is_dir, data) in existing_data {
if is_dir {
zw.add_directory(&name)?;
} else {
zw.add_file(&name, &data)?;
}
}
for (name, is_dir, data) in &new_entries {
if *is_dir {
zw.add_directory(name)?;
if verbose {
println!(" Added: {}/", name);
}
} else {
zw.add_file(name, data)?;
if verbose {
println!(" Added: {} ({} bytes)", name, data.len());
}
}
}
zw.finish()?;
}
std::fs::rename(&tmp, archive)?;
if verbose {
eprintln!("Updated {}", archive.display());
}
Ok(())
}
fn add_to_tar(
archive: &Path,
files: &[PathBuf],
verbose: bool,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(archive)?;
let reader = BufReader::new(file);
let mut tar = TarReader::new(reader)?;
let existing: Vec<_> = tar.entries().to_vec();
let mut existing_data: Vec<AddEntry> = Vec::with_capacity(existing.len());
for entry in &existing {
if entry.is_dir() {
existing_data.push((entry.name.clone(), true, Vec::new()));
} else if entry.entry_type == EntryType::File {
let data = tar.extract_to_vec(entry)?;
existing_data.push((entry.name.clone(), false, data));
} else {
if verbose {
eprintln!(
"note: skipping non-file entry {} (type {:?})",
entry.name, entry.entry_type
);
}
}
}
drop(tar);
let new_entries = collect_input_entries(files)?;
if dry_run {
println!("[DRY RUN] Would update TAR archive: {}", archive.display());
println!("[DRY RUN] Existing entries: {}", existing_data.len());
for (name, is_dir, data) in &new_entries {
if *is_dir {
println!("[DRY RUN] + (dir) {}", name);
} else {
println!("[DRY RUN] + {} ({} bytes)", name, data.len());
}
}
println!("[DRY RUN] No archive was modified.");
return Ok(());
}
let tmp = temp_path_for(archive);
{
let out = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp)?;
let writer = BufWriter::new(out);
let mut tw = TarWriter::new(writer);
for (name, is_dir, data) in existing_data {
if is_dir {
tw.add_directory(&name)?;
} else {
tw.add_file(&name, &data)?;
}
}
for (name, is_dir, data) in &new_entries {
if *is_dir {
tw.add_directory(name)?;
if verbose {
println!(" Added: {}/", name);
}
} else {
tw.add_file(name, data)?;
if verbose {
println!(" Added: {} ({} bytes)", name, data.len());
}
}
}
tw.finish()?;
}
std::fs::rename(&tmp, archive)?;
if verbose {
eprintln!("Updated {}", archive.display());
}
Ok(())
}
fn add_to_lzh(
archive: &Path,
files: &[PathBuf],
verbose: bool,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(archive)?;
let reader = BufReader::new(file);
let mut lzh = LzhReader::new(reader)?;
let existing: Vec<_> = lzh.entries();
let mut existing_data: Vec<AddEntry> = Vec::with_capacity(existing.len());
for entry in &existing {
if entry.is_dir() {
existing_data.push((entry.name.clone(), true, Vec::new()));
} else if entry.entry_type == EntryType::File {
let data = lzh.extract_to_vec(entry)?;
existing_data.push((entry.name.clone(), false, data));
}
}
drop(lzh);
let new_entries = collect_input_entries(files)?;
if dry_run {
println!("[DRY RUN] Would update LZH archive: {}", archive.display());
println!("[DRY RUN] Existing entries: {}", existing_data.len());
for (name, is_dir, data) in &new_entries {
if *is_dir {
println!("[DRY RUN] + (dir) {}", name);
} else {
println!("[DRY RUN] + {} ({} bytes)", name, data.len());
}
}
println!("[DRY RUN] No archive was modified.");
return Ok(());
}
let tmp = temp_path_for(archive);
{
let out = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp)?;
let writer = BufWriter::new(out);
let mut lw = LzhWriter::new(writer);
lw.set_compression(LzhCompressionLevel::Store);
for (name, is_dir, data) in existing_data {
if is_dir {
lw.add_directory(&name)?;
} else {
lw.add_file(&name, &data)?;
}
}
for (name, is_dir, data) in &new_entries {
if *is_dir {
lw.add_directory(name)?;
if verbose {
println!(" Added: {}/", name);
}
} else {
lw.add_file(name, data)?;
if verbose {
println!(" Added: {} ({} bytes)", name, data.len());
}
}
}
lw.finish()?;
}
std::fs::rename(&tmp, archive)?;
if verbose {
eprintln!("Updated {}", archive.display());
}
Ok(())
}