mod archive_source;
mod ignore;
pub(crate) mod iter;
#[cfg(unix)]
pub(crate) mod mtree;
pub(crate) mod path;
mod path_filter;
pub(crate) mod path_lock;
mod path_transformer;
pub(crate) mod permission;
pub(crate) mod re;
pub(crate) mod safe_writer;
pub(crate) mod time_filter;
pub(crate) mod timestamp;
pub(crate) use self::archive_source::SplitArchiveReader;
pub(crate) use self::path::PathnameEditor;
pub(crate) use self::permission::{ModeStrategy, OwnerOptions, OwnerStrategy, Umask};
pub(crate) use self::safe_writer::SafeWriter;
pub(crate) use self::timestamp::{TimeSource, TimestampStrategy};
use crate::{
cli::{CipherAlgorithmArgs, CompressionAlgorithmArgs, HashAlgorithmArgs, MissingTimePolicy},
utils::{self, PathPartExt, fs::HardlinkResolver},
};
pub(crate) use iter::ReorderByIndex;
pub(crate) use path_filter::PathFilter;
use path_slash::*;
pub(crate) use path_transformer::PathTransformers;
use pna::{
Archive, EntryBuilder, EntryPart, MIN_CHUNK_BYTES_SIZE, NormalEntry, PNA_HEADER, ReadEntry,
SolidEntryBuilder, WriteOptions, prelude::*,
};
use std::{
borrow::Cow,
fmt, fs,
io::{self, prelude::*},
path::{Path, PathBuf},
time::SystemTime,
};
pub(crate) use time_filter::{TimeFilter, TimeFilters};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) enum SourceFormat {
Pna,
Mtree,
}
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) fn detect_format<R: io::BufRead>(reader: &mut R) -> io::Result<SourceFormat> {
let buf = reader.fill_buf()?;
Ok(if buf.starts_with(PNA_HEADER) {
SourceFormat::Pna
} else {
SourceFormat::Mtree
})
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProcessAction {
Continue,
Stop,
}
#[derive(Clone, Debug)]
pub(crate) struct CollectOptions<'a> {
pub(crate) recursive: bool,
pub(crate) keep_dir: bool,
pub(crate) gitignore: bool,
pub(crate) nodump: bool,
pub(crate) follow_links: bool,
pub(crate) follow_command_links: bool,
pub(crate) one_file_system: bool,
pub(crate) filter: &'a PathFilter<'a>,
pub(crate) time_filters: &'a TimeFilters,
}
pub(crate) struct TimeFilterResolver<'a> {
pub(crate) newer_ctime_than: Option<&'a Path>,
pub(crate) older_ctime_than: Option<&'a Path>,
pub(crate) newer_ctime: Option<SystemTime>,
pub(crate) older_ctime: Option<SystemTime>,
pub(crate) newer_mtime_than: Option<&'a Path>,
pub(crate) older_mtime_than: Option<&'a Path>,
pub(crate) newer_mtime: Option<SystemTime>,
pub(crate) older_mtime: Option<SystemTime>,
pub(crate) missing_ctime: MissingTimePolicy,
pub(crate) missing_mtime: MissingTimePolicy,
}
impl TimeFilterResolver<'_> {
pub(crate) fn resolve(self) -> io::Result<TimeFilters> {
fn resolve_ctime(path: &Path) -> io::Result<SystemTime> {
fs::metadata(path)?.created().map_err(|_| {
io::Error::new(
io::ErrorKind::Unsupported,
format!(
"creation time (birth time) is not available for '{}'",
path.display()
),
)
})
}
Ok(TimeFilters {
ctime: TimeFilter {
newer_than: match self.newer_ctime_than {
Some(p) => Some(resolve_ctime(p)?),
None => self.newer_ctime,
},
older_than: match self.older_ctime_than {
Some(p) => Some(resolve_ctime(p)?),
None => self.older_ctime,
},
missing_policy: self.missing_ctime,
},
mtime: TimeFilter {
newer_than: match self.newer_mtime_than {
Some(p) => Some(fs::metadata(p)?.modified()?),
None => self.newer_mtime,
},
older_than: match self.older_mtime_than {
Some(p) => Some(fs::metadata(p)?.modified()?),
None => self.older_mtime,
},
missing_policy: self.missing_mtime,
},
})
}
}
pub(crate) const SPLIT_ARCHIVE_OVERHEAD_BYTES: usize =
PNA_HEADER.len() + MIN_CHUNK_BYTES_SIZE * 3 + 8;
pub(crate) const MIN_SPLIT_PART_BYTES: usize = SPLIT_ARCHIVE_OVERHEAD_BYTES + MIN_CHUNK_BYTES_SIZE;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum XattrStrategy {
Never,
Always,
}
impl XattrStrategy {
pub(crate) const fn from_flags(
keep_xattr: bool,
no_keep_xattr: bool,
default_preserve: bool,
) -> Self {
if no_keep_xattr {
Self::Never
} else if keep_xattr || default_preserve {
Self::Always
} else {
Self::Never
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum AclStrategy {
Never,
Always,
}
impl AclStrategy {
pub(crate) const fn from_flags(keep_acl: bool, no_keep_acl: bool) -> Self {
if no_keep_acl {
Self::Never
} else if keep_acl {
Self::Always
} else {
Self::Never
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum FflagsStrategy {
Never,
Always,
}
impl FflagsStrategy {
pub(crate) const fn from_flags(keep_fflags: bool, no_keep_fflags: bool) -> Self {
if no_keep_fflags {
Self::Never
} else if keep_fflags {
Self::Always
} else {
Self::Never
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum MacMetadataStrategy {
#[default]
Never,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
Always,
}
impl MacMetadataStrategy {
#[cfg(target_os = "macos")]
pub(crate) const fn from_flags(mac_metadata: bool, no_mac_metadata: bool) -> Self {
if no_mac_metadata {
Self::Never
} else if mac_metadata {
Self::Always
} else {
Self::Never
}
}
#[cfg(not(target_os = "macos"))]
pub(crate) const fn from_flags(_mac_metadata: bool, _no_mac_metadata: bool) -> Self {
Self::Never
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) struct KeepOptions {
pub(crate) timestamp_strategy: TimestampStrategy,
pub(crate) mode_strategy: ModeStrategy,
pub(crate) owner_strategy: OwnerStrategy,
pub(crate) xattr_strategy: XattrStrategy,
pub(crate) acl_strategy: AclStrategy,
pub(crate) fflags_strategy: FflagsStrategy,
pub(crate) mac_metadata_strategy: MacMetadataStrategy,
}
pub(crate) struct TimestampStrategyResolver {
pub(crate) keep_timestamp: bool,
pub(crate) no_keep_timestamp: bool,
pub(crate) default_preserve: bool,
pub(crate) mtime: Option<SystemTime>,
pub(crate) clamp_mtime: bool,
pub(crate) ctime: Option<SystemTime>,
pub(crate) clamp_ctime: bool,
pub(crate) atime: Option<SystemTime>,
pub(crate) clamp_atime: bool,
}
impl TimestampStrategyResolver {
fn time_source_from(value: Option<SystemTime>, clamp: bool) -> TimeSource {
match (value, clamp) {
(Some(t), true) => TimeSource::ClampTo(t),
(Some(t), false) => TimeSource::Override(t),
(None, _) => TimeSource::FromSource,
}
}
fn has_any_override(&self) -> bool {
self.mtime.is_some() || self.ctime.is_some() || self.atime.is_some()
}
pub(crate) fn resolve(self) -> TimestampStrategy {
if self.no_keep_timestamp {
TimestampStrategy::NoPreserve
} else if self.keep_timestamp || self.has_any_override() {
TimestampStrategy::Preserve {
mtime: Self::time_source_from(self.mtime, self.clamp_mtime),
ctime: Self::time_source_from(self.ctime, self.clamp_ctime),
atime: Self::time_source_from(self.atime, self.clamp_atime),
}
} else if self.default_preserve {
TimestampStrategy::preserve()
} else {
TimestampStrategy::NoPreserve
}
}
}
pub(crate) struct PermissionStrategyResolver {
pub(crate) keep_permission: bool,
pub(crate) no_keep_permission: bool,
pub(crate) same_owner: bool,
pub(crate) uname: Option<String>,
pub(crate) gname: Option<String>,
pub(crate) uid: Option<u32>,
pub(crate) gid: Option<u32>,
pub(crate) numeric_owner: bool,
}
impl PermissionStrategyResolver {
pub(crate) fn resolve(self) -> (ModeStrategy, OwnerStrategy) {
if self.no_keep_permission {
(ModeStrategy::Never, OwnerStrategy::Never)
} else if self.keep_permission {
let mode_strategy = ModeStrategy::Preserve;
let owner_strategy = if self.same_owner {
OwnerStrategy::Preserve {
options: OwnerOptions {
uname: if self.numeric_owner {
Some(String::new())
} else {
self.uname
},
gname: if self.numeric_owner {
Some(String::new())
} else {
self.gname
},
uid: self.uid,
gid: self.gid,
},
}
} else {
OwnerStrategy::Never
};
(mode_strategy, owner_strategy)
} else {
(ModeStrategy::Never, OwnerStrategy::Never)
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct CreateOptions {
pub(crate) option: WriteOptions,
pub(crate) keep_options: KeepOptions,
pub(crate) pathname_editor: PathnameEditor,
}
#[derive(Clone, Debug)]
pub(crate) struct CollectedEntry {
pub(crate) path: PathBuf,
pub(crate) store_as: StoreAs,
pub(crate) metadata: fs::Metadata,
}
#[derive(Clone, Debug)]
pub(crate) enum StoreAs {
File,
Dir,
Symlink,
Hardlink(PathBuf),
}
#[derive(Clone, Debug)]
pub(crate) enum ArchiveSource {
File(PathBuf),
Stdin,
}
impl fmt::Display for ArchiveSource {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(path) => fmt::Display::fmt(&path.display(), f),
Self::Stdin => f.write_str("-"),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum ItemSource {
Filesystem(PathBuf),
Archive(ArchiveSource),
}
impl ItemSource {
pub(crate) fn parse(arg: &str) -> Self {
if let Some(archive_path) = arg.strip_prefix('@') {
if archive_path.is_empty() || archive_path == "-" {
Self::Archive(ArchiveSource::Stdin)
} else {
Self::Archive(ArchiveSource::File(PathBuf::from(archive_path)))
}
} else {
Self::Filesystem(PathBuf::from(arg))
}
}
pub(crate) fn parse_many(args: &[String]) -> Vec<Self> {
args.iter().map(|s| Self::parse(s)).collect()
}
}
pub(crate) fn validate_no_duplicate_stdin(sources: &[ItemSource]) -> anyhow::Result<()> {
let stdin_count = sources
.iter()
.filter(|s| matches!(s, ItemSource::Archive(ArchiveSource::Stdin)))
.count();
if stdin_count > 1 {
anyhow::bail!("stdin (@- or @) can only be specified once as an archive source");
}
Ok(())
}
#[derive(Clone, Debug)]
pub(crate) enum CollectedItem {
Filesystem(CollectedEntry),
ArchiveMarker(ArchiveSource),
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum EntryResult {
Single(io::Result<Option<NormalEntry>>),
Batch(io::Result<Vec<io::Result<Option<NormalEntry>>>>),
}
impl EntryResult {
pub(crate) fn into_entries(self) -> Vec<io::Result<Option<NormalEntry>>> {
match self {
EntryResult::Single(entry) => vec![entry],
EntryResult::Batch(entries) => entries.unwrap_or_else(|e| vec![Err(e)]),
}
}
}
pub(crate) fn drain_entry_results<I, F, T>(results: I, mut add_entry: F) -> io::Result<()>
where
I: IntoIterator<Item = (usize, EntryResult)>,
F: FnMut(NormalEntry) -> io::Result<T>,
{
for result in ReorderByIndex::new(results.into_iter()) {
match result {
EntryResult::Single(entry) => {
if let Some(entry) = entry? {
add_entry(entry)?;
}
}
EntryResult::Batch(entries) => {
for entry in entries? {
if let Some(entry) = entry? {
add_entry(entry)?;
}
}
}
}
}
Ok(())
}
pub(crate) fn spawn_entry_results(
target_items: Vec<CollectedItem>,
create_options: &CreateOptions,
filter: &PathFilter<'_>,
time_filters: &TimeFilters,
password: Option<&[u8]>,
allow_concatenated_archives: bool,
) -> std::sync::mpsc::Receiver<(usize, EntryResult)> {
let (tx, rx) = std::sync::mpsc::channel();
rayon::scope_fifo(|s| {
for (idx, item) in target_items.into_iter().enumerate() {
match item {
CollectedItem::Filesystem(entry) => {
let tx = tx.clone();
s.spawn_fifo(move |_| {
log::debug!("Adding: {}", entry.path.display());
tx.send((
idx,
EntryResult::Single(create_entry(&entry, create_options)),
))
.unwrap_or_else(|_| unreachable!("receiver is held by scope owner"));
})
}
CollectedItem::ArchiveMarker(source) => {
let result = read_archive_source(
&source,
create_options,
filter,
time_filters,
password,
allow_concatenated_archives,
);
tx.send((idx, EntryResult::Batch(result)))
.unwrap_or_else(|_| unreachable!("receiver is held by scope owner"));
}
}
}
drop(tx);
});
rx
}
pub(crate) fn collect_items_from_sources(
sources: impl IntoIterator<Item = ItemSource>,
options: &CollectOptions<'_>,
hardlink_resolver: &mut HardlinkResolver,
) -> io::Result<Vec<CollectedItem>> {
let mut results = Vec::new();
for source in sources {
match source {
ItemSource::Filesystem(path) => {
let items = collect_items_with_state(&path, options, hardlink_resolver)?;
results.extend(items.into_iter().map(CollectedItem::Filesystem));
}
ItemSource::Archive(archive_source) => {
results.push(CollectedItem::ArchiveMarker(archive_source));
}
}
}
Ok(results)
}
pub(crate) fn collect_items_from_paths<P: AsRef<Path>>(
paths: impl IntoIterator<Item = P>,
options: &CollectOptions<'_>,
hardlink_resolver: &mut HardlinkResolver,
) -> io::Result<Vec<CollectedEntry>> {
let mut results = Vec::new();
for path in paths {
results.extend(collect_items_with_state(
path.as_ref(),
options,
hardlink_resolver,
)?);
}
Ok(results)
}
pub(crate) fn collect_items_with_state(
path: &Path,
options: &CollectOptions<'_>,
hardlink_resolver: &mut HardlinkResolver,
) -> io::Result<Vec<CollectedEntry>> {
let mut ig = ignore::Ignore::default();
let mut out = Vec::new();
let mut iter = if options.recursive {
walkdir::WalkDir::new(path)
} else {
walkdir::WalkDir::new(path).max_depth(0)
}
.follow_links(options.follow_links)
.follow_root_links(options.follow_command_links)
.same_file_system(options.one_file_system)
.into_iter();
while let Some(res) = iter.next() {
match res {
Ok(entry) => {
let path = entry.path();
let ty = entry.file_type();
let depth = entry.depth();
let should_follow =
options.follow_links || (depth == 0 && options.follow_command_links);
let is_dir = ty.is_dir() || (ty.is_symlink() && should_follow && path.is_dir());
let is_file = ty.is_file() || (ty.is_symlink() && should_follow && path.is_file());
let is_symlink = ty.is_symlink() && !should_follow;
if options.filter.excluded(path.to_slash_lossy()) {
if is_dir {
iter.skip_current_dir();
}
continue;
}
if options.gitignore {
if ig.is_ignore(path, is_dir) {
if is_dir {
iter.skip_current_dir();
}
continue;
}
if is_dir {
ig.add_path(path);
}
}
if options.nodump {
match utils::fs::is_nodump(path) {
Ok(true) => {
if is_dir {
iter.skip_current_dir();
}
continue;
}
Ok(false) => {}
Err(e) => {
log::warn!("Failed to check nodump flag for {}: {}", path.display(), e);
}
}
}
let store = if is_symlink {
Some((StoreAs::Symlink, fs::symlink_metadata(path)?))
} else if is_file {
if let Some(linked) = hardlink_resolver.resolve(path).ok().flatten() {
Some((StoreAs::Hardlink(linked), fs::symlink_metadata(path)?))
} else {
Some((StoreAs::File, fs::metadata(path)?))
}
} else if is_dir {
if options.keep_dir {
Some((StoreAs::Dir, fs::metadata(path)?))
} else {
None
}
} else {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("Unsupported file type: {}", path.display()),
));
};
if let Some((store_as, metadata)) = store
&& options
.time_filters
.matches_or_inactive(metadata.created().ok(), metadata.modified().ok())
{
out.push(CollectedEntry {
path: path.to_path_buf(),
store_as,
metadata,
});
}
}
Err(e) => {
if let Some(ioe) = e.io_error()
&& let Some(path) = e.path()
{
let metadata = fs::symlink_metadata(path)?;
if is_broken_symlink_error(&metadata, ioe) {
out.push(CollectedEntry {
path: path.to_path_buf(),
store_as: StoreAs::Symlink,
metadata,
});
continue;
}
}
return Err(io::Error::other(e));
}
}
}
Ok(out)
}
#[inline]
fn is_broken_symlink_error(meta: &fs::Metadata, err: &io::Error) -> bool {
meta.is_symlink() && err.kind() == io::ErrorKind::NotFound
}
pub(crate) fn collect_split_archives(first: impl AsRef<Path>) -> io::Result<Vec<fs::File>> {
let first = first.as_ref();
let mut archives = Vec::new();
let mut n = 1;
let mut target_archive = Cow::from(first);
while fs::exists(&target_archive)? {
archives.push(fs::File::open(&target_archive)?);
n += 1;
target_archive = target_archive.with_part(n).into();
}
if archives.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("No archive found at {}", first.display()),
));
}
Ok(archives)
}
const IN_MEMORY_THRESHOLD: usize = 50 * 1024 * 1024;
#[inline]
fn copy_buffered(file: fs::File, writer: &mut impl Write) -> io::Result<()> {
let mut reader = io::BufReader::with_capacity(IN_MEMORY_THRESHOLD, file);
io::copy(&mut reader, writer)?;
Ok(())
}
#[inline]
pub(crate) fn write_from_path(writer: &mut impl Write, path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
let mut file = fs::File::open(path)?;
let file_size = file
.metadata()
.ok()
.and_then(|meta| usize::try_from(meta.len()).ok());
if let Some(size) = file_size {
if size < IN_MEMORY_THRESHOLD {
let mut contents = vec![0u8; size];
file.read_exact(&mut contents)?;
writer.write_all(&contents)?;
return Ok(());
}
#[cfg(feature = "memmap")]
{
let mmap = utils::mmap::Mmap::map_with_size(file, size)?;
writer.write_all(&mmap[..])?;
return Ok(());
}
}
copy_buffered(file, writer)
}
pub(crate) fn create_entry(
item: &CollectedEntry,
CreateOptions {
option,
keep_options,
pathname_editor,
}: &CreateOptions,
) -> io::Result<Option<NormalEntry>> {
let CollectedEntry {
path,
store_as,
metadata,
} = item;
let Some(entry_name) = pathname_editor.edit_entry_name(path) else {
log::debug!("Skip: {}", path.display());
return Ok(None);
};
match store_as {
StoreAs::Hardlink(source) => {
let Some((reference, _)) = pathname_editor.edit_hardlink(source) else {
log::debug!("Skip: {}", path.display());
return Ok(None);
};
let entry = EntryBuilder::new_hard_link(entry_name, reference)?;
apply_metadata(entry, path, keep_options, metadata)?.build()
}
StoreAs::Symlink => {
let source = fs::read_link(path)?;
let reference = pathname_editor.edit_symlink(&source);
let entry = EntryBuilder::new_symlink(entry_name, reference)?;
apply_metadata(entry, path, keep_options, metadata)?.build()
}
StoreAs::File => {
let mut entry = EntryBuilder::new_file(entry_name, option)?;
write_from_path(&mut entry, path)?;
apply_metadata(entry, path, keep_options, metadata)?.build()
}
StoreAs::Dir => {
let entry = EntryBuilder::new_dir(entry_name);
apply_metadata(entry, path, keep_options, metadata)?.build()
}
}
.map(Some)
}
pub(crate) fn entry_option(
compression: CompressionAlgorithmArgs,
cipher: CipherAlgorithmArgs,
hash: HashAlgorithmArgs,
password: Option<&[u8]>,
) -> WriteOptions {
let (algorithm, level) = compression.algorithm();
let mut option_builder = WriteOptions::builder();
option_builder
.compression(algorithm)
.compression_level(level.unwrap_or_default())
.encryption(if password.is_some() {
cipher.algorithm()
} else {
pna::Encryption::No
})
.cipher_mode(cipher.mode())
.hash_algorithm(hash.algorithm())
.password(password);
option_builder.build()
}
#[cfg_attr(target_os = "wasi", allow(unused_variables))]
pub(crate) fn apply_metadata(
mut entry: EntryBuilder,
path: &Path,
keep_options: &KeepOptions,
meta: &fs::Metadata,
) -> io::Result<EntryBuilder> {
if let TimestampStrategy::Preserve {
mtime,
ctime,
atime,
} = keep_options.timestamp_strategy
{
if let Some(c) = ctime.resolve(meta.created().ok()) {
entry.created_time(c);
}
if let Some(m) = mtime.resolve(meta.modified().ok()) {
entry.modified_time(m);
}
if let Some(a) = atime.resolve(meta.accessed().ok()) {
entry.accessed_time(a);
}
}
#[cfg(unix)]
if let OwnerStrategy::Preserve { options } = &keep_options.owner_strategy {
use crate::utils::fs::{Group, User};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let mode = meta.permissions().mode() as u16;
let uid = options.uid.unwrap_or(meta.uid());
let gid = options.gid.unwrap_or(meta.gid());
let uname = match &options.uname {
None => User::from_uid(uid.into())?
.name()
.unwrap_or_default()
.into(),
Some(uname) => uname.clone(),
};
let gname = match &options.gname {
None => Group::from_gid(gid.into())?
.name()
.unwrap_or_default()
.into(),
Some(gname) => gname.clone(),
};
entry.permission(pna::Permission::new(
uid.into(),
uname,
gid.into(),
gname,
mode,
));
}
#[cfg(windows)]
if let OwnerStrategy::Preserve { options } = &keep_options.owner_strategy {
use crate::utils::os::windows::{fs::stat, security::SecurityDescriptor};
let sd = SecurityDescriptor::try_from(path)?;
let stat = stat(sd.path.as_ptr() as _)?;
let mode = stat.st_mode;
let user = sd.owner_sid()?;
let group = sd.group_sid()?;
let uid = options.uid.map_or(u64::MAX, Into::into);
let gid = options.gid.map_or(u64::MAX, Into::into);
let uname = options.uname.clone().unwrap_or(user.name);
let gname = options.gname.clone().unwrap_or(group.name);
entry.permission(pna::Permission::new(uid, uname, gid, gname, mode));
}
#[cfg(target_os = "macos")]
let skip_xattr_acl = matches!(
keep_options.mac_metadata_strategy,
MacMetadataStrategy::Always
);
#[cfg(not(target_os = "macos"))]
let skip_xattr_acl = false;
#[cfg(feature = "acl")]
if !skip_xattr_acl {
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "macos",
windows
))]
if let AclStrategy::Always = keep_options.acl_strategy {
use crate::chunk;
use pna::RawChunk;
match utils::acl::get_facl(path) {
Ok(acl) => {
entry
.add_extra_chunk(RawChunk::from_data(chunk::faCl, acl.platform.to_bytes()));
for ace in acl.entries {
entry.add_extra_chunk(RawChunk::from_data(chunk::faCe, ace.to_bytes()));
}
}
Err(e) if e.kind() == std::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 = keep_options.acl_strategy {
log::warn!("Currently acl is not supported on this platform.");
}
}
#[cfg(not(feature = "acl"))]
if let AclStrategy::Always = keep_options.acl_strategy {
log::warn!("Please enable `acl` feature and rebuild and install pna.");
}
#[cfg(unix)]
if !skip_xattr_acl && matches!(keep_options.xattr_strategy, XattrStrategy::Always) {
match utils::os::unix::fs::xattrs::get_xattrs(path) {
Ok(xattrs) => {
for attr in xattrs {
entry.add_xattr(attr);
}
}
Err(e) if e.kind() == std::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.");
}
if let FflagsStrategy::Always = keep_options.fflags_strategy {
match utils::fs::get_flags(path) {
Ok(flags) => {
for flag in flags {
entry.add_extra_chunk(crate::chunk::fflag_chunk(&flag));
}
}
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 let MacMetadataStrategy::Always = keep_options.mac_metadata_strategy {
use pna::RawChunk;
match utils::os::unix::fs::copyfile::pack_apple_double(path) {
Ok(apple_double_data) => {
if !apple_double_data.is_empty() {
let len = apple_double_data.len();
entry.add_extra_chunk(RawChunk::from_data(
crate::chunk::maMd,
apple_double_data,
));
log::debug!(
"Packed macOS metadata for '{}' ({len} bytes)",
path.display(),
);
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
}
Err(e) => {
log::warn!(
"Failed to pack macOS metadata for '{}': {}",
path.display(),
e
);
}
}
}
#[cfg(not(target_os = "macos"))]
if let MacMetadataStrategy::Always = keep_options.mac_metadata_strategy {
log::warn!("macOS metadata (--mac-metadata) is only supported on macOS.");
}
Ok(entry)
}
pub(crate) fn split_to_parts(
mut entry_part: EntryPart<&[u8]>,
first: usize,
max: usize,
) -> io::Result<Vec<EntryPart<&[u8]>>> {
let mut parts = vec![];
let mut split_size = first;
loop {
match entry_part.try_split(split_size) {
Ok((write_part, Some(remaining_part))) => {
parts.push(write_part);
entry_part = remaining_part;
split_size = max;
}
Ok((write_part, None)) => {
parts.push(write_part);
break;
}
Err(unsplit_part) => {
if split_size < max && parts.is_empty() {
entry_part = unsplit_part;
split_size = max;
continue;
}
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"A chunk was detected that could not be divided into chunks smaller than the given size {max}"
),
));
}
}
}
Ok(parts)
}
pub(crate) trait TransformStrategy {
fn transform<W, T, F>(
archive: &mut Archive<W>,
password: Option<&[u8]>,
read_entry: io::Result<ReadEntry<T>>,
transformer: F,
) -> io::Result<()>
where
W: Write,
T: AsRef<[u8]>,
F: FnMut(io::Result<NormalEntry<T>>) -> io::Result<Option<NormalEntry<T>>>,
NormalEntry<T>: From<NormalEntry>,
NormalEntry<T>: Entry;
}
pub(crate) struct TransformStrategyUnSolid;
impl TransformStrategy for TransformStrategyUnSolid {
fn transform<W, T, F>(
archive: &mut Archive<W>,
password: Option<&[u8]>,
read_entry: io::Result<ReadEntry<T>>,
mut transformer: F,
) -> io::Result<()>
where
W: Write,
T: AsRef<[u8]>,
F: FnMut(io::Result<NormalEntry<T>>) -> io::Result<Option<NormalEntry<T>>>,
NormalEntry<T>: From<NormalEntry>,
NormalEntry<T>: Entry,
{
match read_entry? {
ReadEntry::Solid(s) => {
for n in s.entries(password)? {
if let Some(entry) = transformer(n.map(Into::into))? {
archive.add_entry(entry)?;
}
}
Ok(())
}
ReadEntry::Normal(n) => {
if let Some(entry) = transformer(Ok(n))? {
archive.add_entry(entry)?;
}
Ok(())
}
}
}
}
pub(crate) struct TransformStrategyKeepSolid;
impl TransformStrategy for TransformStrategyKeepSolid {
fn transform<W, T, F>(
archive: &mut Archive<W>,
password: Option<&[u8]>,
read_entry: io::Result<ReadEntry<T>>,
mut transformer: F,
) -> io::Result<()>
where
W: Write,
T: AsRef<[u8]>,
F: FnMut(io::Result<NormalEntry<T>>) -> io::Result<Option<NormalEntry<T>>>,
NormalEntry<T>: From<NormalEntry>,
NormalEntry<T>: Entry,
{
match read_entry? {
ReadEntry::Solid(s) => {
let header = s.header();
let mut builder = SolidEntryBuilder::new(
WriteOptions::builder()
.compression(header.compression())
.encryption(header.encryption())
.cipher_mode(header.cipher_mode())
.password(password)
.build(),
)?;
for n in s.entries(password)? {
if let Some(entry) = transformer(n.map(Into::into))? {
builder.add_entry(entry)?;
}
}
archive.add_entry(builder.build()?)?;
Ok(())
}
ReadEntry::Normal(n) => {
if let Some(entry) = transformer(Ok(n))? {
archive.add_entry(entry)?;
}
Ok(())
}
}
}
}
pub(crate) fn run_across_archive<R, F>(
provider: impl IntoIterator<Item = R>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
R: Read,
F: FnMut(&mut Archive<R>) -> io::Result<()>,
{
let mut iter = provider.into_iter();
let mut archive = Archive::read_header(iter.next().expect(""))?;
loop {
processor(&mut archive)?;
if archive.has_next_archive() {
let next_reader = iter.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Archive is split, but no subsequent archives are found",
)
})?;
archive = archive.read_next_archive(next_reader)?;
continue;
}
if !allow_concatenated_archives {
break;
}
let reader = archive.into_inner();
match Archive::read_header(reader) {
Ok(next) => archive = next,
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
Err(err) => return Err(err),
}
}
Ok(())
}
fn run_across_archive_stoppable<R, F>(
provider: impl IntoIterator<Item = R>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
R: Read,
F: FnMut(&mut Archive<R>) -> io::Result<ProcessAction>,
{
let mut iter = provider.into_iter();
let mut archive = Archive::read_header(iter.next().expect(""))?;
loop {
if let ProcessAction::Stop = processor(&mut archive)? {
break;
}
if archive.has_next_archive() {
let next_reader = iter.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Archive is split, but no subsequent archives are found",
)
})?;
archive = archive.read_next_archive(next_reader)?;
continue;
}
if !allow_concatenated_archives {
break;
}
let reader = archive.into_inner();
match Archive::read_header(reader) {
Ok(next) => archive = next,
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
Err(err) => return Err(err),
}
}
Ok(())
}
pub(crate) fn run_process_archive<'p, Provider, F>(
archive_provider: impl IntoIterator<Item = impl Read>,
mut password_provider: Provider,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
Provider: FnMut() -> Option<&'p [u8]>,
F: FnMut(io::Result<NormalEntry>) -> io::Result<()>,
{
let password = password_provider();
run_read_entries(
archive_provider,
|entry| match entry? {
ReadEntry::Solid(solid) => solid.entries(password)?.try_for_each(&mut processor),
ReadEntry::Normal(regular) => processor(Ok(regular)),
},
allow_concatenated_archives,
)
}
pub(crate) fn run_process_archive_stoppable<'p, Provider, F>(
archive_provider: impl IntoIterator<Item = impl Read>,
mut password_provider: Provider,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
Provider: FnMut() -> Option<&'p [u8]>,
F: FnMut(io::Result<NormalEntry>) -> io::Result<ProcessAction>,
{
let password = password_provider();
run_read_entries_stoppable(
archive_provider,
|entry| match entry? {
ReadEntry::Solid(solid) => {
for n in solid.entries(password)? {
match processor(n)? {
ProcessAction::Continue => {}
ProcessAction::Stop => return Ok(ProcessAction::Stop),
}
}
Ok(ProcessAction::Continue)
}
ReadEntry::Normal(regular) => processor(Ok(regular)),
},
allow_concatenated_archives,
)
}
#[cfg(feature = "memmap")]
pub(crate) fn run_across_archive_mem<'d, F>(
archives: impl IntoIterator<Item = &'d [u8]>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
F: FnMut(&mut Archive<&'d [u8]>) -> io::Result<()>,
{
let mut iter = archives.into_iter();
let mut archive = Archive::read_header_from_slice(iter.next().expect(""))?;
loop {
processor(&mut archive)?;
if archive.has_next_archive() {
let next_reader = iter.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Archive is split, but no subsequent archives are found",
)
})?;
archive = archive.read_next_archive_from_slice(next_reader)?;
continue;
}
if !allow_concatenated_archives {
break;
}
let bytes = archive.into_inner();
match Archive::read_header_from_slice(bytes) {
Ok(next) => archive = next,
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
Err(err) => return Err(err),
}
}
Ok(())
}
#[cfg(feature = "memmap")]
pub(crate) fn run_read_entries_mem<'d, F>(
archives: impl IntoIterator<Item = &'d [u8]>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
F: FnMut(io::Result<ReadEntry<Cow<'d, [u8]>>>) -> io::Result<()>,
{
run_across_archive_mem(
archives,
|archive| archive.entries_slice().try_for_each(&mut processor),
allow_concatenated_archives,
)
}
#[cfg(feature = "memmap")]
pub(crate) fn run_entries<'d, 'p, Provider, F>(
archives: impl IntoIterator<Item = &'d [u8]>,
mut password_provider: Provider,
mut processor: F,
) -> io::Result<()>
where
Provider: FnMut() -> Option<&'p [u8]>,
F: FnMut(io::Result<NormalEntry<Cow<'d, [u8]>>>) -> io::Result<()>,
{
let password = password_provider();
run_read_entries_mem(
archives,
|entry| match entry? {
ReadEntry::Solid(s) => s
.entries(password)?
.try_for_each(|r| processor(r.map(Into::into))),
ReadEntry::Normal(r) => processor(Ok(r)),
},
false,
)
}
#[cfg(feature = "memmap")]
fn run_across_archive_mem_stoppable<'d, F>(
archives: impl IntoIterator<Item = &'d [u8]>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
F: FnMut(&mut Archive<&'d [u8]>) -> io::Result<ProcessAction>,
{
let mut iter = archives.into_iter();
let mut archive = Archive::read_header_from_slice(iter.next().expect(""))?;
loop {
if let ProcessAction::Stop = processor(&mut archive)? {
break;
}
if archive.has_next_archive() {
let next_reader = iter.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Archive is split, but no subsequent archives are found",
)
})?;
archive = archive.read_next_archive_from_slice(next_reader)?;
continue;
}
if !allow_concatenated_archives {
break;
}
let bytes = archive.into_inner();
match Archive::read_header_from_slice(bytes) {
Ok(next) => archive = next,
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
Err(err) => return Err(err),
}
}
Ok(())
}
#[cfg(feature = "memmap")]
fn run_read_entries_mem_stoppable<'d, F>(
archives: impl IntoIterator<Item = &'d [u8]>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
F: FnMut(io::Result<ReadEntry<Cow<'d, [u8]>>>) -> io::Result<ProcessAction>,
{
run_across_archive_mem_stoppable(
archives,
|archive| {
for entry in archive.entries_slice() {
match processor(entry)? {
ProcessAction::Continue => {}
ProcessAction::Stop => return Ok(ProcessAction::Stop),
}
}
Ok(ProcessAction::Continue)
},
allow_concatenated_archives,
)
}
#[cfg(feature = "memmap")]
pub(crate) fn run_entries_stoppable<'d, 'p, Provider, F>(
archives: impl IntoIterator<Item = &'d [u8]>,
mut password_provider: Provider,
mut processor: F,
) -> io::Result<()>
where
Provider: FnMut() -> Option<&'p [u8]>,
F: FnMut(io::Result<NormalEntry<Cow<'d, [u8]>>>) -> io::Result<ProcessAction>,
{
let password = password_provider();
run_read_entries_mem_stoppable(
archives,
|entry| match entry? {
ReadEntry::Solid(s) => {
for n in s.entries(password)? {
match processor(n.map(Into::into))? {
ProcessAction::Continue => {}
ProcessAction::Stop => return Ok(ProcessAction::Stop),
}
}
Ok(ProcessAction::Continue)
}
ReadEntry::Normal(r) => processor(Ok(r)),
},
false,
)
}
#[cfg(feature = "memmap")]
fn run_transform_entry<'d, 'p, W, Provider, F, Transform>(
writer: W,
archives: impl IntoIterator<Item = &'d [u8]>,
mut password_provider: Provider,
mut processor: F,
_strategy: Transform,
) -> anyhow::Result<()>
where
W: Write,
Provider: FnMut() -> Option<&'p [u8]>,
F: FnMut(
io::Result<NormalEntry<Cow<'d, [u8]>>>,
) -> io::Result<Option<NormalEntry<Cow<'d, [u8]>>>>,
Transform: TransformStrategy,
{
let password = password_provider();
let mut out_archive = Archive::write_header(writer)?;
run_read_entries_mem(
archives,
|entry| Transform::transform(&mut out_archive, password, entry, &mut processor),
false,
)?;
out_archive.finalize()?;
Ok(())
}
pub(crate) fn run_read_entries<F>(
archive_provider: impl IntoIterator<Item = impl Read>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
F: FnMut(io::Result<ReadEntry>) -> io::Result<()>,
{
run_across_archive(
archive_provider,
|archive| archive.entries().try_for_each(&mut processor),
allow_concatenated_archives,
)
}
pub(crate) fn run_read_entries_stoppable<F>(
archive_provider: impl IntoIterator<Item = impl Read>,
mut processor: F,
allow_concatenated_archives: bool,
) -> io::Result<()>
where
F: FnMut(io::Result<ReadEntry>) -> io::Result<ProcessAction>,
{
run_across_archive_stoppable(
archive_provider,
|archive| {
for entry in archive.entries() {
match processor(entry)? {
ProcessAction::Continue => {}
ProcessAction::Stop => return Ok(ProcessAction::Stop),
}
}
Ok(ProcessAction::Continue)
},
allow_concatenated_archives,
)
}
#[cfg(not(feature = "memmap"))]
fn run_transform_entry<'p, W, Provider, F, Transform>(
writer: W,
archives: impl IntoIterator<Item = impl Read>,
mut password_provider: Provider,
mut processor: F,
_strategy: Transform,
) -> anyhow::Result<()>
where
W: Write,
Provider: FnMut() -> Option<&'p [u8]>,
F: FnMut(io::Result<NormalEntry>) -> io::Result<Option<NormalEntry>>,
Transform: TransformStrategy,
{
let password = password_provider();
let mut out_archive = Archive::write_header(writer)?;
run_read_entries(
archives,
|entry| Transform::transform(&mut out_archive, password, entry, &mut processor),
false,
)?;
out_archive.finalize()?;
Ok(())
}
pub(crate) fn write_split_archive(
archive: impl AsRef<Path>,
entries: impl Iterator<Item = io::Result<impl Entry + Sized>>,
max_file_size: usize,
overwrite: bool,
) -> anyhow::Result<()> {
write_split_archive_path(
archive,
entries,
|base, n| base.with_part(n),
max_file_size,
overwrite,
)
}
pub(crate) fn write_split_archive_path<F, P>(
archive: impl AsRef<Path>,
entries: impl Iterator<Item = io::Result<impl Entry + Sized>>,
mut get_part_path: F,
max_file_size: usize,
overwrite: bool,
) -> anyhow::Result<()>
where
F: FnMut(&Path, usize) -> P,
P: AsRef<Path>,
{
let archive = archive.as_ref();
let first_item_path = get_part_path(archive, 1);
let first_item_path = first_item_path.as_ref();
let file = utils::fs::file_create(first_item_path, overwrite)?;
let buffered = io::BufWriter::with_capacity(64 * 1024, file);
write_split_archive_writer(
buffered,
entries,
|n| {
let file = utils::fs::file_create(get_part_path(archive, n), overwrite)?;
Ok(io::BufWriter::with_capacity(64 * 1024, file))
},
max_file_size,
|n| {
if n == 1 {
fs::rename(first_item_path, archive)?;
};
Ok(())
},
)
}
pub(crate) fn write_split_archive_writer<W, F, C>(
initial_writer: W,
entries: impl Iterator<Item = io::Result<impl Entry + Sized>>,
mut get_next_writer: F,
max_file_size: usize,
mut on_complete: C,
) -> anyhow::Result<()>
where
W: Write,
F: FnMut(usize) -> io::Result<W>,
C: FnMut(usize) -> io::Result<()>,
{
if max_file_size < MIN_SPLIT_PART_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Split size must be at least {MIN_SPLIT_PART_BYTES} bytes to accommodate headers"
),
)
.into());
}
let mut part_num = 1;
let mut writer = Archive::write_header(initial_writer)?;
let max_file_size = max_file_size - SPLIT_ARCHIVE_OVERHEAD_BYTES;
let mut written_entry_size = 0;
for entry in entries {
let p = EntryPart::from(entry?);
let parts = split_to_parts(
p.as_ref(),
max_file_size - written_entry_size,
max_file_size,
)?;
for part in parts {
if written_entry_size + part.bytes_len() > max_file_size {
part_num += 1;
let file = get_next_writer(part_num)?;
writer = writer.split_to_next_archive(file)?;
written_entry_size = 0;
}
written_entry_size += writer.add_entry_part(part)?;
}
}
writer.finalize()?;
on_complete(part_num)?;
Ok(())
}
#[inline]
fn read_paths_reader(reader: impl BufRead, nul: bool) -> io::Result<Vec<String>> {
if nul {
utils::io::read_to_nul(reader)
} else {
utils::io::read_to_lines(reader)
}
}
#[inline]
pub(crate) fn read_paths<P: AsRef<Path>>(path: P, nul: bool) -> io::Result<Vec<String>> {
let file = fs::File::open(path)?;
let reader = io::BufReader::new(file);
read_paths_reader(reader, nul)
}
#[inline]
pub(crate) fn read_paths_stdin(nul: bool) -> io::Result<Vec<String>> {
read_paths_reader(io::stdin().lock(), nul)
}
#[allow(unused_variables)]
pub(crate) fn apply_chroot(chroot: bool) -> anyhow::Result<()> {
if !chroot {
return Ok(());
}
#[cfg(all(unix, not(target_os = "fuchsia")))]
{
use anyhow::Context;
std::os::unix::fs::chroot(
std::env::current_dir().with_context(|| "resolving current directory before chroot")?,
)
.with_context(|| "chroot into current directory")?;
std::env::set_current_dir("/").with_context(|| "changing directory to / after chroot")?;
}
#[cfg(not(all(unix, not(target_os = "fuchsia"))))]
{
log::warn!("chroot not supported on this platform");
}
Ok(())
}
pub(crate) fn transform_archive_entries<R: io::Read>(
reader: R,
create_options: &CreateOptions,
filter: &PathFilter<'_>,
time_filters: &TimeFilters,
password: Option<&[u8]>,
allow_concatenated_archives: bool,
) -> io::Result<Vec<io::Result<Option<NormalEntry>>>> {
let mut results = Vec::new();
run_process_archive(
std::iter::once(reader),
|| password,
|entry| {
let entry = entry?;
if filter.excluded(entry.header().path()) {
return Ok(());
}
let ctime = entry.metadata().created_time();
let mtime = entry.metadata().modified_time();
if !time_filters.matches_or_inactive(ctime, mtime) {
return Ok(());
}
results.push(transform_normal_entry(entry, create_options));
Ok(())
},
allow_concatenated_archives,
)?;
Ok(results)
}
#[cfg(unix)]
pub(crate) fn read_archive_source(
source: &ArchiveSource,
create_options: &CreateOptions,
filter: &PathFilter<'_>,
time_filters: &TimeFilters,
password: Option<&[u8]>,
allow_concatenated_archives: bool,
) -> io::Result<Vec<io::Result<Option<NormalEntry>>>> {
fn process_source<R: io::BufRead>(
mut reader: R,
source_name: &str,
create_options: &CreateOptions,
filter: &PathFilter<'_>,
time_filters: &TimeFilters,
password: Option<&[u8]>,
allow_concatenated_archives: bool,
) -> io::Result<Vec<io::Result<Option<NormalEntry>>>> {
let format = detect_format(&mut reader)
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", source_name, e)))?;
match format {
SourceFormat::Pna => transform_archive_entries(
reader,
create_options,
filter,
time_filters,
password,
allow_concatenated_archives,
),
SourceFormat::Mtree => {
mtree::transform_mtree_entries(reader, create_options, filter, time_filters)
}
}
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", source_name, e)))
}
match source {
ArchiveSource::File(path) => {
let file = fs::File::open(path)
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))?;
let reader = io::BufReader::with_capacity(64 * 1024, file);
process_source(
reader,
&path.display().to_string(),
create_options,
filter,
time_filters,
password,
allow_concatenated_archives,
)
}
ArchiveSource::Stdin => {
let reader = io::BufReader::new(io::stdin().lock());
process_source(
reader,
"<stdin>",
create_options,
filter,
time_filters,
password,
allow_concatenated_archives,
)
}
}
}
#[cfg(not(unix))]
pub(crate) fn read_archive_source(
source: &ArchiveSource,
create_options: &CreateOptions,
filter: &PathFilter<'_>,
time_filters: &TimeFilters,
password: Option<&[u8]>,
allow_concatenated_archives: bool,
) -> io::Result<Vec<io::Result<Option<NormalEntry>>>> {
match source {
ArchiveSource::File(path) => {
let file = fs::File::open(path)
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))?;
let reader = io::BufReader::with_capacity(64 * 1024, file);
transform_archive_entries(
reader,
create_options,
filter,
time_filters,
password,
allow_concatenated_archives,
)
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))
}
ArchiveSource::Stdin => {
let reader = io::BufReader::new(io::stdin().lock());
transform_archive_entries(
reader,
create_options,
filter,
time_filters,
password,
allow_concatenated_archives,
)
.map_err(|e| io::Error::new(e.kind(), format!("<stdin>: {}", e)))
}
}
}
fn transform_normal_entry(
entry: NormalEntry,
CreateOptions {
pathname_editor,
keep_options,
..
}: &CreateOptions,
) -> io::Result<Option<NormalEntry>> {
let original_name = entry.header().path();
let Some(new_name) = pathname_editor.edit_entry_name(original_name.as_ref()) else {
log::debug!("Skip: {original_name}");
return Ok(None);
};
let mut result = entry.with_name(new_name);
let mut metadata = result.metadata().clone();
match keep_options.timestamp_strategy {
TimestampStrategy::Preserve {
mtime,
ctime,
atime,
} => {
let created = ctime.resolve(metadata.created_time());
let modified = mtime.resolve(metadata.modified_time());
let accessed = atime.resolve(metadata.accessed_time());
metadata = metadata
.with_created_time(created)
.with_modified_time(modified)
.with_accessed_time(accessed);
}
TimestampStrategy::NoPreserve => {
metadata = metadata
.with_created_time(None)
.with_modified_time(None)
.with_accessed_time(None);
}
}
if let OwnerStrategy::Preserve {
options:
OwnerOptions {
uid,
gid,
uname,
gname,
},
} = &keep_options.owner_strategy
{
if (uid.is_some() || gid.is_some() || uname.is_some() || gname.is_some())
&& let Some(perm) = metadata.permission()
{
let new_perm = pna::Permission::new(
uid.map(u64::from).unwrap_or_else(|| perm.uid()),
uname.clone().unwrap_or_else(|| perm.uname().to_string()),
gid.map(u64::from).unwrap_or_else(|| perm.gid()),
gname.clone().unwrap_or_else(|| perm.gname().to_string()),
perm.permissions(),
);
metadata = metadata.with_permission(Some(new_perm));
}
}
result = result.with_metadata(metadata);
Ok(Some(result))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::MissingTimePolicy;
use std::collections::HashSet;
const EMPTY_PATTERNS: [&str; 0] = [];
fn empty_path_filter<'a>() -> PathFilter<'a> {
PathFilter::new(EMPTY_PATTERNS, EMPTY_PATTERNS)
}
fn empty_time_filters() -> TimeFilters {
TimeFilters {
ctime: TimeFilter {
newer_than: None,
older_than: None,
missing_policy: MissingTimePolicy::Include,
},
mtime: TimeFilter {
newer_than: None,
older_than: None,
missing_policy: MissingTimePolicy::Include,
},
}
}
fn default_collect_options<'a>(
filter: &'a PathFilter<'a>,
time_filters: &'a TimeFilters,
) -> CollectOptions<'a> {
CollectOptions {
recursive: false,
keep_dir: false,
gitignore: false,
nodump: false,
follow_links: false,
follow_command_links: false,
one_file_system: false,
filter,
time_filters,
}
}
#[test]
fn collect_items_only_file() {
let source = concat!(env!("CARGO_MANIFEST_DIR"), "/../resources/test/raw",);
let filter = empty_path_filter();
let time_filters = empty_time_filters();
let options = default_collect_options(&filter, &time_filters);
let mut resolver = HardlinkResolver::new(options.follow_links);
let items = collect_items_from_paths([source], &options, &mut resolver).unwrap();
assert_eq!(
items.into_iter().map(|it| it.path).collect::<HashSet<_>>(),
HashSet::new()
);
}
#[test]
fn collect_items_keep_dir() {
let source = concat!(env!("CARGO_MANIFEST_DIR"), "/../resources/test/raw",);
let filter = empty_path_filter();
let time_filters = empty_time_filters();
let mut options = default_collect_options(&filter, &time_filters);
options.keep_dir = true;
let mut resolver = HardlinkResolver::new(options.follow_links);
let items = collect_items_from_paths([source], &options, &mut resolver).unwrap();
assert_eq!(
items.into_iter().map(|it| it.path).collect::<HashSet<_>>(),
[concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw",
)]
.into_iter()
.map(Into::into)
.collect::<HashSet<_>>()
);
}
#[test]
fn collect_items_recursive() {
let source = concat!(env!("CARGO_MANIFEST_DIR"), "/../resources/test/raw",);
let filter = empty_path_filter();
let time_filters = empty_time_filters();
let mut options = default_collect_options(&filter, &time_filters);
options.recursive = true;
let mut resolver = HardlinkResolver::new(options.follow_links);
let items = collect_items_from_paths([source], &options, &mut resolver).unwrap();
assert_eq!(
items.into_iter().map(|it| it.path).collect::<HashSet<_>>(),
[
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/first/second/third/pna.txt"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/images/icon.bmp"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/images/icon.png"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/images/icon.svg"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/parent/child.txt"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/pna/empty.pna"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/pna/nest.pna"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/empty.txt"
),
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../resources/test/raw/text.txt"
),
]
.into_iter()
.map(Into::into)
.collect::<HashSet<_>>()
);
}
mod item_source_parse {
use super::*;
#[test]
fn at_alone_is_stdin() {
let result = ItemSource::parse("@");
assert!(matches!(result, ItemSource::Archive(ArchiveSource::Stdin)));
}
#[test]
fn at_dash_is_stdin() {
let result = ItemSource::parse("@-");
assert!(matches!(result, ItemSource::Archive(ArchiveSource::Stdin)));
}
#[test]
fn at_path_is_archive_file() {
let result = ItemSource::parse("@archive.pna");
assert!(matches!(
result,
ItemSource::Archive(ArchiveSource::File(p)) if p == Path::new("archive.pna")
));
}
#[test]
fn plain_path_is_filesystem() {
let result = ItemSource::parse("some/path");
assert!(matches!(
result,
ItemSource::Filesystem(p) if p == Path::new("some/path")
));
}
#[test]
fn dot_slash_at_is_filesystem_escape() {
let result = ItemSource::parse("./@file");
assert!(matches!(
result,
ItemSource::Filesystem(p) if p == Path::new("./@file")
));
}
#[test]
fn parse_many_mixed() {
let args = vec![
"file1".to_string(),
"@archive.pna".to_string(),
"@".to_string(),
"./@literal".to_string(),
];
let results = ItemSource::parse_many(&args);
assert_eq!(results.len(), 4);
assert!(matches!(&results[0], ItemSource::Filesystem(p) if p == Path::new("file1")));
assert!(
matches!(&results[1], ItemSource::Archive(ArchiveSource::File(p)) if p == Path::new("archive.pna"))
);
assert!(matches!(
&results[2],
ItemSource::Archive(ArchiveSource::Stdin)
));
assert!(
matches!(&results[3], ItemSource::Filesystem(p) if p == Path::new("./@literal"))
);
}
#[test]
fn validate_no_duplicate_stdin_ok() {
let sources = vec![
ItemSource::Filesystem(PathBuf::from("file")),
ItemSource::Archive(ArchiveSource::Stdin),
ItemSource::Archive(ArchiveSource::File(PathBuf::from("archive.pna"))),
];
assert!(super::validate_no_duplicate_stdin(&sources).is_ok());
}
#[test]
fn validate_no_duplicate_stdin_error() {
let sources = vec![
ItemSource::Archive(ArchiveSource::Stdin),
ItemSource::Archive(ArchiveSource::Stdin),
];
assert!(super::validate_no_duplicate_stdin(&sources).is_err());
}
#[test]
fn validate_no_duplicate_stdin_empty() {
let sources: Vec<ItemSource> = vec![];
assert!(super::validate_no_duplicate_stdin(&sources).is_ok());
}
}
mod detect_format_tests {
use super::*;
use std::io::Cursor;
#[test]
fn pna_magic() {
let data = [0x89, 0x50, 0x4E, 0x41, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00];
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Pna);
}
#[test]
fn mtree_header() {
let data = b"#mtree\n/set type=file mode=0644\n";
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Mtree);
}
#[test]
fn mtree_set_directive() {
let data = b"/set type=file mode=0644\n";
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Mtree);
}
#[test]
fn mtree_entry_line() {
let data = b"usr/bin/hello mode=0755\n";
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Mtree);
}
#[test]
fn empty_falls_back_to_mtree() {
let data = b"";
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Mtree);
}
#[test]
fn partial_pna_magic_is_mtree() {
let data = [0x89, 0x50, 0x4E, 0x41, 0x00, 0x00, 0x00, 0x00];
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Mtree);
}
#[test]
fn short_buffer_is_mtree() {
let data = [0x89, 0x50, 0x4E, 0x41];
let mut reader = Cursor::new(&data[..]);
let format = detect_format(&mut reader).unwrap();
assert_eq!(format, SourceFormat::Mtree);
}
}
}