#[cfg(feature = "memmap")]
use crate::command::core::{run_entries, run_entries_stoppable};
use crate::ext::*;
#[cfg(any(unix, windows))]
use crate::utils::fs::lchown;
use crate::{
cli::{DateTime, FileArgsCompat, MissingTimePolicy, PasswordArgs},
command::{
Command, ask_password,
core::{
AclStrategy, FflagsStrategy, KeepOptions, MacMetadataStrategy, ModeStrategy,
OwnerOptions, OwnerStrategy, PathFilter, PathTransformers, PathnameEditor,
PermissionStrategyResolver, ProcessAction, SafeWriter, TimeFilterResolver, TimeFilters,
TimestampStrategy, TimestampStrategyResolver, Umask, XattrStrategy, apply_chroot,
collect_split_archives,
path_lock::OrderedPathLocks,
re::{bsd::SubstitutionRule, gnu::TransformRule},
read_paths, run_process_archive, run_process_archive_stoppable,
},
},
utils::{
self, BsdGlobMatcher, PathWithCwd, VCS_FILES,
fmt::DurationDisplay,
fs::{Group, User},
},
};
use anyhow::Context;
use clap::{ArgGroup, Parser, ValueHint};
use pna::{DataKind, EntryName, EntryReference, NormalEntry, Permission, ReadOptions, prelude::*};
#[cfg(target_os = "macos")]
use std::os::macos::fs::FileTimesExt;
#[cfg(windows)]
use std::os::windows::fs::FileTimesExt;
use std::{
borrow::Cow,
env, fs,
io::{self, prelude::*},
path::{Component, Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Instant,
};
#[derive(Parser, Clone, Debug)]
#[command(
group(
ArgGroup::new("from-input")
.args(["files_from", "exclude_from"])
.multiple(true)
),
group(ArgGroup::new("null-requires").arg("null").requires("from-input")),
group(ArgGroup::new("keep-timestamp-flag").args(["keep_timestamp", "no_keep_timestamp"])),
group(ArgGroup::new("keep-permission-flag").args(["keep_permission", "no_keep_permission"])),
group(ArgGroup::new("keep-xattr-flag").args(["keep_xattr", "no_keep_xattr"])),
group(ArgGroup::new("keep-acl-flag").args(["keep_acl", "no_keep_acl"])),
group(ArgGroup::new("path-transform").args(["substitutions", "transforms"])),
group(ArgGroup::new("owner-flag").args(["same_owner", "no_same_owner"])),
group(ArgGroup::new("user-flag").args(["numeric_owner", "uname"])),
group(ArgGroup::new("group-flag").args(["numeric_owner", "gname"])),
group(ArgGroup::new("ctime-older-than-source").args(["older_ctime", "older_ctime_than"])),
group(ArgGroup::new("ctime-newer-than-source").args(["newer_ctime", "newer_ctime_than"])),
group(ArgGroup::new("mtime-older-than-source").args(["older_mtime", "older_mtime_than"])),
group(ArgGroup::new("mtime-newer-than-source").args(["newer_mtime", "newer_mtime_than"])),
group(ArgGroup::new("ctime-filter").args(["older_ctime", "older_ctime_than", "newer_ctime", "newer_ctime_than"]).multiple(true)),
group(ArgGroup::new("mtime-filter").args(["older_mtime", "older_mtime_than", "newer_mtime", "newer_mtime_than"]).multiple(true)),
group(
ArgGroup::new("overwrite-flag")
.args(["overwrite", "no_overwrite", "keep_newer_files", "keep_old_files"])
),
group(ArgGroup::new("safe-writes-flag").args(["safe_writes", "no_safe_writes"])),
group(ArgGroup::new("unsafe-links-flag").args(["allow_unsafe_links", "no_allow_unsafe_links"])),
)]
pub(crate) struct ExtractCommand {
#[arg(long, help = "Overwrite file")]
overwrite: bool,
#[arg(
long,
help = "Do not overwrite files. This is the inverse option of --overwrite"
)]
no_overwrite: bool,
#[arg(
long,
requires = "unstable",
help_heading = "Unstable Options",
help = "Skip extracting files if a newer version already exists"
)]
keep_newer_files: bool,
#[arg(
long,
requires = "unstable",
help_heading = "Unstable Options",
help = "Skip extracting files if they already exist"
)]
keep_old_files: bool,
#[arg(long, value_name = "DIRECTORY", help = "Output directory of extracted files", value_hint = ValueHint::DirPath)]
out_dir: Option<PathBuf>,
#[command(flatten)]
pub(crate) password: PasswordArgs,
#[arg(
long,
visible_alias = "preserve-timestamps",
help = "Restore the timestamp of the files"
)]
pub(crate) keep_timestamp: bool,
#[arg(
long,
visible_alias = "no-preserve-timestamps",
help = "Do not restore timestamp of files. This is the inverse option of --preserve-timestamps"
)]
pub(crate) no_keep_timestamp: bool,
#[arg(
long,
value_name = "DATETIME",
help = "Overrides the modification time"
)]
mtime: Option<DateTime>,
#[arg(
long,
requires = "mtime",
help = "Clamp the modification time of the entries to the specified time by --mtime"
)]
clamp_mtime: bool,
#[arg(long, value_name = "DATETIME", help = "Overrides the creation time")]
ctime: Option<DateTime>,
#[arg(
long,
requires = "ctime",
help = "Clamp the creation time of the entries to the specified time by --ctime"
)]
clamp_ctime: bool,
#[arg(long, value_name = "DATETIME", help = "Overrides the access time")]
atime: Option<DateTime>,
#[arg(
long,
requires = "atime",
help = "Clamp the access time of the entries to the specified time by --atime"
)]
clamp_atime: bool,
#[arg(
long,
visible_alias = "preserve-permissions",
help = "Restore the permissions of the files"
)]
#[cfg_attr(windows, arg(requires = "unstable", help_heading = "Unstable Options"))]
keep_permission: bool,
#[arg(
long,
visible_alias = "no-preserve-permissions",
help = "Do not restore permissions of files. This is the inverse option of --preserve-permissions"
)]
#[cfg_attr(windows, arg(requires = "unstable", help_heading = "Unstable Options"))]
no_keep_permission: bool,
#[arg(
long,
visible_alias = "preserve-xattrs",
help = "Restore the extended attributes of the files"
)]
pub(crate) keep_xattr: bool,
#[arg(
long,
visible_alias = "no-preserve-xattrs",
help = "Do not restore extended attributes of files. This is the inverse option of --preserve-xattrs"
)]
pub(crate) no_keep_xattr: bool,
#[arg(
long,
visible_alias = "preserve-acls",
requires = "unstable",
help_heading = "Unstable Options",
help = "Restore ACLs"
)]
keep_acl: bool,
#[arg(
long,
visible_alias = "no-preserve-acls",
requires = "unstable",
help_heading = "Unstable Options",
help = "Do not restore ACLs. This is the inverse option of --keep-acl"
)]
no_keep_acl: bool,
#[arg(long, value_name = "NAME", help = "Restore user from given name")]
uname: Option<String>,
#[arg(long, value_name = "NAME", help = "Restore group from given name")]
gname: Option<String>,
#[arg(
long,
value_name = "ID",
help = "Overrides the user id in the archive; the user name in the archive will be ignored"
)]
uid: Option<u32>,
#[arg(
long,
value_name = "ID",
help = "Overrides the group id in the archive; the group name in the archive will be ignored"
)]
gid: Option<u32>,
#[arg(
long,
help = "This is equivalent to --uname \"\" --gname \"\". It causes user and group names in the archive to be ignored in favor of the numeric user and group ids."
)]
numeric_owner: bool,
#[arg(
long,
value_name = "DATETIME",
requires = "unstable",
help_heading = "Unstable Options",
help = "Only include files and directories older than the specified date. This compares ctime entries."
)]
older_ctime: Option<DateTime>,
#[arg(
long,
value_name = "DATETIME",
requires = "unstable",
help_heading = "Unstable Options",
help = "Only include files and directories older than the specified date. This compares mtime entries."
)]
older_mtime: Option<DateTime>,
#[arg(
long,
value_name = "DATETIME",
requires = "unstable",
help_heading = "Unstable Options",
help = "Only include files and directories newer than the specified date. This compares ctime entries."
)]
newer_ctime: Option<DateTime>,
#[arg(
long,
value_name = "DATETIME",
requires = "unstable",
help_heading = "Unstable Options",
help = "Only include files and directories newer than the specified date. This compares mtime entries."
)]
newer_mtime: Option<DateTime>,
#[arg(
long,
value_name = "file",
requires = "unstable",
visible_alias = "newer-than",
help_heading = "Unstable Options",
help = "Only include files and directories newer than the specified file. This compares ctime entries."
)]
newer_ctime_than: Option<PathBuf>,
#[arg(
long,
value_name = "file",
requires = "unstable",
help_heading = "Unstable Options",
help = "Only include files and directories newer than the specified file. This compares mtime entries."
)]
newer_mtime_than: Option<PathBuf>,
#[arg(
long,
value_name = "file",
requires = "unstable",
visible_alias = "older-than",
help_heading = "Unstable Options",
help = "Only include files and directories older than the specified file. This compares ctime entries."
)]
older_ctime_than: Option<PathBuf>,
#[arg(
long,
value_name = "file",
requires = "unstable",
help_heading = "Unstable Options",
help = "Only include files and directories older than the specified file. This compares mtime entries."
)]
older_mtime_than: Option<PathBuf>,
#[arg(
long,
requires_all = ["unstable", "ctime-filter"],
help = "Behavior for entries without ctime when time filtering (unstable). Values: include, exclude, now, epoch, or a datetime. [default: include]"
)]
missing_ctime: Option<MissingTimePolicy>,
#[arg(
long,
requires_all = ["unstable", "mtime-filter"],
help = "Behavior for entries without mtime when time filtering (unstable). Values: include, exclude, now, epoch, or a datetime. [default: include]"
)]
missing_mtime: Option<MissingTimePolicy>,
#[arg(
long,
value_name = "PATTERN",
requires = "unstable",
help_heading = "Unstable Options",
help = "Process only files or directories that match the specified pattern. Note that exclusions specified with --exclude take precedence over inclusions"
)]
include: Vec<String>,
#[arg(
long,
value_name = "PATTERN",
requires = "unstable",
help_heading = "Unstable Options",
help = "Exclude path glob",
value_hint = ValueHint::AnyPath
)]
exclude: Vec<String>,
#[arg(
long,
value_name = "FILE",
requires = "unstable",
help_heading = "Unstable Options",
help = "Read exclude files from given path",
value_hint = ValueHint::FilePath
)]
exclude_from: Option<PathBuf>,
#[arg(
long,
requires = "unstable",
help_heading = "Unstable Options",
help = "Exclude files or directories internally used by version control systems (`Arch`, `Bazaar`, `CVS`, `Darcs`, `Mercurial`, `RCS`, `SCCS`, `SVN`, `git`)"
)]
exclude_vcs: bool,
#[arg(
long,
value_name = "FILE",
requires = "unstable",
help_heading = "Unstable Options",
help = "Read extraction patterns from given path",
value_hint = ValueHint::FilePath
)]
files_from: Option<PathBuf>,
#[arg(
long,
help = "Filenames or patterns are separated by null characters, not by newlines"
)]
null: bool,
#[arg(
long,
value_name = "N",
help = "Remove the specified number of leading path elements. Path names with fewer elements will be silently skipped"
)]
strip_components: Option<usize>,
#[arg(
short = 's',
value_name = "PATTERN",
requires = "unstable",
help_heading = "Unstable Options",
help = "Modify file or archive member names according to pattern that like BSD tar -s option"
)]
substitutions: Option<Vec<SubstitutionRule>>,
#[arg(
long = "transform",
visible_alias = "xform",
value_name = "PATTERN",
requires = "unstable",
help_heading = "Unstable Options",
help = "Modify file or archive member names according to pattern that like GNU tar -transform option"
)]
transforms: Option<Vec<TransformRule>>,
#[arg(
long,
help = "Try extracting files with the same ownership as exists in the archive"
)]
same_owner: bool,
#[arg(long, help = "Extract files as yourself")]
no_same_owner: bool,
#[arg(
short = 'C',
long = "cd",
visible_aliases = ["directory"],
value_name = "DIRECTORY",
help = "Change directories after opening the archive but before extracting entries from the archive",
value_hint = ValueHint::DirPath
)]
working_dir: Option<PathBuf>,
#[arg(
long,
help = "chroot() to the current directory after processing any --cd options and before extracting any files (requires root privileges)"
)]
chroot: bool,
#[arg(
long,
help = "Allow extracting symbolic links and hard links that contain root or parent paths"
)]
allow_unsafe_links: bool,
#[arg(
long,
help = "Do not allow extracting symbolic links and hard links that contain root or parent paths (default)"
)]
no_allow_unsafe_links: bool,
#[arg(
long,
requires = "unstable",
help_heading = "Unstable Options",
help = "Extract files atomically via temp file and rename"
)]
safe_writes: bool,
#[arg(
long,
requires = "unstable",
help_heading = "Unstable Options",
help = "Disable atomic extraction. This is the inverse option of --safe-writes"
)]
no_safe_writes: bool,
#[command(flatten)]
pub(crate) file: FileArgsCompat,
}
impl Command for ExtractCommand {
#[inline]
fn execute(self, _ctx: &crate::cli::GlobalContext) -> anyhow::Result<()> {
extract_archive(self)
}
}
#[hooq::hooq(anyhow)]
fn extract_archive(args: ExtractCommand) -> anyhow::Result<()> {
let password = ask_password(args.password).with_context(|| "reading password")?;
let start = Instant::now();
let archive = args.file.archive();
log::info!("Extract archive {}", PathWithCwd::new(&archive));
let archives = collect_split_archives(&archive)
.with_context(|| format!("opening archive '{}'", PathWithCwd::new(&archive)))?;
let mut exclude = args.exclude;
if let Some(p) = args.exclude_from {
exclude.extend(
read_paths(&p, args.null).with_context(|| {
format!("reading exclude patterns from {}", PathWithCwd::new(&p))
})?,
);
}
let vcs_patterns = args
.exclude_vcs
.then(|| VCS_FILES.iter().copied())
.into_iter()
.flatten();
let filter = PathFilter::new(
args.include.iter().map(|s| s.as_str()),
exclude.iter().map(|s| s.as_str()).chain(vcs_patterns),
);
let mut files = args.file.files();
if let Some(path) = &args.files_from {
files.extend(
read_paths(path, args.null)
.with_context(|| format!("reading file list from {}", PathWithCwd::new(path)))?,
);
}
let time_filters = TimeFilterResolver {
newer_ctime_than: args.newer_ctime_than.as_deref(),
older_ctime_than: args.older_ctime_than.as_deref(),
newer_ctime: args.newer_ctime.map(|it| it.to_system_time()),
older_ctime: args.older_ctime.map(|it| it.to_system_time()),
newer_mtime_than: args.newer_mtime_than.as_deref(),
older_mtime_than: args.older_mtime_than.as_deref(),
newer_mtime: args.newer_mtime.map(|it| it.to_system_time()),
older_mtime: args.older_mtime.map(|it| it.to_system_time()),
missing_ctime: args.missing_ctime.unwrap_or(MissingTimePolicy::Include),
missing_mtime: args.missing_mtime.unwrap_or(MissingTimePolicy::Include),
}
.resolve()?;
let overwrite_strategy = OverwriteStrategy::from_flags(
args.overwrite,
args.no_overwrite,
args.keep_newer_files,
args.keep_old_files,
OverwriteStrategy::Never,
);
let (mode_strategy, owner_strategy) = PermissionStrategyResolver {
keep_permission: args.keep_permission,
no_keep_permission: args.no_keep_permission,
same_owner: !args.no_same_owner,
uname: args.uname,
gname: args.gname,
uid: args.uid,
gid: args.gid,
numeric_owner: args.numeric_owner,
}
.resolve();
let keep_options = KeepOptions {
timestamp_strategy: TimestampStrategyResolver {
keep_timestamp: args.keep_timestamp,
no_keep_timestamp: args.no_keep_timestamp,
default_preserve: false,
mtime: args.mtime.map(|it| it.to_system_time()),
clamp_mtime: args.clamp_mtime,
ctime: args.ctime.map(|it| it.to_system_time()),
clamp_ctime: args.clamp_ctime,
atime: args.atime.map(|it| it.to_system_time()),
clamp_atime: args.clamp_atime,
}
.resolve(),
mode_strategy,
owner_strategy,
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr, false),
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
fflags_strategy: FflagsStrategy::Never,
mac_metadata_strategy: MacMetadataStrategy::Never,
};
let output_options = OutputOption {
overwrite_strategy,
allow_unsafe_links: args.allow_unsafe_links,
out_dir: args.out_dir,
to_stdout: false,
filter,
keep_options,
pathname_editor: PathnameEditor::new(
args.strip_components,
PathTransformers::new(args.substitutions, args.transforms),
false,
false,
),
ordered_path_locks: Arc::new(OrderedPathLocks::default()),
unlink_first: false,
time_filters,
safe_writes: args.safe_writes && !args.no_safe_writes,
verbose: false,
absolute_paths: false,
warned_lead_slash: Arc::new(AtomicBool::new(false)),
};
if let Some(working_dir) = args.working_dir {
env::set_current_dir(&working_dir)
.with_context(|| format!("changing directory to {}", PathWithCwd::new(&working_dir)))?;
}
apply_chroot(args.chroot)?;
#[cfg(not(feature = "memmap"))]
run_extract_archive_reader(
archives
.into_iter()
.map(|it| io::BufReader::with_capacity(64 * 1024, it)),
files,
|| password.as_deref(),
output_options,
true,
false,
false,
)
.with_context(|| format!("extracting entries from '{}'", PathWithCwd::new(&archive)))?;
#[cfg(feature = "memmap")]
let mmaps = archives
.into_iter()
.map(utils::mmap::Mmap::try_from)
.collect::<io::Result<Vec<_>>>()
.with_context(|| format!("memory-mapping archive '{}'", PathWithCwd::new(&archive)))?;
#[cfg(feature = "memmap")]
let archives = mmaps.iter().map(|m| m.as_ref());
#[cfg(feature = "memmap")]
run_extract_archive(
archives,
files,
|| password.as_deref(),
output_options,
true,
false,
)
.with_context(|| format!("extracting entries from '{}'", PathWithCwd::new(&archive)))?;
log::info!(
"Successfully extracted an archive in {}",
DurationDisplay(start.elapsed())
);
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum OverwriteStrategy {
Never,
Always,
KeepNewer,
KeepOlder,
}
impl OverwriteStrategy {
pub(crate) const fn from_flags(
overwrite: bool,
no_overwrite: bool,
keep_newer: bool,
keep_older: bool,
default_strategy: Self,
) -> Self {
if overwrite {
Self::Always
} else if no_overwrite {
Self::Never
} else if keep_newer {
Self::KeepNewer
} else if keep_older {
Self::KeepOlder
} else {
default_strategy
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct OutputOption<'a> {
pub(crate) overwrite_strategy: OverwriteStrategy,
pub(crate) allow_unsafe_links: bool,
pub(crate) out_dir: Option<PathBuf>,
pub(crate) to_stdout: bool,
pub(crate) filter: PathFilter<'a>,
pub(crate) keep_options: KeepOptions,
pub(crate) pathname_editor: PathnameEditor,
pub(crate) ordered_path_locks: Arc<OrderedPathLocks>,
pub(crate) unlink_first: bool,
pub(crate) time_filters: TimeFilters,
pub(crate) safe_writes: bool,
pub(crate) verbose: bool,
pub(crate) absolute_paths: bool,
pub(crate) warned_lead_slash: Arc<AtomicBool>,
}
pub(crate) fn run_extract_archive_reader<'a, 'p, Provider>(
reader: impl IntoIterator<Item = impl Read> + Send,
files: Vec<String>,
mut password_provider: Provider,
args: OutputOption<'a>,
no_recursive: bool,
fast_read: bool,
allow_concatenated_archives: bool,
) -> anyhow::Result<()>
where
Provider: FnMut() -> Option<&'p [u8]> + Send,
{
let password = password_provider();
let patterns = files;
let mut globs =
BsdGlobMatcher::new(patterns.iter().map(|it| it.as_str())).with_no_recursive(no_recursive);
let mut link_entries = Vec::new();
#[cfg(not(target_family = "wasm"))]
{
let (tx, rx) = std::sync::mpsc::channel();
rayon::scope_fifo(|s| -> anyhow::Result<()> {
if fast_read && !globs.is_empty() {
run_process_archive_stoppable(
reader,
password_provider,
|entry| {
let item = entry.map_err(|e| {
io::Error::new(e.kind(), format!("reading archive entry: {e}"))
})?;
let item_path = item.name().to_string();
let name =
match filter_entry_fast_read(&item, &item_path, &mut globs, &args) {
FastReadFilterAction::Skip(action) => return Ok(action),
FastReadFilterAction::Accept(name) => name,
};
if args.verbose {
eprintln!("x {}", name);
}
if args.to_stdout {
extract_entry_to_stdout(&item, password)?;
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
if matches!(
item.header().data_kind(),
DataKind::SymbolicLink | DataKind::HardLink
) {
link_entries.push((name, item));
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
if item.header().data_kind() == DataKind::Directory {
extract_entry(item, &name, password, &args).map_err(|e| {
io::Error::new(e.kind(), format!("extracting {item_path}: {e}"))
})?;
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
let path = build_output_path(args.out_dir.as_deref(), name.as_path());
let ticket = args.ordered_path_locks.register(&path);
let tx = tx.clone();
let args = args.clone();
let all_matched = globs.all_matched();
s.spawn_fifo(move |_| {
let _guard = ticket.wait_for_turn();
tx.send(
extract_entry(item, &name, password, &args)
.with_context(|| format!("extracting {}", item_path)),
)
.unwrap_or_else(|_| unreachable!("receiver is held by scope owner"));
});
if all_matched {
return Ok(ProcessAction::Stop);
}
Ok(ProcessAction::Continue)
},
allow_concatenated_archives,
)
.with_context(|| "streaming archive entries")?;
} else {
run_process_archive(
reader,
password_provider,
|entry| {
let item = entry.map_err(|e| {
io::Error::new(e.kind(), format!("reading archive entry: {e}"))
})?;
let Some(name) = filter_entry(&item, &mut globs, &args) else {
return Ok(());
};
if args.verbose {
eprintln!("x {}", name);
}
if args.to_stdout {
return extract_entry_to_stdout(&item, password);
}
if matches!(
item.header().data_kind(),
DataKind::SymbolicLink | DataKind::HardLink
) {
link_entries.push((name, item));
return Ok(());
}
if item.header().data_kind() == DataKind::Directory {
let item_path = item.name().to_string();
extract_entry(item, &name, password, &args).map_err(|e| {
io::Error::new(e.kind(), format!("extracting {item_path}: {e}"))
})?;
return Ok(());
}
let path = build_output_path(args.out_dir.as_deref(), name.as_path());
let ticket = args.ordered_path_locks.register(&path);
let item_path = item.name().to_string();
let tx = tx.clone();
let args = args.clone();
s.spawn_fifo(move |_| {
let _guard = ticket.wait_for_turn();
tx.send(
extract_entry(item, &name, password, &args)
.with_context(|| format!("extracting {}", item_path)),
)
.unwrap_or_else(|_| unreachable!("receiver is held by scope owner"));
});
Ok(())
},
allow_concatenated_archives,
)
.with_context(|| "streaming archive entries")?;
}
drop(tx);
Ok(())
})?;
for result in rx {
result?;
}
}
#[cfg(target_family = "wasm")]
{
if fast_read && !globs.is_empty() {
run_process_archive_stoppable(
reader,
password_provider,
|entry| {
let item = entry.map_err(|e| {
io::Error::new(e.kind(), format!("reading archive entry: {e}"))
})?;
let item_path = item.name().to_string();
let name = match filter_entry_fast_read(&item, &item_path, &mut globs, &args) {
FastReadFilterAction::Skip(action) => return Ok(action),
FastReadFilterAction::Accept(name) => name,
};
if args.verbose {
eprintln!("x {}", name);
}
if args.to_stdout {
extract_entry_to_stdout(&item, password)?;
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
if matches!(
item.header().data_kind(),
DataKind::SymbolicLink | DataKind::HardLink
) {
link_entries.push((name, item));
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
extract_entry(item, &name, password, &args).map_err(|e| {
io::Error::new(e.kind(), format!("extracting {}: {e}", item_path))
})?;
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
Ok(ProcessAction::Continue)
},
allow_concatenated_archives,
)
.with_context(|| "streaming archive entries")?;
} else {
run_process_archive(
reader,
password_provider,
|entry| {
let item = entry.map_err(|e| {
io::Error::new(e.kind(), format!("reading archive entry: {e}"))
})?;
let Some(name) = filter_entry(&item, &mut globs, &args) else {
return Ok(());
};
if args.verbose {
eprintln!("x {}", name);
}
if args.to_stdout {
return extract_entry_to_stdout(&item, password);
}
if matches!(
item.header().data_kind(),
DataKind::SymbolicLink | DataKind::HardLink
) {
link_entries.push((name, item));
return Ok(());
}
extract_entry(item, &name, password, &args).map_err(|e| {
io::Error::new(e.kind(), format!("extracting {}: {e}", name))
})?;
Ok(())
},
allow_concatenated_archives,
)
.with_context(|| "streaming archive entries")?;
}
}
for (name, item) in link_entries {
extract_entry(item, &name, password, &args)
.with_context(|| format!("extracting deferred link {name}"))?;
}
globs.ensure_all_matched()?;
Ok(())
}
#[cfg(feature = "memmap")]
#[hooq::hooq(anyhow)]
pub(crate) fn run_extract_archive<'a, 'd, 'p, Provider>(
archives: impl IntoIterator<Item = &'d [u8]> + Send,
files: Vec<String>,
mut password_provider: Provider,
args: OutputOption<'a>,
no_recursive: bool,
fast_read: bool,
) -> anyhow::Result<()>
where
Provider: FnMut() -> Option<&'p [u8]> + Send,
{
let password = password_provider();
let mut globs =
BsdGlobMatcher::new(files.iter().map(|it| it.as_str())).with_no_recursive(no_recursive);
let mut link_entries: Vec<(EntryName, NormalEntry<Vec<u8>>)> = Vec::new();
let (tx, rx) = std::sync::mpsc::channel();
rayon::scope_fifo(|s| -> anyhow::Result<()> {
if fast_read && !globs.is_empty() {
#[hooq::skip_all]
run_entries_stoppable(archives, password_provider, |entry| {
let item = entry
.map_err(|e| io::Error::new(e.kind(), format!("reading archive entry: {e}")))?;
let item_path = item.name().to_string();
let name = match filter_entry_fast_read(&item, &item_path, &mut globs, &args) {
FastReadFilterAction::Skip(action) => return Ok(action),
FastReadFilterAction::Accept(name) => name,
};
if args.verbose {
eprintln!("x {}", name);
}
if args.to_stdout {
extract_entry_to_stdout(&item, password)?;
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
if matches!(
item.header().data_kind(),
DataKind::SymbolicLink | DataKind::HardLink
) {
link_entries.push((name, item.into()));
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
if item.header().data_kind() == DataKind::Directory {
extract_entry(item, &name, password, &args).map_err(|e| {
io::Error::new(e.kind(), format!("extracting {item_path}: {e}"))
})?;
if globs.all_matched() {
return Ok(ProcessAction::Stop);
}
return Ok(ProcessAction::Continue);
}
let path = build_output_path(args.out_dir.as_deref(), name.as_path());
let ticket = args.ordered_path_locks.register(&path);
let tx = tx.clone();
let args = args.clone();
let all_matched = globs.all_matched();
s.spawn_fifo(move |_| {
let _guard = ticket.wait_for_turn();
tx.send(
extract_entry(item, &name, password, &args)
.with_context(|| format!("extracting {}", item_path)),
)
.unwrap_or_else(|_| unreachable!("receiver is held by scope owner"));
});
if all_matched {
return Ok(ProcessAction::Stop);
}
Ok(ProcessAction::Continue)
})
.with_context(|| "streaming archive entries")?;
} else {
#[hooq::skip_all]
run_entries(archives, password_provider, |entry| {
let item = entry
.map_err(|e| io::Error::new(e.kind(), format!("reading archive entry: {e}")))?;
let Some(name) = filter_entry(&item, &mut globs, &args) else {
return Ok(());
};
if args.verbose {
eprintln!("x {}", name);
}
if args.to_stdout {
return extract_entry_to_stdout(&item, password);
}
if matches!(
item.header().data_kind(),
DataKind::SymbolicLink | DataKind::HardLink
) {
link_entries.push((name, item.into()));
return Ok(());
}
if item.header().data_kind() == DataKind::Directory {
let item_path = item.name().to_string();
extract_entry(item, &name, password, &args).map_err(|e| {
io::Error::new(e.kind(), format!("extracting {item_path}: {e}"))
})?;
return Ok(());
}
let path = build_output_path(args.out_dir.as_deref(), name.as_path());
let ticket = args.ordered_path_locks.register(&path);
let item_path = item.name().to_string();
let tx = tx.clone();
let args = args.clone();
s.spawn_fifo(move |_| {
let _guard = ticket.wait_for_turn();
tx.send(
extract_entry(item, &name, password, &args)
.with_context(|| format!("extracting {}", item_path)),
)
.unwrap_or_else(|_| unreachable!("receiver is held by scope owner"));
});
Ok(())
})
.with_context(|| "streaming archive entries")?;
}
drop(tx);
Ok(())
})?;
for result in rx {
result?;
}
for (name, item) in link_entries {
extract_entry(item, &name, password, &args)
.with_context(|| format!("extracting deferred link {name}"))?;
}
globs.ensure_all_matched()?;
Ok(())
}
#[inline]
fn entry_matches_time_filters<T>(item: &NormalEntry<T>, filters: &TimeFilters) -> bool
where
T: AsRef<[u8]>,
pna::RawChunk<T>: Chunk,
{
let metadata = item.metadata();
filters.matches_or_inactive(metadata.created_time(), metadata.modified_time())
}
fn filter_entry<T: AsRef<[u8]>>(
item: &NormalEntry<T>,
globs: &mut BsdGlobMatcher<'_>,
args: &OutputOption<'_>,
) -> Option<EntryName>
where
pna::RawChunk<T>: Chunk,
{
let item_name = item.name();
if !globs.is_empty() && !globs.matches(item_name) {
log::debug!("Skip: {item_name}");
return None;
}
if args.filter.excluded(item_name) {
log::debug!("Skip: {item_name}");
return None;
}
if !entry_matches_time_filters(item, &args.time_filters) {
log::debug!("Skip: {item_name}");
return None;
}
let name = args.pathname_editor.edit_entry_name(item_name.as_path());
if name.is_none() {
log::debug!("Skip: {item_name}");
}
name
}
enum FastReadFilterAction {
Accept(EntryName),
Skip(ProcessAction),
}
fn filter_entry_fast_read<T: AsRef<[u8]>>(
item: &NormalEntry<T>,
item_path: &str,
globs: &mut BsdGlobMatcher<'_>,
args: &OutputOption<'_>,
) -> FastReadFilterAction
where
pna::RawChunk<T>: Chunk,
{
if !globs.matches_any_pattern(item_path) {
log::debug!("Skip: {item_path}");
return FastReadFilterAction::Skip(ProcessAction::Continue);
}
if args.filter.excluded(item.name()) {
log::debug!("Skip: {item_path}");
return FastReadFilterAction::Skip(ProcessAction::Continue);
}
if !entry_matches_time_filters(item, &args.time_filters) {
log::debug!("Skip: {item_path}");
return if globs.all_matched() {
FastReadFilterAction::Skip(ProcessAction::Stop)
} else {
FastReadFilterAction::Skip(ProcessAction::Continue)
};
}
let Some(name) = args.pathname_editor.edit_entry_name(item.name().as_path()) else {
log::debug!("Skip: {item_path}");
return FastReadFilterAction::Skip(ProcessAction::Continue);
};
globs.mark_satisfied(item_path);
FastReadFilterAction::Accept(name)
}
#[derive(Debug, Clone, Copy)]
enum ExtractionDecision {
Proceed { remove_existing: bool },
Skip,
}
fn check_and_prepare_target<T>(
path: &Path,
entry_kind: DataKind,
item: &NormalEntry<T>,
overwrite_strategy: OverwriteStrategy,
unlink_first: bool,
secure_symlinks: bool,
) -> io::Result<ExtractionDecision>
where
T: AsRef<[u8]>,
{
let metadata = match fs::symlink_metadata(path) {
Ok(meta) => Some(meta),
Err(err) if err.kind() == io::ErrorKind::NotFound => None,
Err(err) if err.kind() == io::ErrorKind::NotADirectory => None,
Err(err) => return Err(err),
};
if let Some(existing) = &metadata {
match overwrite_strategy {
OverwriteStrategy::Never if !unlink_first => {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{} already exists", path.display()),
));
}
OverwriteStrategy::KeepOlder if !unlink_first => {
log::debug!(
"Skipped extracting {}: existing one kept by --keep-older",
path.display()
);
return Ok(ExtractionDecision::Skip);
}
OverwriteStrategy::KeepNewer if !unlink_first => {
if is_existing_newer(existing, item) {
log::debug!(
"Skipped extracting {}: newer one already exists (--keep-newer)",
path.display()
);
return Ok(ExtractionDecision::Skip);
}
}
_ => (),
}
}
let (had_existing, existing_is_dir) = metadata
.as_ref()
.map(|meta| {
let is_dir = if !secure_symlinks && meta.is_symlink() {
fs::metadata(path).is_ok_and(|m| m.is_dir())
} else {
meta.is_dir()
};
(true, is_dir)
})
.unwrap_or((false, false));
let unlink_existing =
unlink_first && had_existing && (entry_kind != DataKind::Directory || !existing_is_dir);
let should_overwrite_existing = matches!(
overwrite_strategy,
OverwriteStrategy::Always | OverwriteStrategy::KeepNewer
) && had_existing;
if unlink_existing {
utils::io::ignore_not_found(utils::fs::remove_path(path))?;
}
if let Some(parent) = path.parent() {
ensure_directory_components(parent, unlink_first, secure_symlinks)?;
}
if let Some(meta) = metadata
&& (meta.is_symlink() || (meta.is_file() && entry_kind == DataKind::Directory))
{
let follow_symlink = !secure_symlinks
&& meta.is_symlink()
&& entry_kind == DataKind::Directory
&& path.metadata().is_ok_and(|m| m.is_dir());
if !follow_symlink {
match utils::fs::remove_path(path) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) if e.kind() == io::ErrorKind::IsADirectory => {}
Err(e) => return Err(e),
}
}
}
let remove_existing = should_overwrite_existing && !unlink_existing;
Ok(ExtractionDecision::Proceed { remove_existing })
}
pub(crate) fn extract_entry<'a, T>(
item: NormalEntry<T>,
item_path: &EntryName,
password: Option<&'a [u8]>,
OutputOption {
overwrite_strategy,
allow_unsafe_links,
out_dir,
to_stdout: _,
filter: _,
keep_options,
pathname_editor,
ordered_path_locks: _,
unlink_first,
time_filters: _,
safe_writes,
verbose: _,
absolute_paths,
warned_lead_slash,
}: &OutputOption<'a>,
) -> io::Result<()>
where
T: AsRef<[u8]>,
pna::RawChunk<T>: Chunk,
{
log::debug!("Extract: {}", item.name());
let path = build_output_path(out_dir.as_deref(), item_path.as_path());
let entry_kind = item.header().data_kind();
log::debug!("start: {}", path.display());
let secure_symlinks = !absolute_paths;
let ExtractionDecision::Proceed { remove_existing } = check_and_prepare_target(
&path,
entry_kind,
&item,
*overwrite_strategy,
*unlink_first,
secure_symlinks,
)?
else {
return Ok(());
};
match entry_kind {
DataKind::File => {
if *safe_writes {
let mut safe_writer = SafeWriter::new(&path)?;
{
let mut writer =
io::BufWriter::with_capacity(64 * 1024, safe_writer.as_file_mut());
let mut reader = item.reader(ReadOptions::with_password(password))?;
io::copy(&mut reader, &mut writer)?;
writer.flush()?;
}
restore_timestamps(safe_writer.as_file_mut(), item.metadata(), keep_options)?;
safe_writer.persist()?;
} else {
if remove_existing {
utils::io::ignore_not_found(utils::fs::remove_path(&path))?;
}
let file = utils::fs::file_create(&path, remove_existing)?;
let mut writer = io::BufWriter::with_capacity(64 * 1024, file);
let mut reader = item.reader(ReadOptions::with_password(password))?;
io::copy(&mut reader, &mut writer)?;
let mut file = writer.into_inner().map_err(|e| e.into_error())?;
restore_timestamps(&mut file, item.metadata(), keep_options)?;
}
}
DataKind::Directory => {
ensure_directory_components(&path, *unlink_first, secure_symlinks)?;
}
DataKind::SymbolicLink => {
let reader = item.reader(ReadOptions::with_password(password))?;
let original = io::read_to_string(reader)?;
let original = pathname_editor.edit_symlink(original.as_ref());
if !allow_unsafe_links && is_unsafe_link(&original) {
log::warn!(
"Skipped extracting a symbolic link that contains an unsafe link. If you need to extract it, use `--allow-unsafe-links`."
);
return Ok(());
}
if *safe_writes || remove_existing {
utils::io::ignore_not_found(utils::fs::remove_path(&path))?;
}
utils::fs::symlink(original, &path)?;
}
DataKind::HardLink => {
let reader = item.reader(ReadOptions::with_password(password))?;
let original = io::read_to_string(reader)?;
let Some((original, had_root)) = pathname_editor.edit_hardlink(original.as_ref())
else {
log::warn!(
"Skipped extracting a hard link that pointed at a file which was skipped.: {}",
original
);
return Ok(());
};
if had_root && !warned_lead_slash.swap(true, Ordering::Relaxed) {
eprintln!("bsdtar: Removing leading '/' from member names");
}
if !allow_unsafe_links && is_unsafe_link(&original) {
log::warn!(
"Skipped extracting a hard link that contains an unsafe link. If you need to extract it, use `--allow-unsafe-links`."
);
return Ok(());
}
let original = if let Some(out_dir) = out_dir {
Cow::from(out_dir.join(original))
} else {
Cow::from(original.as_path())
};
if *safe_writes || remove_existing {
utils::io::ignore_not_found(utils::fs::remove_path(&path))?;
}
fs::hard_link(original, &path)?;
}
}
restore_metadata(&item, &path, keep_options)?;
log::debug!("end: {}", path.display());
Ok(())
}
#[inline]
fn build_output_path<'a>(out_dir: Option<&'a Path>, item_path: &'a Path) -> Cow<'a, Path> {
let path = if let Some(out_dir) = out_dir {
Cow::from(out_dir.join(item_path))
} else {
Cow::Borrowed(item_path)
};
if path.as_os_str().is_empty() {
Cow::Borrowed(".".as_ref())
} else {
path
}
}
#[inline]
fn restore_timestamps(
file: &mut fs::File,
metadata: &pna::Metadata,
keep_options: &KeepOptions,
) -> io::Result<()> {
if let TimestampStrategy::Preserve {
mtime,
ctime: _ctime,
atime,
} = keep_options.timestamp_strategy
{
let mut times = fs::FileTimes::new();
if let Some(accessed) = atime.resolve(metadata.accessed_time()) {
times = times.set_accessed(accessed);
}
if let Some(modified) = mtime.resolve(metadata.modified_time()) {
times = times.set_modified(modified);
}
#[cfg(any(windows, target_os = "macos"))]
if let Some(created) = _ctime.resolve(metadata.created_time()) {
times = times.set_created(created);
}
file.set_times(times)?;
}
Ok(())
}
fn restore_metadata<T>(
item: &NormalEntry<T>,
path: &Path,
keep_options: &KeepOptions,
) -> io::Result<()>
where
T: AsRef<[u8]>,
{
if let Some(p) = item.metadata().permission() {
if let OwnerStrategy::Preserve { options } = &keep_options.owner_strategy {
restore_owner(path, p, options)?;
}
match keep_options.mode_strategy {
ModeStrategy::Preserve => restore_mode(path, p)?,
ModeStrategy::Masked(mask) => restore_mode_masked(path, p, mask)?,
ModeStrategy::Never => {}
}
}
#[cfg(target_os = "macos")]
let skip_xattr_acl = matches!(
keep_options.mac_metadata_strategy,
MacMetadataStrategy::Always
) && item.mac_metadata().is_some();
#[cfg(not(target_os = "macos"))]
let skip_xattr_acl = false;
#[cfg(unix)]
if !skip_xattr_acl && matches!(keep_options.xattr_strategy, XattrStrategy::Always) {
match utils::os::unix::fs::xattrs::set_xattrs(path, item.xattrs()) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::Unsupported => {
log::warn!(
"Extended attributes are not supported on filesystem for '{}': {}",
path.display(),
e
);
}
Err(e) => return Err(e),
}
}
#[cfg(not(unix))]
if let XattrStrategy::Always = keep_options.xattr_strategy {
log::warn!("Currently extended attribute is not supported on this platform.");
}
#[cfg(feature = "acl")]
if !skip_xattr_acl {
restore_acls(path, item.acl()?, keep_options.acl_strategy)?;
}
#[cfg(not(feature = "acl"))]
if let AclStrategy::Always = keep_options.acl_strategy {
log::warn!("Please enable `acl` feature and rebuild and install pna.");
}
if let FflagsStrategy::Always = keep_options.fflags_strategy {
let flags = item.fflags();
if !flags.is_empty() {
match utils::fs::set_flags(path, &flags) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::Unsupported => {
log::warn!(
"File flags are not supported on filesystem for '{}': {}",
path.display(),
e
);
}
Err(e) => return Err(e),
}
}
}
#[cfg(target_os = "macos")]
if matches!(
keep_options.mac_metadata_strategy,
MacMetadataStrategy::Always
) && let Some(apple_double_data) = item.mac_metadata()
{
match utils::os::unix::fs::copyfile::unpack_apple_double(apple_double_data, path) {
Ok(()) => {
log::debug!("Unpacked macOS metadata for '{}'", path.display());
}
Err(e) => {
log::warn!(
"Failed to restore macOS metadata for '{}': {}",
path.display(),
e
);
}
}
}
#[cfg(not(target_os = "macos"))]
if matches!(
keep_options.mac_metadata_strategy,
MacMetadataStrategy::Always
) && item.mac_metadata().is_some()
{
log::warn!(
"macOS metadata present but cannot be restored on this platform: '{}'",
path.display()
);
}
Ok(())
}
#[cfg(feature = "acl")]
fn restore_acls(path: &Path, acls: Acls, acl_strategy: AclStrategy) -> io::Result<()> {
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "macos",
windows
))]
if let AclStrategy::Always = acl_strategy {
use crate::chunk::{AcePlatform, Acl, acl_convert_current_platform};
use itertools::Itertools;
let platform = AcePlatform::CURRENT;
if let Some((platform, acl)) = acls.into_iter().find_or_first(|(p, _)| p.eq(&platform))
&& !acl.is_empty()
{
match utils::acl::set_facl(
path,
acl_convert_current_platform(Acl {
platform,
entries: acl,
}),
) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::Unsupported => {
log::warn!(
"ACL not supported on this filesystem, skipping '{}': {}",
path.display(),
e
);
}
Err(e) => return Err(e),
}
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "macos",
windows
)))]
if let AclStrategy::Always = acl_strategy {
log::warn!("Currently acl is not supported on this platform.");
}
Ok(())
}
fn resolve_owner(
permission: &Permission,
uname_override: Option<&str>,
gname_override: Option<&str>,
uid_override: Option<u32>,
gid_override: Option<u32>,
) -> (Option<User>, Option<Group>) {
let user = if let Some(uid) = uid_override {
User::from_uid(uid.into()).ok()
} else {
let name = uname_override.unwrap_or(permission.uname());
search_owner(name, permission.uid()).ok()
};
let group = if let Some(gid) = gid_override {
Group::from_gid(gid.into()).ok()
} else {
let name = gname_override.unwrap_or(permission.gname());
search_group(name, permission.gid()).ok()
};
(user, group)
}
#[inline]
fn restore_owner(path: &Path, p: &Permission, options: &OwnerOptions) -> io::Result<()> {
#[cfg(any(unix, windows))]
{
let (user, group) = resolve_owner(
p,
options.uname.as_deref(),
options.gname.as_deref(),
options.uid,
options.gid,
);
match lchown(path, user, group) {
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
log::warn!("failed to restore owner of {}: {}", path.display(), e)
}
r => r?,
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = (path, p, options);
log::warn!("Currently ownership restoration is not supported on this platform.");
}
Ok(())
}
#[inline]
fn restore_mode(path: &Path, p: &Permission) -> io::Result<()> {
#[cfg(any(unix, windows))]
{
utils::fs::chmod(path, p.permissions())?;
}
#[cfg(not(any(unix, windows)))]
{
let _ = (path, p);
log::warn!(
"Skipping mode restoration for '{}': not supported on this platform.",
path.display()
);
}
Ok(())
}
#[inline]
fn restore_mode_masked(path: &Path, p: &Permission, umask: Umask) -> io::Result<()> {
#[cfg(any(unix, windows))]
{
utils::fs::chmod(path, umask.apply(p.permissions()))?;
}
#[cfg(not(any(unix, windows)))]
{
let _ = (path, p, umask);
log::warn!(
"Skipping mode restoration for '{}': not supported on this platform.",
path.display()
);
}
Ok(())
}
fn is_existing_newer<T>(metadata: &fs::Metadata, item: &NormalEntry<T>) -> bool
where
T: AsRef<[u8]>,
{
if let (Ok(existing_modified), Some(entry_modified)) =
(metadata.modified(), item.metadata().modified_time())
{
existing_modified >= entry_modified
} else {
false
}
}
fn extract_entry_to_stdout<T>(item: &NormalEntry<T>, password: Option<&[u8]>) -> io::Result<()>
where
T: AsRef<[u8]>,
pna::RawChunk<T>: Chunk,
{
if !matches!(item.header().data_kind(), DataKind::File) {
return Ok(());
}
let mut reader = item.reader(ReadOptions::with_password(password))?;
let mut stdout = io::stdout().lock();
io::copy(&mut reader, &mut stdout)?;
stdout.flush()?;
Ok(())
}
fn ensure_directory_components(
path: &Path,
unlink_first: bool,
secure_symlinks: bool,
) -> io::Result<()> {
if path.as_os_str().is_empty() {
return Ok(());
}
if !secure_symlinks {
match fs::create_dir_all(path) {
Ok(()) => return Ok(()),
Err(err)
if err.kind() == io::ErrorKind::NotADirectory
|| err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => return Err(err),
}
}
let mut current = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => continue,
Component::ParentDir => {
current.pop();
continue;
}
Component::RootDir | Component::Prefix(_) | Component::Normal(_) => {
current.push(component.as_os_str());
}
}
if current.as_os_str().is_empty() {
continue;
}
match fs::symlink_metadata(¤t) {
Ok(meta) => {
if meta.is_dir() {
continue;
}
if !secure_symlinks
&& meta.is_symlink()
&& fs::metadata(¤t).is_ok_and(|m| m.is_dir())
{
continue;
}
if secure_symlinks && meta.is_symlink() && !unlink_first {
return Err(io::Error::other(format!(
"Cannot extract through symlink {}",
current.display()
)));
}
if unlink_first {
utils::fs::remove_path_all(¤t)?;
} else {
match utils::fs::remove_path(¤t) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) if e.kind() == io::ErrorKind::IsADirectory => continue,
Err(e) => return Err(e),
}
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
if let Err(err) = fs::create_dir(¤t)
&& err.kind() != io::ErrorKind::AlreadyExists
{
return Err(err);
}
}
Ok(())
}
fn search_owner(name: &str, id: u64) -> io::Result<User> {
let user = User::from_name(name);
if user.is_ok() {
return user;
}
User::from_uid((id as u32).into())
}
fn search_group(name: &str, id: u64) -> io::Result<Group> {
let group = Group::from_name(name);
if group.is_ok() {
return group;
}
Group::from_gid((id as u32).into())
}
#[inline]
fn is_unsafe_link(reference: &EntryReference) -> bool {
crate::command::core::path::is_unsafe_link_path(reference.as_str())
}