use std::collections::{HashMap, HashSet};
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use camino::{Utf8Path, Utf8PathBuf};
use globset::GlobSet;
use crate::cmd::Format;
use crate::error::{Error, Result};
use crate::filter;
use crate::{CompressOpts, progress::NoProgress};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AppendMode {
Append,
Update,
}
fn default_level_for(fmt: Format) -> Option<u32> {
match fmt {
Format::TarGz | Format::TarBz2 | Format::TarXz => Some(6),
_ => None,
}
}
const TAR_BLOCK: u64 = 512;
fn temp_path(archive: &Utf8Path) -> Utf8PathBuf {
Utf8PathBuf::from(format!("{archive}.tmp.rzappend"))
}
pub fn append(
archive: &Utf8Path,
fmt: Format,
inputs: &[Utf8PathBuf],
mode: AppendMode,
opts: &CompressOpts<'_>,
) -> Result<()> {
let op = match mode {
AppendMode::Append => "append",
AppendMode::Update => "update",
};
let inputs = &filter::validate_inputs(inputs, opts)?;
match fmt {
Format::Tar => tar_append(archive, inputs, mode, opts),
Format::TarGz | Format::TarZst | Format::TarXz => {
tar_compressed_append(archive, fmt, inputs, mode, opts)
}
#[cfg(feature = "bzip2")]
Format::TarBz2 => tar_compressed_append(archive, fmt, inputs, mode, opts),
#[cfg(not(feature = "bzip2"))]
Format::TarBz2 => Err(Error::FormatFeatureDisabled {
format: fmt.to_string(),
feature: "bzip2",
}),
Format::Zip => zip_append(archive, inputs, mode, opts),
Format::SevenZ => Err(Error::ModifyUnsupported {
operation: op,
format: fmt.to_string(),
}),
}
}
pub fn remove(
archive: &Utf8Path,
fmt: Format,
patterns: &[String],
level: Option<u32>,
) -> Result<()> {
let glob = filter::build_glob_set(patterns)?;
match fmt {
Format::Tar => tar_remove(archive, &glob),
Format::TarGz | Format::TarZst | Format::TarXz => {
tar_compressed_remove(archive, fmt, &glob, level)
}
#[cfg(feature = "bzip2")]
Format::TarBz2 => tar_compressed_remove(archive, fmt, &glob, level),
#[cfg(not(feature = "bzip2"))]
Format::TarBz2 => Err(Error::FormatFeatureDisabled {
format: fmt.to_string(),
feature: "bzip2",
}),
Format::Zip => zip_remove(archive, &glob),
Format::SevenZ => Err(Error::ModifyUnsupported {
operation: "remove",
format: fmt.to_string(),
}),
}
}
fn tar_append(
archive: &Utf8Path,
inputs: &[Utf8PathBuf],
mode: AppendMode,
opts: &CompressOpts<'_>,
) -> Result<()> {
let (archive_idx, body_end) = {
let file = fs_err::File::open(archive)?;
let mut buf = BufReader::new(file);
let scan = crate::tar_raw::scan(&mut buf)?;
let idx = (mode == AppendMode::Update).then(|| {
scan.entries
.iter()
.map(|e| (written_name(&e.name), e.mtime))
.collect::<HashMap<String, u64>>()
});
(idx, scan.body_end)
};
let opts = filtered_opts(opts, archive_idx.as_ref());
let res = tar_write_appended(archive, body_end, inputs, &opts, archive_idx.as_ref());
if res.is_err() {
let _ = tar_truncate_and_terminate(archive, body_end);
}
res
}
fn tar_write_appended(
archive: &Utf8Path,
body_end: u64,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let mut file = fs_err::OpenOptions::new()
.read(true)
.write(true)
.open(archive)?;
file.seek(SeekFrom::Start(body_end))?;
file.set_len(body_end)?;
let buf = BufWriter::new(file);
let mut builder = tar::Builder::new(buf);
builder.follow_symlinks(opts.follow_symlinks);
append_inputs_with_index(&mut builder, inputs, opts, archive_idx)?;
let buf = builder.into_inner()?;
let file = buf.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
Ok(())
}
fn tar_truncate_and_terminate(archive: &Utf8Path, body_end: u64) -> Result<()> {
let mut file = fs_err::OpenOptions::new()
.read(true)
.write(true)
.open(archive)?;
file.set_len(body_end)?;
file.seek(SeekFrom::Start(body_end))?;
file.write_all(&[0u8; 2 * TAR_BLOCK as usize])?;
file.sync_all()?;
Ok(())
}
fn filtered_opts<'a>(
opts: &CompressOpts<'a>,
_archive_idx: Option<&HashMap<String, u64>>,
) -> CompressOpts<'a> {
CompressOpts {
level: opts.level,
excludes: opts.excludes.clone(),
follow_symlinks: opts.follow_symlinks,
exclude_vcs_ignores: opts.exclude_vcs_ignores,
no_recursion: opts.no_recursion,
progress: opts.progress,
fixed_mtime: opts.fixed_mtime,
fixed_uid: opts.fixed_uid,
fixed_gid: opts.fixed_gid,
fixed_mode: opts.fixed_mode,
newer_than: opts.newer_than,
older_than: opts.older_than,
ignore_failed_read: opts.ignore_failed_read,
password: opts.password.clone(),
}
}
fn append_inputs_with_index<W: Write>(
builder: &mut tar::Builder<W>,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let Some(idx) = archive_idx else {
return filter::append_inputs(builder, inputs, opts);
};
for input in inputs {
let meta = filter::input_metadata(input, opts.follow_symlinks)?;
let name = filter::input_base_name(input)?;
if opts.excludes.is_match(&name) {
continue;
}
if meta.is_dir() {
walk_and_append_with_index(builder, input, &name, opts, idx)?;
} else {
if !update_wants(&name, &meta, idx) {
continue;
}
append_one_file(builder, input, &name, opts)?;
opts.progress.set_entry(&name);
opts.progress.inc(meta.len());
}
}
Ok(())
}
fn written_name(name: &str) -> String {
let parts: Vec<&str> = name
.split('/')
.filter(|c| !c.is_empty() && *c != ".")
.collect();
if parts.is_empty() {
".".to_owned()
} else {
parts.join("/")
}
}
fn update_wants(name: &str, meta: &std::fs::Metadata, idx: &HashMap<String, u64>) -> bool {
is_newer_than_archive(&written_name(name), mtime_secs(meta), idx)
}
fn walk_and_append_with_index<W: Write>(
builder: &mut tar::Builder<W>,
dir: &Utf8Path,
prefix: &str,
opts: &CompressOpts<'_>,
idx: &HashMap<String, u64>,
) -> Result<()> {
if opts.no_recursion {
let meta = filter::input_metadata(dir, opts.follow_symlinks)?;
if update_wants(prefix, &meta, idx) {
builder.append_dir(prefix, dir.as_std_path())?;
}
return Ok(());
}
filter::walk_dir(dir, prefix, opts, &mut |entry| {
let meta = filter::input_metadata(&entry.fs_path, opts.follow_symlinks)?;
if !update_wants(&entry.archive_name, &meta, idx) {
return Ok(());
}
if entry.is_dir {
builder.append_dir(&entry.archive_name, entry.fs_path.as_std_path())?;
} else {
append_one_file(builder, &entry.fs_path, &entry.archive_name, opts)?;
opts.progress.set_entry(&entry.archive_name);
opts.progress.inc(meta.len());
}
Ok(())
})
}
fn plan_update_names(
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
idx: &HashMap<String, u64>,
) -> Result<HashSet<String>> {
let mut planned = HashSet::new();
for input in inputs {
let meta = filter::input_metadata(input, opts.follow_symlinks)?;
let name = filter::input_base_name(input)?;
if opts.excludes.is_match(&name) {
continue;
}
if meta.is_dir() {
if opts.no_recursion {
if update_wants(&name, &meta, idx) {
planned.insert(written_name(&name));
}
continue;
}
filter::walk_dir(input, &name, opts, &mut |entry| {
let entry_meta = filter::input_metadata(&entry.fs_path, opts.follow_symlinks)?;
if update_wants(&entry.archive_name, &entry_meta, idx) {
planned.insert(written_name(&entry.archive_name));
}
Ok(())
})?;
} else if update_wants(&name, &meta, idx) {
planned.insert(written_name(&name));
}
}
Ok(planned)
}
fn append_one_file<W: Write>(
builder: &mut tar::Builder<W>,
fs_path: &Utf8Path,
archive_name: &str,
opts: &CompressOpts<'_>,
) -> Result<()> {
let _ = opts;
builder.append_path_with_name(fs_path, archive_name)?;
Ok(())
}
fn mtime_secs(meta: &std::fs::Metadata) -> u64 {
meta.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn is_newer_than_archive(name: &str, fs_mtime: u64, idx: &HashMap<String, u64>) -> bool {
match idx.get(name) {
Some(&archive_mtime) => fs_mtime > archive_mtime,
None => true,
}
}
fn open_tar_reader(archive: &Utf8Path, fmt: Format) -> Result<Box<dyn Read>> {
let file = fs_err::File::open(archive)?;
let buf = BufReader::new(file);
match fmt {
Format::Tar => Ok(Box::new(buf)),
Format::TarGz => Ok(Box::new(flate2::read::MultiGzDecoder::new(buf))),
Format::TarZst => Ok(Box::new(crate::tar_zst::MultiFrameDecoder::new(buf)?)),
Format::TarXz => xz_read(buf),
#[cfg(feature = "bzip2")]
Format::TarBz2 => Ok(Box::new(bzip2::read::MultiBzDecoder::new(buf))),
#[cfg(not(feature = "bzip2"))]
Format::TarBz2 => Err(Error::FormatFeatureDisabled {
format: fmt.to_string(),
feature: "bzip2",
}),
Format::Zip | Format::SevenZ => Err(Error::ModifyUnsupported {
operation: "tar-reader",
format: fmt.to_string(),
}),
}
}
#[cfg(feature = "xz2")]
fn xz_read(buf: BufReader<fs_err::File>) -> Result<Box<dyn Read>> {
Ok(Box::new(xz2::read::XzDecoder::new(buf)))
}
#[cfg(not(feature = "xz2"))]
fn xz_read(buf: BufReader<fs_err::File>) -> Result<Box<dyn Read>> {
Ok(Box::new(lzma_rust2::XzReader::new(buf, true)))
}
fn tar_compressed_writer(
fmt: Format,
writer: BufWriter<fs_err::File>,
level: Option<u32>,
) -> Result<Box<dyn EncoderHandle>> {
match fmt {
Format::TarGz => Ok(Box::new(GzHandle::new(
writer,
crate::tar_gz::validate_level(level.unwrap_or(6))?,
))),
Format::TarZst => Ok(Box::new(ZstHandle::new(writer, level)?)),
Format::TarXz => xz_writer(writer, level.unwrap_or(6)),
#[cfg(feature = "bzip2")]
Format::TarBz2 => Ok(Box::new(Bz2Handle::new(writer, level.unwrap_or(6))?)),
#[cfg(not(feature = "bzip2"))]
Format::TarBz2 => Err(Error::FormatFeatureDisabled {
format: fmt.to_string(),
feature: "bzip2",
}),
_ => Err(Error::ModifyUnsupported {
operation: "rewrite",
format: fmt.to_string(),
}),
}
}
#[cfg(feature = "xz2")]
fn xz_writer(writer: BufWriter<fs_err::File>, level: u32) -> Result<Box<dyn EncoderHandle>> {
let level = crate::tar_xz::validate_level(level)?;
Ok(Box::new(XzHandle::new(writer, level)))
}
#[cfg(not(feature = "xz2"))]
fn xz_writer(writer: BufWriter<fs_err::File>, level: u32) -> Result<Box<dyn EncoderHandle>> {
let level = crate::tar_xz::validate_level(level)?;
Ok(Box::new(LzmaRust2XzHandle::new(writer, level)?))
}
trait EncoderHandle {
fn copy_existing(
&mut self,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()>;
fn append_inputs(
&mut self,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()>;
fn finish(self: Box<Self>) -> Result<()>;
}
fn copy_raw_entries<W: Write>(
builder: &mut tar::Builder<W>,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()> {
crate::tar_raw::copy_entries(reader, builder.get_mut(), keep)
}
struct GzHandle {
builder: Option<tar::Builder<flate2::write::GzEncoder<BufWriter<fs_err::File>>>>,
}
impl GzHandle {
fn new(writer: BufWriter<fs_err::File>, level: u32) -> Self {
let enc = flate2::write::GzEncoder::new(writer, flate2::Compression::new(level));
Self {
builder: Some(tar::Builder::new(enc)),
}
}
}
impl EncoderHandle for GzHandle {
fn copy_existing(
&mut self,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
copy_raw_entries(b, reader, keep)
}
fn append_inputs(
&mut self,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
b.follow_symlinks(opts.follow_symlinks);
append_inputs_with_index(b, inputs, opts, archive_idx)
}
fn finish(mut self: Box<Self>) -> Result<()> {
let b = self.builder.take().ok_or_else(builder_taken_err)?;
let enc = b.into_inner()?;
let buf = enc.finish()?;
let file = buf.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
Ok(())
}
}
struct ZstHandle {
tar_buf: Vec<u8>,
builder: Option<tar::Builder<Vec<u8>>>,
out: Option<BufWriter<fs_err::File>>,
level: ruzstd::encoding::CompressionLevel,
}
impl ZstHandle {
fn new(out: BufWriter<fs_err::File>, level: Option<u32>) -> Result<Self> {
let level = match level {
None => ruzstd::encoding::CompressionLevel::Fastest,
Some(0) => ruzstd::encoding::CompressionLevel::Uncompressed,
Some(_) => return Err(Error::ZstdLevelUnsupported),
};
let mut s = Self {
tar_buf: Vec::new(),
builder: None,
out: Some(out),
level,
};
s.builder = Some(tar::Builder::new(std::mem::take(&mut s.tar_buf)));
Ok(s)
}
}
impl EncoderHandle for ZstHandle {
fn copy_existing(
&mut self,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
copy_raw_entries(b, reader, keep)
}
fn append_inputs(
&mut self,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
b.follow_symlinks(opts.follow_symlinks);
append_inputs_with_index(b, inputs, opts, archive_idx)
}
fn finish(mut self: Box<Self>) -> Result<()> {
let b = self.builder.take().ok_or_else(builder_taken_err)?;
let tar_data = b.into_inner()?;
let mut out = self.out.take().ok_or_else(builder_taken_err)?;
let mut compressed = Vec::new();
ruzstd::encoding::compress(std::io::Cursor::new(&tar_data), &mut compressed, self.level);
out.write_all(&compressed)?;
let file = out.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
Ok(())
}
}
#[cfg(feature = "xz2")]
struct XzHandle {
builder: Option<tar::Builder<xz2::write::XzEncoder<BufWriter<fs_err::File>>>>,
}
#[cfg(feature = "xz2")]
impl XzHandle {
fn new(writer: BufWriter<fs_err::File>, level: u32) -> Self {
let enc = xz2::write::XzEncoder::new(writer, level);
Self {
builder: Some(tar::Builder::new(enc)),
}
}
}
#[cfg(feature = "xz2")]
impl EncoderHandle for XzHandle {
fn copy_existing(
&mut self,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
copy_raw_entries(b, reader, keep)
}
fn append_inputs(
&mut self,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
b.follow_symlinks(opts.follow_symlinks);
append_inputs_with_index(b, inputs, opts, archive_idx)
}
fn finish(mut self: Box<Self>) -> Result<()> {
let b = self.builder.take().ok_or_else(builder_taken_err)?;
let enc = b.into_inner()?;
let buf = enc.finish()?;
let file = buf.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
Ok(())
}
}
#[cfg(not(feature = "xz2"))]
struct LzmaRust2XzHandle {
builder: Option<tar::Builder<lzma_rust2::XzWriter<BufWriter<fs_err::File>>>>,
}
#[cfg(not(feature = "xz2"))]
impl LzmaRust2XzHandle {
fn new(writer: BufWriter<fs_err::File>, level: u32) -> Result<Self> {
let enc = lzma_rust2::XzWriter::new(writer, lzma_rust2::XzOptions::with_preset(level))?;
Ok(Self {
builder: Some(tar::Builder::new(enc)),
})
}
}
#[cfg(not(feature = "xz2"))]
impl EncoderHandle for LzmaRust2XzHandle {
fn copy_existing(
&mut self,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
copy_raw_entries(b, reader, keep)
}
fn append_inputs(
&mut self,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
b.follow_symlinks(opts.follow_symlinks);
append_inputs_with_index(b, inputs, opts, archive_idx)
}
fn finish(mut self: Box<Self>) -> Result<()> {
let b = self.builder.take().ok_or_else(builder_taken_err)?;
let enc = b.into_inner()?;
let buf = enc.finish()?;
let file = buf.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
Ok(())
}
}
#[cfg(feature = "bzip2")]
struct Bz2Handle {
builder: Option<tar::Builder<bzip2::write::BzEncoder<BufWriter<fs_err::File>>>>,
}
#[cfg(feature = "bzip2")]
impl Bz2Handle {
fn new(writer: BufWriter<fs_err::File>, level: u32) -> Result<Self> {
let compression = bzip2::Compression::try_new(level)
.ok_or_else(|| std::io::Error::other("bzip2 compression level must be 1..=9"))?;
let enc = bzip2::write::BzEncoder::new(writer, compression);
Ok(Self {
builder: Some(tar::Builder::new(enc)),
})
}
}
#[cfg(feature = "bzip2")]
impl EncoderHandle for Bz2Handle {
fn copy_existing(
&mut self,
reader: &mut dyn Read,
keep: &mut dyn FnMut(&str) -> bool,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
copy_raw_entries(b, reader, keep)
}
fn append_inputs(
&mut self,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<()> {
let b = self.builder.as_mut().ok_or_else(builder_taken_err)?;
b.follow_symlinks(opts.follow_symlinks);
append_inputs_with_index(b, inputs, opts, archive_idx)
}
fn finish(mut self: Box<Self>) -> Result<()> {
let b = self.builder.take().ok_or_else(builder_taken_err)?;
let enc = b.into_inner()?;
let buf = enc.finish()?;
let file = buf.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
Ok(())
}
}
fn builder_taken_err() -> Error {
Error::Io(std::io::Error::other(
"tar builder already finalized — internal modify-pipeline error",
))
}
fn tar_compressed_append(
archive: &Utf8Path,
fmt: Format,
inputs: &[Utf8PathBuf],
mode: AppendMode,
opts: &CompressOpts<'_>,
) -> Result<()> {
let tmp = temp_path(archive);
let res = tar_compressed_append_into(archive, &tmp, fmt, inputs, mode, opts);
if res.is_err() {
let _ = fs_err::remove_file(&tmp);
}
res
}
fn tar_compressed_append_into(
archive: &Utf8Path,
tmp: &Utf8Path,
fmt: Format,
inputs: &[Utf8PathBuf],
mode: AppendMode,
opts: &CompressOpts<'_>,
) -> Result<()> {
let level = opts.level.or_else(|| default_level_for(fmt));
let (archive_idx, superseded) = if mode == AppendMode::Update {
let mut reader = open_tar_reader(archive, fmt)?;
let scan = crate::tar_raw::scan(&mut reader)?;
let idx: HashMap<String, u64> = scan
.entries
.iter()
.map(|e| (written_name(&e.name), e.mtime))
.collect();
let mut planned = plan_update_names(inputs, opts, &idx)?;
loop {
let rescued: Vec<String> = scan
.entries
.iter()
.filter(|e| !planned.contains(&written_name(&e.name)))
.filter_map(|e| e.hardlink_target.as_deref().map(written_name))
.filter(|target| planned.contains(target))
.collect();
if rescued.is_empty() {
break;
}
for target in rescued {
planned.remove(&target);
}
}
(Some(idx), planned)
} else {
(None, HashSet::new())
};
let out_file = fs_err::File::create(tmp)?;
let out_buf = BufWriter::new(out_file);
let mut handle = tar_compressed_writer(fmt, out_buf, level)?;
let mut reader = open_tar_reader(archive, fmt)?;
let mut keep = |name: &str| !superseded.contains(&written_name(name));
handle.copy_existing(&mut reader, &mut keep)?;
handle.append_inputs(inputs, opts, archive_idx.as_ref())?;
handle.finish()?;
fs_err::rename(tmp, archive)?;
Ok(())
}
fn tar_remove(archive: &Utf8Path, glob: &GlobSet) -> Result<()> {
let tmp = temp_path(archive);
let res = tar_remove_into(archive, &tmp, glob);
if res.is_err() {
let _ = fs_err::remove_file(&tmp);
}
res
}
fn tar_remove_into(archive: &Utf8Path, tmp: &Utf8Path, glob: &GlobSet) -> Result<()> {
let out_file = fs_err::File::create(tmp)?;
let out_buf = BufWriter::new(out_file);
let mut builder = tar::Builder::new(out_buf);
let in_file = fs_err::File::open(archive)?;
let mut reader: Box<dyn Read> = Box::new(BufReader::new(in_file));
let mut keep = |name: &str| !glob.is_match(name.trim_end_matches('/'));
copy_raw_entries(&mut builder, &mut reader, &mut keep)?;
let buf = builder.into_inner()?;
let file = buf.into_inner().map_err(std::io::Error::other)?;
file.sync_all()?;
fs_err::rename(tmp, archive)?;
Ok(())
}
fn tar_compressed_remove(
archive: &Utf8Path,
fmt: Format,
glob: &GlobSet,
level: Option<u32>,
) -> Result<()> {
let tmp = temp_path(archive);
let res = tar_compressed_remove_into(archive, &tmp, fmt, glob, level);
if res.is_err() {
let _ = fs_err::remove_file(&tmp);
}
res
}
fn tar_compressed_remove_into(
archive: &Utf8Path,
tmp: &Utf8Path,
fmt: Format,
glob: &GlobSet,
level: Option<u32>,
) -> Result<()> {
let level = level.or_else(|| default_level_for(fmt));
let out_file = fs_err::File::create(tmp)?;
let out_buf = BufWriter::new(out_file);
let mut handle = tar_compressed_writer(fmt, out_buf, level)?;
let mut reader = open_tar_reader(archive, fmt)?;
let mut keep = |name: &str| !glob.is_match(name.trim_end_matches('/'));
handle.copy_existing(&mut reader, &mut keep)?;
let empty: &[Utf8PathBuf] = &[];
let opts = CompressOpts::new(level, GlobSet::empty());
let opts = CompressOpts {
progress: &NoProgress,
..opts
};
handle.append_inputs(empty, &opts, None)?;
handle.finish()?;
fs_err::rename(tmp, archive)?;
Ok(())
}
fn zip_index(archive: &Utf8Path) -> Result<HashMap<String, u64>> {
let file = fs_err::File::open(archive)?;
let mut a = zip::ZipArchive::new(file)?;
let mut idx = HashMap::new();
for i in 0..a.len() {
let entry = a.by_index_raw(i)?;
let name = entry.name().to_owned();
let mtime = entry.last_modified().map(zip_dt_to_secs).unwrap_or(0);
idx.insert(name, mtime);
}
Ok(idx)
}
fn zip_dt_to_secs(dt: zip::DateTime) -> u64 {
let Ok(month) = time::Month::try_from(dt.month()) else {
return 0;
};
let Ok(date) = time::Date::from_calendar_date(dt.year() as i32, month, dt.day()) else {
return 0;
};
let Ok(t) = time::Time::from_hms(dt.hour(), dt.minute(), dt.second()) else {
return 0;
};
let stamp = time::PrimitiveDateTime::new(date, t)
.assume_utc()
.unix_timestamp();
if stamp >= 0 { stamp as u64 } else { 0 }
}
enum PlannedZipEntry {
File {
name: String,
fs_path: Utf8PathBuf,
},
Symlink {
name: String,
fs_path: Utf8PathBuf,
},
Dir {
name: String,
},
}
impl PlannedZipEntry {
fn name(&self) -> &str {
match self {
Self::File { name, .. } | Self::Symlink { name, .. } | Self::Dir { name } => name,
}
}
}
fn plan_zip_file(name: String, fs_path: Utf8PathBuf, follow_symlinks: bool) -> Result<PlannedZipEntry> {
let is_symlink =
!follow_symlinks && fs_err::symlink_metadata(&fs_path)?.file_type().is_symlink();
Ok(if is_symlink {
PlannedZipEntry::Symlink { name, fs_path }
} else {
PlannedZipEntry::File { name, fs_path }
})
}
fn zip_append(
archive: &Utf8Path,
inputs: &[Utf8PathBuf],
mode: AppendMode,
opts: &CompressOpts<'_>,
) -> Result<()> {
let archive_idx = if mode == AppendMode::Update {
Some(zip_index(archive)?)
} else {
None
};
let planned = plan_zip_entries(inputs, opts, archive_idx.as_ref())?;
if planned.is_empty() {
return Ok(());
}
let tmp = temp_path(archive);
let res = zip_rewrite_with(archive, &tmp, &planned, opts);
if res.is_err() {
let _ = fs_err::remove_file(&tmp);
}
res
}
fn plan_zip_entries(
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
archive_idx: Option<&HashMap<String, u64>>,
) -> Result<Vec<PlannedZipEntry>> {
let mut planned = Vec::new();
for input in inputs {
let meta = filter::input_metadata(input, opts.follow_symlinks)?;
let name = filter::input_base_name(input)?;
if opts.excludes.is_match(&name) {
continue;
}
if meta.is_dir() {
if opts.no_recursion {
planned.push(PlannedZipEntry::Dir { name });
continue;
}
filter::walk_dir(input, &name, opts, &mut |entry| {
if entry.is_dir {
return Ok(());
}
let meta = filter::input_metadata(&entry.fs_path, opts.follow_symlinks)?;
if !should_add_zip_entry(&entry.archive_name, &meta, archive_idx) {
return Ok(());
}
if filter::skip_unarchivable_special(&meta, &entry.archive_name) {
return Ok(());
}
planned.push(plan_zip_file(
entry.archive_name,
entry.fs_path,
opts.follow_symlinks,
)?);
Ok(())
})?;
} else if should_add_zip_entry(&name, &meta, archive_idx) {
if filter::skip_unarchivable_special(&meta, &name) {
continue;
}
planned.push(plan_zip_file(name, input.clone(), opts.follow_symlinks)?);
}
}
Ok(planned)
}
fn carry_zip_entry<R, W>(
src: &mut zip::ZipArchive<R>,
index: usize,
dst: &mut zip::ZipWriter<W>,
) -> Result<()>
where
R: Read + std::io::Seek,
W: Write + std::io::Seek,
{
let raw = src.by_index_raw(index)?;
let name = raw.name().to_owned();
let mode = raw.unix_mode();
let mtime = raw.last_modified().filter(zip::DateTime::is_valid);
let is_dir = raw.is_dir();
let rewrite_symlink = raw.is_symlink() && !raw.encrypted();
drop(raw);
let mut options = zip::write::SimpleFileOptions::default();
if let Some(dt) = mtime {
options = options.last_modified_time(dt);
}
if let Some(mode) = mode {
options = options.unix_permissions(mode);
}
if is_dir {
dst.add_directory(name, options)?;
} else if rewrite_symlink {
let entry = src.by_index(index)?;
let mut target = Vec::new();
entry
.take(crate::zip::MAX_SYMLINK_TARGET + 1)
.read_to_end(&mut target)?;
if target.len() as u64 > crate::zip::MAX_SYMLINK_TARGET {
return Err(Error::SymlinkTargetTooLong {
path: name.into(),
max: crate::zip::MAX_SYMLINK_TARGET,
});
}
let target = String::from_utf8(target)
.map_err(|e| Error::InvalidUtf8Path(String::from_utf8_lossy(e.as_bytes()).into_owned()))?;
dst.add_symlink(name, target, options)?;
} else {
dst.raw_copy_file(src.by_index_raw(index)?)?;
}
Ok(())
}
fn zip_rewrite_with(
archive: &Utf8Path,
tmp: &Utf8Path,
planned: &[PlannedZipEntry],
opts: &CompressOpts<'_>,
) -> Result<()> {
let superseded: HashSet<&str> = planned.iter().map(PlannedZipEntry::name).collect();
let in_file = fs_err::File::open(archive)?;
let mut src = zip::ZipArchive::new(in_file)?;
let out_file = fs_err::File::create(tmp)?;
let mut dst = zip::ZipWriter::new(out_file);
for i in 0..src.len() {
let skip = superseded.contains(src.by_index_raw(i)?.name().trim_end_matches('/'));
if skip {
continue;
}
carry_zip_entry(&mut src, i, &mut dst)?;
}
let (method, level) = crate::zip::compression_settings(opts.level);
let options = zip::write::SimpleFileOptions::default()
.compression_method(method)
.compression_level(level);
for entry in planned {
match entry {
PlannedZipEntry::Dir { name } => {
dst.add_directory(format!("{name}/"), options)?;
}
PlannedZipEntry::Symlink { name, fs_path } => {
crate::zip::write_symlink_entry(&mut dst, fs_path, name, options, opts)?;
}
PlannedZipEntry::File { name, fs_path } => {
let meta = filter::input_metadata(fs_path, opts.follow_symlinks)?;
dst.start_file(name, crate::zip::with_unix_mode(options, &meta))?;
let mut f = fs_err::File::open(fs_path)?;
let size = std::io::copy(&mut f, &mut dst)?;
opts.progress.set_entry(name);
opts.progress.inc(size);
}
}
}
let file = dst.finish()?;
file.sync_all()?;
fs_err::rename(tmp, archive)?;
Ok(())
}
fn should_add_zip_entry(
name: &str,
meta: &std::fs::Metadata,
archive_idx: Option<&HashMap<String, u64>>,
) -> bool {
let Some(idx) = archive_idx else {
return true;
};
let fs_mtime = mtime_secs(meta);
is_newer_than_archive(name, fs_mtime, idx)
}
fn zip_remove(archive: &Utf8Path, glob: &GlobSet) -> Result<()> {
let tmp = temp_path(archive);
let res = zip_remove_into(archive, &tmp, glob);
if res.is_err() {
let _ = fs_err::remove_file(&tmp);
}
res
}
fn zip_remove_into(archive: &Utf8Path, tmp: &Utf8Path, glob: &GlobSet) -> Result<()> {
let in_file = fs_err::File::open(archive)?;
let mut src = zip::ZipArchive::new(in_file)?;
let out_file = fs_err::File::create(tmp)?;
let mut dst = zip::ZipWriter::new(out_file);
for i in 0..src.len() {
let name = src.by_index_raw(i)?.name().to_owned();
if glob.is_match(name.trim_end_matches('/')) {
continue;
}
carry_zip_entry(&mut src, i, &mut dst)?;
}
let file = dst.finish()?;
file.sync_all()?;
fs_err::rename(tmp, archive)?;
Ok(())
}