Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ path-slash = "0.2.1"
pna = { version = "0.30.0", path = "../pna" }
rayon = "1.11.0"
regex = "1.12.2"
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
tabled = { version = "0.20.0", default-features = false, features = ["std", "ansi"] }
Expand All @@ -57,7 +58,6 @@ exacl = { version = "0.12.0", optional = true }
nix = { version = "0.30.1", features = ["user", "fs", "ioctl"] }

[target.'cfg(windows)'.dependencies]
scopeguard = "1.2.0"
windows = { version = "0.62.2", features = [
"Win32_Storage_FileSystem",
"Win32_Security_Authorization",
Expand Down
2 changes: 2 additions & 0 deletions cli/src/chunk.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod acl;
mod fflag;
mod mac_metadata;

pub use acl::*;
pub use fflag::*;
pub use mac_metadata::*;
10 changes: 10 additions & 0 deletions cli/src/chunk/mac_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use pna::ChunkType;

/// Private chunk type for macOS metadata (AppleDouble format).
/// Name follows PNA chunk naming convention where case has semantic meaning:
/// - lowercase first letter: ancillary (not critical)
/// - lowercase second letter: private (not public)
/// - uppercase third letter: reserved
/// - lowercase fourth letter: safe to copy
#[allow(non_upper_case_globals)]
pub const maMd: ChunkType = unsafe { ChunkType::from_unchecked(*b"maMd") };
7 changes: 4 additions & 3 deletions cli/src/command/append.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use crate::{
Command, ask_password, check_password,
core::{
AclStrategy, CollectOptions, CollectedItem, CreateOptions, FflagsStrategy, KeepOptions,
OwnerOptions, PathFilter, PathTransformers, PathnameEditor, PermissionStrategy,
TimeFilterResolver, TimestampStrategyResolver, XattrStrategy, collect_items_from_paths,
create_entry, entry_option,
MacMetadataStrategy, OwnerOptions, PathFilter, PathTransformers, PathnameEditor,
PermissionStrategy, TimeFilterResolver, TimestampStrategyResolver, XattrStrategy,
collect_items_from_paths, create_entry, entry_option,
re::{bsd::SubstitutionRule, gnu::TransformRule},
read_paths, read_paths_stdin,
},
Expand Down Expand Up @@ -392,6 +392,7 @@ fn append_to_archive(args: AppendCommand) -> anyhow::Result<()> {
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
fflags_strategy: FflagsStrategy::Never,
mac_metadata_strategy: MacMetadataStrategy::Never,
};
let owner_options = OwnerOptions::new(
args.uname,
Expand Down
107 changes: 93 additions & 14 deletions cli/src/command/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,46 @@
}
}

/// Strategy for handling macOS metadata in AppleDouble format.
/// When enabled, creates `._` prefixed entries containing AppleDouble data
/// (extended attributes, ACLs, resource forks) for each file with Mac metadata.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum MacMetadataStrategy {
Comment thread Dismissed
Comment thread Dismissed
/// Do not create AppleDouble entries (default)
#[default]
Never,
/// Create AppleDouble entries for files with Mac metadata (macOS only)
Always,
Comment thread Dismissed
Comment thread Dismissed
}

impl MacMetadataStrategy {
/// Creates a strategy from CLI flags, considering platform.
/// On non-macOS platforms, always returns Never regardless of flags.
#[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(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) struct KeepOptions {
pub(crate) timestamp_strategy: TimestampStrategy,
pub(crate) permission_strategy: PermissionStrategy,
pub(crate) xattr_strategy: XattrStrategy,
pub(crate) acl_strategy: AclStrategy,
pub(crate) fflags_strategy: FflagsStrategy,
pub(crate) mac_metadata_strategy: MacMetadataStrategy,
}

/// Resolves CLI timestamp options into a `TimestampStrategy`.
Expand Down Expand Up @@ -735,8 +768,18 @@
mode,
));
}
// On macOS, when mac_metadata_strategy is Always, AppleDouble packing via copyfile()
// already includes xattrs and ACLs. Skip separate handling to avoid duplication.
#[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",
Expand Down Expand Up @@ -767,23 +810,25 @@
log::warn!("Please enable `acl` feature and rebuild and install pna.");
}
#[cfg(unix)]
if let XattrStrategy::Always = keep_options.xattr_strategy {
match utils::os::unix::fs::xattrs::get_xattrs(path) {
Ok(xattrs) => {
for attr in xattrs {
entry.add_xattr(attr);
if !skip_xattr_acl {
if let XattrStrategy::Always = keep_options.xattr_strategy {
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),
}
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),
}
}

Check warning

Code scanning / clippy

this if statement can be collapsed Warning

this if statement can be collapsed

Check warning

Code scanning / clippy

this if statement can be collapsed Warning

this if statement can be collapsed
#[cfg(not(unix))]
if let XattrStrategy::Always = keep_options.xattr_strategy {
log::warn!("Currently extended attribute is not supported on this platform.");
Expand All @@ -805,6 +850,40 @@
Err(e) => return Err(e),
}
}
// macOS metadata (AppleDouble) - packs xattrs, ACLs, resource forks via copyfile()
#[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 => {
// File has no Mac metadata, this is fine
}
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.");
}
Comment thread
ChanTsune marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Ok(entry)
}

Expand Down
7 changes: 4 additions & 3 deletions cli/src/command/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use crate::{
Command, ask_password, check_password,
core::{
AclStrategy, CollectOptions, CollectedItem, CreateOptions, FflagsStrategy, KeepOptions,
MIN_SPLIT_PART_BYTES, OwnerOptions, PathFilter, PathTransformers, PathnameEditor,
PermissionStrategy, TimeFilterResolver, TimestampStrategyResolver, XattrStrategy,
collect_items_from_paths, create_entry, entry_option,
MIN_SPLIT_PART_BYTES, MacMetadataStrategy, OwnerOptions, PathFilter, PathTransformers,
PathnameEditor, PermissionStrategy, TimeFilterResolver, TimestampStrategyResolver,
XattrStrategy, collect_items_from_paths, create_entry, entry_option,
re::{bsd::SubstitutionRule, gnu::TransformRule},
read_paths, read_paths_stdin, write_split_archive,
},
Expand Down Expand Up @@ -478,6 +478,7 @@ fn create_archive(args: CreateCommand) -> anyhow::Result<()> {
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
fflags_strategy: FflagsStrategy::Never,
mac_metadata_strategy: MacMetadataStrategy::Never,
};
let owner_options = OwnerOptions::new(
args.uname,
Expand Down
76 changes: 60 additions & 16 deletions cli/src/command/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
command::{
Command, ask_password,
core::{
AclStrategy, FflagsStrategy, KeepOptions, OwnerOptions, PathFilter, PathTransformers,
PathnameEditor, PermissionStrategy, TimeFilterResolver, TimeFilters, TimestampStrategy,
TimestampStrategyResolver, XattrStrategy, apply_chroot, collect_split_archives,
AclStrategy, FflagsStrategy, KeepOptions, MacMetadataStrategy, OwnerOptions,
PathFilter, PathTransformers, PathnameEditor, PermissionStrategy, TimeFilterResolver,
TimeFilters, TimestampStrategy, TimestampStrategyResolver, XattrStrategy, apply_chroot,
collect_split_archives,
path_lock::PathLocks,
re::{bsd::SubstitutionRule, gnu::TransformRule},
read_paths, run_process_archive,
Expand Down Expand Up @@ -420,6 +421,7 @@
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
fflags_strategy: FflagsStrategy::Never,
mac_metadata_strategy: MacMetadataStrategy::Never,
};
let owner_options = OwnerOptions::new(
args.uname,
Expand Down Expand Up @@ -872,9 +874,9 @@
Ok(())
}

/// Restores file metadata (permissions, extended attributes, and ACLs) for an extracted entry according to the provided keep and owner options.
/// Restores file metadata (permissions, extended attributes, ACLs, and macOS metadata) for an extracted entry according to the provided keep and owner options.
///
/// Permissions are applied when `keep_options.permission_strategy` is `Always`. Extended attributes are applied on Unix when `keep_options.xattr_strategy` is `Always` (logs a warning if the filesystem or platform does not support xattrs). ACLs are restored when the `acl` feature is enabled and `keep_options.acl_strategy` requests them; if the `acl` feature is not compiled in but ACLs were requested, a warning is emitted.
/// Permissions are applied when `keep_options.permission_strategy` is `Always`. Extended attributes are applied on Unix when `keep_options.xattr_strategy` is `Always` (logs a warning if the filesystem or platform does not support xattrs). ACLs are restored when the `acl` feature is enabled and `keep_options.acl_strategy` requests them; if the `acl` feature is not compiled in but ACLs were requested, a warning is emitted. macOS metadata (AppleDouble) is restored when `keep_options.mac_metadata_strategy` is `Always` on macOS.
fn restore_metadata<T>(
item: &NormalEntry<T>,
path: &Path,
Expand All @@ -890,26 +892,41 @@
restore_permissions(*same_owner, path, p, owner_options)?;
}
}
// On macOS, when mac_metadata_strategy is Always and the entry has mac_metadata,
// AppleDouble restoration via copyfile() will include xattrs and ACLs.
// Skip separate handling to avoid duplication.
#[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 let XattrStrategy::Always = keep_options.xattr_strategy {
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
);
if !skip_xattr_acl {
if let XattrStrategy::Always = keep_options.xattr_strategy {
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),
}
Err(e) => return Err(e),
}
}

Check warning

Code scanning / clippy

this if statement can be collapsed Warning

this if statement can be collapsed

Check warning

Code scanning / clippy

this if statement can be collapsed Warning

this if statement can be collapsed
#[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")]
restore_acls(path, item.acl()?, keep_options.acl_strategy)?;
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.");
Expand All @@ -930,6 +947,33 @@
}
}
}
// macOS metadata (AppleDouble) - restores xattrs, ACLs, resource forks via copyfile()
#[cfg(target_os = "macos")]
if let MacMetadataStrategy::Always = keep_options.mac_metadata_strategy {
if 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 let MacMetadataStrategy::Always = keep_options.mac_metadata_strategy {
if item.mac_metadata().is_some() {
log::warn!(
"macOS metadata present but cannot be restored on this platform: '{}'",
path.display()
);
}
}
Comment thread
ChanTsune marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread Dismissed
Comment thread Dismissed
Ok(())
}

Expand Down
Loading
Loading