use std::io::BufRead;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use camino::{Utf8Path, Utf8PathBuf};
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use crate::error::{Error, Result};
use crate::progress::ProgressReport;
use crate::{CompressOpts, DecompressOpts};
pub fn should_extract(path: &str, includes: &GlobSet, excludes: &GlobSet) -> bool {
let clean = path.trim_end_matches('/');
if !includes.is_empty() && !includes.is_match(clean) {
return false;
}
if !excludes.is_empty() && excludes.is_match(clean) {
return false;
}
true
}
pub fn build_glob_set(patterns: &[String]) -> Result<GlobSet> {
if patterns.is_empty() {
return Ok(GlobSet::empty());
}
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
let effective = if pattern.contains('/') {
pattern.clone()
} else {
format!("**/{pattern}")
};
let glob = GlobBuilder::new(&effective)
.literal_separator(true)
.build()
.map_err(|e| Error::InvalidExcludePattern(e.to_string()))?;
builder.add(glob);
let dir_glob = GlobBuilder::new(&format!("{effective}/**"))
.literal_separator(true)
.build()
.map_err(|e| Error::InvalidExcludePattern(e.to_string()))?;
builder.add(dir_glob);
}
builder
.build()
.map_err(|e| Error::InvalidExcludePattern(e.to_string()))
}
pub fn strip_components(path: &Utf8Path, n: u32) -> Option<Utf8PathBuf> {
if n == 0 {
return Some(path.to_owned());
}
let mut components = path.components();
for _ in 0..n {
components.next()?;
}
let remaining = components.as_path();
if remaining.as_str().is_empty() {
None
} else {
Some(remaining.to_owned())
}
}
pub fn apply_path_rewrites(
path: Utf8PathBuf,
renames: &[(String, String)],
prefix: Option<&Utf8Path>,
) -> Result<Utf8PathBuf> {
let mut s = path.into_string();
for (old, new) in renames {
s = s.replace(old.as_str(), new.as_str());
}
if s.is_empty() {
return Ok(Utf8PathBuf::new());
}
let combined = if let Some(p) = prefix {
let mut joined = p.to_owned();
joined.push(&s);
joined
} else {
Utf8PathBuf::from(s)
};
safe_entry_path(combined.as_str())?;
Ok(combined)
}
pub fn resolve_entry_path(
path: &Utf8Path,
opts: &DecompressOpts<'_>,
) -> Result<Option<Utf8PathBuf>> {
let stripped = match strip_components(path, opts.strip_components) {
Some(p) => p,
None => return Ok(None),
};
let flattened = if opts.no_directory {
match stripped.file_name() {
Some(name) => Utf8PathBuf::from(name),
None => return Ok(None),
}
} else {
stripped
};
match apply_path_rewrites(flattened, &opts.renames, opts.prefix.as_deref())? {
p if p.as_str().is_empty() => Ok(None),
p => Ok(Some(normalize_rel_path(&p))),
}
}
fn normalize_rel_path(p: &Utf8Path) -> Utf8PathBuf {
let normalized: Utf8PathBuf = p
.components()
.filter(|c| matches!(c, camino::Utf8Component::Normal(_)))
.collect();
if normalized.as_str().is_empty() {
Utf8PathBuf::from(".")
} else {
normalized
}
}
pub fn vcs_walker(dir: &Utf8Path, follow_symlinks: bool) -> ignore::Walk {
ignore::WalkBuilder::new(dir.as_std_path())
.standard_filters(false)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.follow_links(follow_symlinks)
.sort_by_file_name(|a, b| a.cmp(b))
.build()
}
pub struct WalkEntry {
pub archive_name: String,
pub fs_path: Utf8PathBuf,
pub is_dir: bool,
}
pub fn walk_dir<F>(
dir: &Utf8Path,
prefix: &str,
opts: &CompressOpts<'_>,
visit: &mut F,
) -> Result<()>
where
F: FnMut(WalkEntry) -> Result<()>,
{
if opts.exclude_vcs_ignores {
walk_dir_vcs(dir, prefix, &opts.excludes, opts.follow_symlinks, visit)
} else {
visit(WalkEntry {
archive_name: prefix.to_owned(),
fs_path: dir.to_owned(),
is_dir: true,
})?;
walk_dir_simple(dir, prefix, &opts.excludes, opts.follow_symlinks, visit)
}
}
fn walk_dir_simple<F>(
dir: &Utf8Path,
prefix: &str,
excludes: &GlobSet,
follow_symlinks: bool,
visit: &mut F,
) -> Result<()>
where
F: FnMut(WalkEntry) -> Result<()>,
{
let mut entries: Vec<_> = fs_err::read_dir(dir)?.collect::<std::result::Result<Vec<_>, _>>()?;
entries.sort_by_cached_key(|e| e.file_name());
for entry in entries {
let entry_path = entry.path();
let file_name = entry_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| Error::InvalidUtf8Path(entry_path.display().to_string()))?;
let archive_name = format!("{prefix}/{file_name}");
if excludes.is_match(&archive_name) {
continue;
}
let entry_str = entry_path
.to_str()
.ok_or_else(|| Error::InvalidUtf8Path(entry_path.display().to_string()))?;
let utf8_path = Utf8Path::new(entry_str);
let is_dir = if follow_symlinks {
fs_err::metadata(utf8_path)?.is_dir()
} else {
entry.file_type()?.is_dir()
};
visit(WalkEntry {
archive_name: archive_name.clone(),
fs_path: utf8_path.to_owned(),
is_dir,
})?;
if is_dir {
walk_dir_simple(utf8_path, &archive_name, excludes, follow_symlinks, visit)?;
}
}
Ok(())
}
fn walk_dir_vcs<F>(
dir: &Utf8Path,
prefix: &str,
excludes: &GlobSet,
follow_symlinks: bool,
visit: &mut F,
) -> Result<()>
where
F: FnMut(WalkEntry) -> Result<()>,
{
for result in vcs_walker(dir, follow_symlinks) {
let entry = result.map_err(|e| std::io::Error::other(e.to_string()))?;
let fs_path = entry.path();
let relative = fs_path
.strip_prefix(dir.as_std_path())
.map_err(|e| std::io::Error::other(e.to_string()))?;
if relative.as_os_str().is_empty() {
visit(WalkEntry {
archive_name: prefix.to_owned(),
fs_path: dir.to_owned(),
is_dir: true,
})?;
continue;
}
let rel_str = relative
.to_str()
.ok_or_else(|| Error::InvalidUtf8Path(relative.display().to_string()))?;
let archive_name = format!("{prefix}/{rel_str}");
if excludes.is_match(&archive_name) {
continue;
}
let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());
let utf8_str = fs_path
.to_str()
.ok_or_else(|| Error::InvalidUtf8Path(fs_path.display().to_string()))?;
visit(WalkEntry {
archive_name,
fs_path: Utf8PathBuf::from(utf8_str),
is_dir,
})?;
}
Ok(())
}
pub fn safe_entry_path(name: &str) -> Result<()> {
for component in Utf8Path::new(name).components() {
if matches!(component, camino::Utf8Component::ParentDir) {
return Err(Error::PathTraversal(name.to_owned()));
}
}
if Utf8Path::new(name).is_absolute() {
return Err(Error::PathTraversal(name.to_owned()));
}
Ok(())
}
pub fn safe_link_target(link: &str, target: &str) -> Result<()> {
if Utf8Path::new(target).is_absolute() {
return Err(Error::PathTraversal(format!("{link} -> {target}")));
}
for component in Utf8Path::new(target).components() {
if matches!(component, camino::Utf8Component::ParentDir) {
return Err(Error::PathTraversal(format!("{link} -> {target}")));
}
}
Ok(())
}
pub fn input_metadata(path: &Utf8Path, follow_symlinks: bool) -> Result<std::fs::Metadata> {
if follow_symlinks {
Ok(fs_err::metadata(path)?)
} else {
Ok(fs_err::symlink_metadata(path)?)
}
}
pub fn input_base_name(input: &Utf8Path) -> Result<String> {
if let Some(name) = input.file_name() {
return Ok(name.to_owned());
}
if !input.as_str().split('/').any(|c| c == "..") {
return Ok(input.as_str().to_owned());
}
let canonical = input.canonicalize_utf8()?;
match canonical.file_name() {
Some(name) => Ok(name.to_owned()),
None => Err(Error::Io(std::io::Error::other(format!(
"cannot derive an entry name for `{input}`: it resolves to the filesystem root"
)))),
}
}
pub fn skip_unarchivable_special(meta: &std::fs::Metadata, name: &str) -> bool {
use std::io::Write;
let ft = meta.file_type();
if ft.is_file() || ft.is_dir() || ft.is_symlink() {
return false;
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"rz: warning: skipping `{}`: special files are not representable in this format",
crate::progress::escape_entry_name(name),
);
true
}
#[allow(clippy::disallowed_methods)]
fn stat_input_raw(path: &Utf8Path, follow_symlinks: bool) -> std::io::Result<std::fs::Metadata> {
if follow_symlinks {
std::fs::metadata(path)
} else {
std::fs::symlink_metadata(path)
}
}
pub fn validate_inputs(
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
) -> Result<Vec<Utf8PathBuf>> {
use std::io::Write;
let mut valid = Vec::with_capacity(inputs.len());
for input in inputs {
let name = input.file_name().unwrap_or(input.as_str());
if opts.excludes.is_match(name) {
continue;
}
match stat_input_raw(input, opts.follow_symlinks) {
Ok(_) => valid.push(input.clone()),
Err(source) if opts.ignore_failed_read => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "rz: warning: cannot read `{input}`: {source}");
}
Err(source) => {
return Err(Error::CannotReadInput {
path: input.clone(),
source,
});
}
}
}
if valid.is_empty() {
return Err(Error::NoReadableInputs);
}
Ok(valid)
}
pub fn extract_tar_to_writer<R: std::io::Read, W: std::io::Write>(
archive: &mut tar::Archive<R>,
writer: &mut W,
opts: &DecompressOpts<'_>,
) -> Result<()> {
for entry in archive.entries()? {
let mut entry = entry?;
let orig_path = entry.path()?;
let orig_path = Utf8PathBuf::try_from(orig_path.into_owned())
.map_err(|e| Error::InvalidUtf8Path(e.into_path_buf().display().to_string()))?;
safe_entry_path(orig_path.as_str())?;
if !should_extract(orig_path.as_str(), &opts.includes, &opts.excludes) {
continue;
}
let entry_mtime = entry.header().mtime().unwrap_or(0) as i64;
if !passes_time_filter(entry_mtime, opts.newer_than, opts.older_than) {
continue;
}
if entry.header().entry_type().is_dir() {
continue;
}
let stripped = match strip_components(&orig_path, opts.strip_components) {
Some(p) => p,
None => continue,
};
let after_no_dir = if opts.no_directory {
match stripped.file_name() {
Some(name) => Utf8PathBuf::from(name),
None => continue,
}
} else {
stripped
};
let display_path =
match apply_path_rewrites(after_no_dir, &opts.renames, opts.prefix.as_deref())? {
p if p.as_str().is_empty() => continue,
p => p,
};
opts.progress.set_entry(display_path.as_str());
let written = std::io::copy(&mut entry, writer)?;
opts.progress.inc(written);
}
Ok(())
}
pub fn verify_tar_entries<R: std::io::Read>(
archive: &mut tar::Archive<R>,
progress: &dyn ProgressReport,
) -> Result<()> {
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
let path = Utf8PathBuf::try_from(path.into_owned())
.map_err(|e| Error::InvalidUtf8Path(e.into_path_buf().display().to_string()))?;
progress.set_entry(path.as_str());
let written = std::io::copy(&mut entry, &mut std::io::sink())?;
progress.inc(written);
}
Ok(())
}
fn apply_header_overrides(header: &mut tar::Header, opts: &CompressOpts<'_>) {
if let Some(mtime) = opts.fixed_mtime {
header.set_mtime(mtime);
}
if let Some(uid) = opts.fixed_uid {
header.set_uid(uid);
}
if let Some(gid) = opts.fixed_gid {
header.set_gid(gid);
}
if let Some(mode) = opts.fixed_mode {
header.set_mode(mode);
}
}
fn has_header_overrides(opts: &CompressOpts<'_>) -> bool {
opts.fixed_mtime.is_some()
|| opts.fixed_uid.is_some()
|| opts.fixed_gid.is_some()
|| opts.fixed_mode.is_some()
}
fn metadata_mode(meta: &std::fs::Metadata) -> u32 {
#[cfg(unix)]
{
meta.permissions().mode()
}
#[cfg(not(unix))]
{
if meta.is_dir() { 0o755 } else { 0o644 }
}
}
fn append_file_entry<W: std::io::Write>(
builder: &mut tar::Builder<W>,
fs_path: &Utf8Path,
archive_name: &str,
opts: &CompressOpts<'_>,
) -> Result<()> {
if has_header_overrides(opts) {
let meta = input_metadata(fs_path, opts.follow_symlinks)?;
let mut header = tar::Header::new_gnu();
header.set_metadata_in_mode(&meta, tar::HeaderMode::Deterministic);
header.set_mode(metadata_mode(&meta));
if opts.fixed_mtime.is_none() {
let mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
header.set_mtime(mtime);
}
apply_header_overrides(&mut header, opts);
let file_type = meta.file_type();
if file_type.is_symlink() {
let target = fs_err::read_link(fs_path)?;
builder.append_link(&mut header, archive_name, target)?;
} else if file_type.is_file() {
header.set_size(meta.len());
let mut file = fs_err::File::open(fs_path)?;
builder.append_data(&mut header, archive_name, &mut file)?;
} else {
#[cfg(unix)]
{
use std::os::unix::fs::{FileTypeExt, MetadataExt};
if file_type.is_socket() {
return Err(Error::Io(std::io::Error::other(format!(
"{fs_path}: socket can not be archived"
))));
}
if file_type.is_char_device() || file_type.is_block_device() {
let dev_id = meta.rdev();
let dev_major = ((dev_id >> 32) & 0xffff_f000) | ((dev_id >> 8) & 0x0000_0fff);
let dev_minor = ((dev_id >> 12) & 0xffff_ff00) | (dev_id & 0x0000_00ff);
header.set_device_major(dev_major as u32)?;
header.set_device_minor(dev_minor as u32)?;
}
}
builder.append_data(&mut header, archive_name, std::io::empty())?;
}
} else {
builder.append_path_with_name(fs_path, archive_name)?;
}
Ok(())
}
fn append_dir_entry<W: std::io::Write>(
builder: &mut tar::Builder<W>,
fs_path: &Utf8Path,
archive_name: &str,
opts: &CompressOpts<'_>,
) -> Result<()> {
if has_header_overrides(opts) {
let meta = input_metadata(fs_path, opts.follow_symlinks)?;
let mut header = tar::Header::new_gnu();
header.set_metadata_in_mode(&meta, tar::HeaderMode::Deterministic);
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(metadata_mode(&meta));
if opts.fixed_mtime.is_none() {
let mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
header.set_mtime(mtime);
}
apply_header_overrides(&mut header, opts);
header.set_cksum();
builder.append_data(&mut header, archive_name, std::io::empty())?;
} else {
builder.append_dir(archive_name, fs_path)?;
}
Ok(())
}
pub fn append_dir_filtered<W: std::io::Write>(
builder: &mut tar::Builder<W>,
dir: &Utf8Path,
prefix: &str,
opts: &CompressOpts<'_>,
) -> Result<()> {
if opts.no_recursion {
append_dir_entry(builder, dir, prefix, opts)?;
return Ok(());
}
walk_dir(dir, prefix, opts, &mut |entry| {
if entry.is_dir {
append_dir_entry(builder, &entry.fs_path, &entry.archive_name, opts)?;
} else {
let meta = input_metadata(&entry.fs_path, opts.follow_symlinks)?;
if !passes_time_filter(fs_mtime_secs(&meta), opts.newer_than, opts.older_than) {
return Ok(());
}
append_file_entry(builder, &entry.fs_path, &entry.archive_name, opts)?;
opts.progress.set_entry(&entry.archive_name);
opts.progress.inc(meta.len());
}
Ok(())
})
}
pub fn append_inputs<W: std::io::Write>(
builder: &mut tar::Builder<W>,
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
) -> Result<()> {
for input in inputs {
let meta = input_metadata(input, opts.follow_symlinks)?;
let name = input_base_name(input)?;
if opts.excludes.is_match(&name) {
continue;
}
if meta.is_dir() {
append_dir_filtered(builder, input, &name, opts)?;
} else {
if !passes_time_filter(fs_mtime_secs(&meta), opts.newer_than, opts.older_than) {
continue;
}
let size = meta.len();
append_file_entry(builder, input, &name, opts)?;
opts.progress.set_entry(&name);
opts.progress.inc(size);
}
}
Ok(())
}
pub fn list_tar_entries<R: std::io::Read>(
archive: &mut tar::Archive<R>,
) -> Result<Vec<crate::Entry>> {
let mut entries = Vec::new();
for entry in archive.entries()? {
let entry = entry?;
let path = entry.path()?;
let path = Utf8PathBuf::try_from(path.into_owned())
.map_err(|e| Error::InvalidUtf8Path(e.into_path_buf().display().to_string()))?;
let link_target = match entry.header().entry_type() {
tar::EntryType::Symlink | tar::EntryType::Link => entry
.link_name()?
.map(|t| t.to_string_lossy().into_owned()),
_ => None,
};
let header = entry.header();
entries.push(crate::Entry {
path,
size: header.size()?,
mtime: header.mtime()?,
mode: header.mode()?,
is_dir: header.entry_type().is_dir(),
link_target,
});
}
Ok(entries)
}
pub fn read_prefix<R: std::io::Read>(reader: &mut R, max: usize) -> std::io::Result<Vec<u8>> {
let mut buf = vec![0u8; max];
let mut filled = 0;
while filled < max {
let n = reader.read(&mut buf[filled..])?;
if n == 0 {
break;
}
filled += n;
}
buf.truncate(filled);
Ok(buf)
}
pub struct CountingReader<R> {
inner: R,
count: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl<R> CountingReader<R> {
pub fn new(inner: R, count: std::sync::Arc<std::sync::atomic::AtomicU64>) -> Self {
Self { inner, count }
}
}
impl<R: std::io::Read> std::io::Read for CountingReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;
self.count
.fetch_add(n as u64, std::sync::atomic::Ordering::Relaxed);
Ok(n)
}
}
pub fn count_tar_entries<R: std::io::Read>(archive: &mut tar::Archive<R>) -> Result<(usize, u64)> {
let mut entry_count: usize = 0;
let mut total_uncompressed: u64 = 0;
for entry in archive.entries()? {
let entry = entry?;
total_uncompressed = total_uncompressed.saturating_add(entry.header().size()?);
entry_count = entry_count.saturating_add(1);
}
Ok((entry_count, total_uncompressed))
}
#[cfg(unix)]
fn process_umask() -> u32 {
use std::sync::OnceLock;
static UMASK: OnceLock<u32> = OnceLock::new();
*UMASK.get_or_init(|| {
let mask = unsafe { libc::umask(0) };
unsafe { libc::umask(mask) };
mask as u32
})
}
pub fn unpack_tar_filtered<R: std::io::Read>(
archive: &mut tar::Archive<R>,
output: &Utf8Path,
opts: &DecompressOpts<'_>,
) -> Result<()> {
archive.set_preserve_permissions(opts.preserve_permissions);
archive.set_preserve_ownerships(opts.same_owner);
let mut deferred_dirs: Vec<DeferredDir> = Vec::new();
let walked = unpack_tar_entries(archive, output, opts, &mut deferred_dirs);
deferred_dirs.sort_by(|a, b| b.dest.as_str().cmp(a.dest.as_str()));
let mut flush_err: Option<Error> = None;
for dir in &deferred_dirs {
if let Err(e) = apply_deferred_dir(dir, opts)
&& flush_err.is_none()
{
flush_err = Some(e);
}
}
walked?;
match flush_err {
Some(e) => Err(e),
None => Ok(()),
}
}
fn unpack_tar_entries<R: std::io::Read>(
archive: &mut tar::Archive<R>,
output: &Utf8Path,
opts: &DecompressOpts<'_>,
deferred_dirs: &mut Vec<DeferredDir>,
) -> Result<()> {
let mut written: std::collections::HashSet<Utf8PathBuf> =
std::collections::HashSet::new();
for entry in archive.entries()? {
let mut entry = entry?;
let orig_path = entry.path()?;
let orig_path = Utf8PathBuf::try_from(orig_path.into_owned())
.map_err(|e| Error::InvalidUtf8Path(e.into_path_buf().display().to_string()))?;
safe_entry_path(orig_path.as_str())?;
let entry_type = entry.header().entry_type();
if matches!(entry_type, tar::EntryType::Symlink | tar::EntryType::Link)
&& let Some(target) = entry.link_name()?
&& (target.is_absolute()
|| target
.components()
.any(|c| matches!(c, std::path::Component::ParentDir)))
{
return Err(Error::PathTraversal(format!(
"{orig_path} -> {}",
target.to_string_lossy()
)));
}
if !should_extract(orig_path.as_str(), &opts.includes, &opts.excludes) {
continue;
}
let entry_mtime = entry.header().mtime().unwrap_or(0) as i64;
if !passes_time_filter(entry_mtime, opts.newer_than, opts.older_than) {
continue;
}
let is_dir = entry_type.is_dir();
if opts.no_directory && is_dir {
continue;
}
let dest_path = match resolve_entry_path(&orig_path, opts)? {
Some(p) => p,
None => continue,
};
let dest = if dest_path.as_str() == "." {
output.to_owned()
} else {
output.join(&dest_path)
};
if let Some(parent) = dest.parent()
&& !parent.as_str().is_empty()
{
fs_err::create_dir_all(parent)?;
}
if !is_dir && !written.contains(&dest) && fs_err::symlink_metadata(&dest).is_ok() {
if let Some(ref suffix) = opts.backup_suffix {
let backup = Utf8PathBuf::from(format!("{dest}{suffix}"));
fs_err::rename(&dest, &backup)?;
} else if opts.keep_newer {
let entry_mtime = entry.header().mtime().unwrap_or(0);
if is_existing_newer(&dest, entry_mtime)? {
continue;
}
} else if opts.no_overwrite {
continue;
} else if !opts.force {
return Err(Error::FileExists(dest));
}
}
#[cfg(unix)]
if !opts.preserve_permissions {
entry.set_mask(process_umask());
}
opts.progress.set_entry(dest_path.as_str());
let size = entry.header().size().unwrap_or(0);
if is_dir {
fs_err::create_dir_all(&dest)?;
let header = entry.header();
deferred_dirs.push(DeferredDir {
dest,
mode: header.mode().ok(),
mtime: header.mtime().ok(),
uid: header.uid().ok(),
gid: header.gid().ok(),
});
} else if entry_type == tar::EntryType::Link {
unpack_hard_link(&entry, output, &dest, opts)?;
written.insert(dest);
} else {
entry.unpack(&dest)?;
written.insert(dest);
}
opts.progress.inc(size);
}
Ok(())
}
struct DeferredDir {
dest: Utf8PathBuf,
mode: Option<u32>,
mtime: Option<u64>,
uid: Option<u64>,
gid: Option<u64>,
}
fn apply_deferred_dir(dir: &DeferredDir, opts: &DecompressOpts<'_>) -> Result<()> {
match fs_err::symlink_metadata(&dir.dest) {
Ok(m) if m.file_type().is_dir() => {}
_ => return Ok(()),
}
#[cfg(unix)]
{
if opts.same_owner
&& let (Some(uid), Some(gid)) = (dir.uid, dir.gid)
{
match std::os::unix::fs::chown(&dir.dest, Some(uid as u32), Some(gid as u32)) {
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {}
other => other?,
}
}
if let Some(mode) = dir.mode {
let mode = if opts.preserve_permissions {
mode
} else {
(mode & 0o777) & !process_umask()
};
fs_err::set_permissions(
&dir.dest,
std::fs::Permissions::from_mode(mode & 0o7777),
)?;
}
}
#[cfg(not(unix))]
{
let _ = (dir.mode, dir.uid, dir.gid, opts);
}
if let Some(mtime) = dir.mtime {
let ft = filetime::FileTime::from_unix_time(mtime as i64, 0);
let _ = filetime::set_file_times(dir.dest.as_std_path(), ft, ft);
}
Ok(())
}
fn unpack_hard_link<R: std::io::Read>(
entry: &tar::Entry<'_, R>,
output: &Utf8Path,
dest: &Utf8Path,
opts: &DecompressOpts<'_>,
) -> Result<()> {
let target = entry
.link_name()?
.ok_or_else(|| Error::Io(std::io::Error::other("hard link entry has no link name")))?;
let target = Utf8PathBuf::try_from(target.into_owned())
.map_err(|e| Error::InvalidUtf8Path(e.into_path_buf().display().to_string()))?;
let resolved = resolve_entry_path(&target, opts)?.ok_or_else(|| {
Error::Io(std::io::Error::other(format!(
"hard link target `{target}` is removed by the active path rewrites"
)))
})?;
if fs_err::symlink_metadata(dest).is_ok() {
fs_err::remove_file(dest)?;
}
fs_err::hard_link(output.join(resolved), dest)?;
Ok(())
}
pub fn passes_time_filter(
mtime_secs: i64,
newer_than: Option<i64>,
older_than: Option<i64>,
) -> bool {
if let Some(after) = newer_than
&& mtime_secs <= after
{
return false;
}
if let Some(before) = older_than
&& mtime_secs >= before
{
return false;
}
true
}
fn fs_mtime_secs(meta: &std::fs::Metadata) -> i64 {
meta.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
pub fn is_existing_newer(path: &Utf8Path, entry_mtime: u64) -> Result<bool> {
let meta = match fs_err::metadata(path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
other => other?,
};
let file_mtime = meta
.modified()?
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(file_mtime >= entry_mtime)
}
pub fn build_excludes(patterns: Vec<String>, pattern_files: &[Utf8PathBuf]) -> Result<GlobSet> {
let mut all = patterns;
for path in pattern_files {
all.extend(read_patterns_from_file(path)?);
}
build_glob_set(&all)
}
fn read_lines_from_file(path: &Utf8Path) -> Result<Vec<String>> {
let file = fs_err::File::open(path)?;
let reader = std::io::BufReader::new(file);
let mut lines = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
lines.push(trimmed.to_owned());
}
Ok(lines)
}
pub fn read_patterns_from_file(path: &Utf8Path) -> Result<Vec<String>> {
read_lines_from_file(path)
}
pub fn read_paths_from_file(path: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
Ok(read_lines_from_file(path)?
.into_iter()
.map(Utf8PathBuf::from)
.collect())
}
pub fn collect_compress_paths(
inputs: &[Utf8PathBuf],
opts: &CompressOpts<'_>,
) -> Result<Vec<String>> {
let mut paths = Vec::new();
for input in inputs {
let meta = input_metadata(input, opts.follow_symlinks)?;
let name = input_base_name(input)?;
if opts.excludes.is_match(&name) {
continue;
}
if meta.is_dir() {
if opts.no_recursion {
paths.push(format!("{name}/"));
} else {
walk_dir(input, &name, opts, &mut |entry| {
if entry.is_dir {
paths.push(format!("{}/", entry.archive_name));
} else {
let entry_meta = input_metadata(&entry.fs_path, opts.follow_symlinks)?;
if !passes_time_filter(
fs_mtime_secs(&entry_meta),
opts.newer_than,
opts.older_than,
) {
return Ok(());
}
paths.push(entry.archive_name);
}
Ok(())
})?;
}
} else if passes_time_filter(fs_mtime_secs(&meta), opts.newer_than, opts.older_than) {
paths.push(name);
}
}
Ok(paths)
}
#[cfg(test)]
mod tests {
use camino::Utf8Path;
use super::*;
#[test]
fn strip_zero_is_identity() {
let p = Utf8Path::new("a/b/c");
assert_eq!(strip_components(p, 0), Some(p.to_owned()));
}
#[test]
fn strip_one() {
assert_eq!(
strip_components(Utf8Path::new("project/src/main.rs"), 1),
Some(Utf8PathBuf::from("src/main.rs")),
);
}
#[test]
fn strip_all_returns_none() {
assert_eq!(strip_components(Utf8Path::new("a/b"), 2), None);
}
#[test]
fn strip_more_than_depth_returns_none() {
assert_eq!(strip_components(Utf8Path::new("a"), 2), None);
}
#[test]
fn strip_dir_entry_with_trailing_slash() {
assert_eq!(
strip_components(Utf8Path::new("a/b/"), 1),
Some(Utf8PathBuf::from("b")),
);
}
#[test]
fn strip_dot_prefix() {
assert_eq!(
strip_components(Utf8Path::new("./dir/file"), 1),
Some(Utf8PathBuf::from("dir/file")),
);
}
#[test]
fn empty_patterns_never_match() {
let set = build_glob_set(&[]).ok();
assert!(set.is_some());
let set = set.map(|s| s.is_match("anything"));
assert_eq!(set, Some(false));
}
#[test]
fn star_pattern_matches_at_any_depth() {
let set = build_glob_set(&["*.log".to_owned()]).ok();
assert!(set.is_some());
let set = set.as_ref().map(|s| s.is_match("foo.log"));
assert_eq!(set, Some(true));
let set2 = build_glob_set(&["*.log".to_owned()]).ok();
let set2 = set2.as_ref().map(|s| s.is_match("dir/foo.log"));
assert_eq!(set2, Some(true));
}
#[test]
fn directory_name_excludes_children() {
let set = build_glob_set(&["node_modules".to_owned()]).ok();
assert!(set.is_some());
let s = set.as_ref();
assert_eq!(s.map(|s| s.is_match("node_modules")), Some(true));
assert_eq!(
s.map(|s| s.is_match("node_modules/package.json")),
Some(true),
);
assert_eq!(s.map(|s| s.is_match("src/node_modules/foo")), Some(true),);
assert_eq!(s.map(|s| s.is_match("src/other")), Some(false));
}
#[test]
fn safe_entry_path_accepts_plain_relative() {
assert!(safe_entry_path("a/b/c.txt").is_ok());
assert!(safe_entry_path("file").is_ok());
assert!(safe_entry_path("deep/nested/dir/x.log").is_ok());
}
#[test]
fn safe_entry_path_rejects_absolute() {
assert!(safe_entry_path("/etc/passwd").is_err());
}
#[test]
fn safe_entry_path_rejects_parent_traversal() {
assert!(safe_entry_path("../etc/passwd").is_err());
assert!(safe_entry_path("a/../b").is_err());
assert!(safe_entry_path("a/b/..").is_err());
}
#[test]
fn safe_entry_path_accepts_current_dir_prefix() {
assert!(safe_entry_path("./foo").is_ok());
}
#[test]
fn safe_link_target_accepts_relative_intra_archive() {
assert!(safe_link_target("bin/sh", "busybox").is_ok());
assert!(safe_link_target("a/link", "b/target").is_ok());
}
#[test]
fn safe_link_target_rejects_absolute() {
assert!(safe_link_target("link", "/etc/passwd").is_err());
}
#[test]
fn safe_link_target_rejects_parent_traversal() {
assert!(safe_link_target("link", "../etc/passwd").is_err());
assert!(safe_link_target("a/link", "../../etc").is_err());
}
#[test]
fn should_extract_excludes_take_precedence_over_includes() {
let includes = build_glob_set(&["*.txt".to_owned()]).unwrap_or(GlobSet::empty());
let excludes = build_glob_set(&["secret.txt".to_owned()]).unwrap_or(GlobSet::empty());
assert!(should_extract("notes.txt", &includes, &excludes));
assert!(!should_extract("secret.txt", &includes, &excludes));
}
#[test]
fn should_extract_empty_includes_means_include_all() {
let includes = GlobSet::empty();
let excludes = build_glob_set(&["*.log".to_owned()]).unwrap_or(GlobSet::empty());
assert!(should_extract("any.txt", &includes, &excludes));
assert!(!should_extract("debug.log", &includes, &excludes));
}
#[test]
fn should_extract_non_matching_include_filters_out() {
let includes = build_glob_set(&["*.txt".to_owned()]).unwrap_or(GlobSet::empty());
let excludes = GlobSet::empty();
assert!(!should_extract("something.bin", &includes, &excludes));
}
}