Skip to content

Commit 8205cb9

Browse files
committed
⚗️ Add --fflags support and document flag semantics
Add the `--fflags` CLI option and store file flags in a private chunk. Document Linux (merge) vs BSD/macOS (overwrite) behavior to match libarchive/bsdtar’s cross-platform differences.
1 parent be353a7 commit 8205cb9

12 files changed

Lines changed: 464 additions & 62 deletions

File tree

cli/src/chunk.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
mod acl;
2+
mod fflag;
23

34
pub use acl::*;
5+
pub use fflag::*;

cli/src/chunk/fflag.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use pna::{ChunkType, RawChunk};
2+
3+
/// Private chunk type for file flags (fflags).
4+
/// Name follows PNA chunk naming convention where case has semantic meaning:
5+
/// - lowercase first letter: ancillary (not critical)
6+
/// - lowercase second letter: private (not public)
7+
/// - uppercase third letter: reserved
8+
/// - lowercase fourth letter: safe to copy
9+
#[allow(non_upper_case_globals)]
10+
pub const ffLg: ChunkType = unsafe { ChunkType::from_unchecked(*b"ffLg") };
11+
12+
pub fn fflag_chunk(flag: &str) -> RawChunk {
13+
RawChunk::from_data(ffLg, flag.as_bytes())
14+
}

cli/src/command/append.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use crate::{
66
command::{
77
Command, ask_password, check_password,
88
core::{
9-
AclStrategy, CollectOptions, CollectedItem, CreateOptions, KeepOptions, OwnerOptions,
10-
PathFilter, PathTransformers, PathnameEditor, PermissionStrategy, TimeFilterResolver,
11-
TimestampStrategyResolver, XattrStrategy, collect_items_from_paths, create_entry,
12-
entry_option,
9+
AclStrategy, CollectOptions, CollectedItem, CreateOptions, FflagsStrategy, KeepOptions,
10+
OwnerOptions, PathFilter, PathTransformers, PathnameEditor, PermissionStrategy,
11+
TimeFilterResolver, TimestampStrategyResolver, XattrStrategy, collect_items_from_paths,
12+
create_entry, entry_option,
1313
re::{bsd::SubstitutionRule, gnu::TransformRule},
1414
read_paths, read_paths_stdin,
1515
},
@@ -391,6 +391,7 @@ fn append_to_archive(args: AppendCommand) -> anyhow::Result<()> {
391391
),
392392
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
393393
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
394+
fflags_strategy: FflagsStrategy::Never,
394395
};
395396
let owner_options = OwnerOptions::new(
396397
args.uname,

cli/src/command/core.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,31 @@ impl AclStrategy {
161161
}
162162
}
163163

164+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
165+
pub(crate) enum FflagsStrategy {
166+
Never,
167+
Always,
168+
}
169+
170+
impl FflagsStrategy {
171+
pub(crate) const fn from_flags(keep_fflags: bool, no_keep_fflags: bool) -> Self {
172+
if no_keep_fflags {
173+
Self::Never
174+
} else if keep_fflags {
175+
Self::Always
176+
} else {
177+
Self::Never
178+
}
179+
}
180+
}
181+
164182
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
165183
pub(crate) struct KeepOptions {
166184
pub(crate) timestamp_strategy: TimestampStrategy,
167185
pub(crate) permission_strategy: PermissionStrategy,
168186
pub(crate) xattr_strategy: XattrStrategy,
169187
pub(crate) acl_strategy: AclStrategy,
188+
pub(crate) fflags_strategy: FflagsStrategy,
170189
}
171190

172191
/// Resolves CLI timestamp options into a `TimestampStrategy`.
@@ -769,6 +788,23 @@ pub(crate) fn apply_metadata(
769788
if let XattrStrategy::Always = keep_options.xattr_strategy {
770789
log::warn!("Currently extended attribute is not supported on this platform.");
771790
}
791+
if let FflagsStrategy::Always = keep_options.fflags_strategy {
792+
match utils::fs::get_flags(path) {
793+
Ok(flags) => {
794+
for flag in flags {
795+
entry.add_extra_chunk(crate::chunk::fflag_chunk(&flag));
796+
}
797+
}
798+
Err(e) if e.kind() == std::io::ErrorKind::Unsupported => {
799+
log::warn!(
800+
"File flags are not supported on filesystem for '{}': {}",
801+
path.display(),
802+
e
803+
);
804+
}
805+
Err(e) => return Err(e),
806+
}
807+
}
772808
Ok(entry)
773809
}
774810

cli/src/command/create.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
command::{
77
Command, ask_password, check_password,
88
core::{
9-
AclStrategy, CollectOptions, CollectedItem, CreateOptions, KeepOptions,
9+
AclStrategy, CollectOptions, CollectedItem, CreateOptions, FflagsStrategy, KeepOptions,
1010
MIN_SPLIT_PART_BYTES, OwnerOptions, PathFilter, PathTransformers, PathnameEditor,
1111
PermissionStrategy, TimeFilterResolver, TimestampStrategyResolver, XattrStrategy,
1212
collect_items_from_paths, create_entry, entry_option,
@@ -477,6 +477,7 @@ fn create_archive(args: CreateCommand) -> anyhow::Result<()> {
477477
),
478478
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
479479
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
480+
fflags_strategy: FflagsStrategy::Never,
480481
};
481482
let owner_options = OwnerOptions::new(
482483
args.uname,

cli/src/command/extract.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#[cfg(feature = "memmap")]
22
use crate::command::core::run_entries;
3-
#[cfg(feature = "acl")]
43
use crate::ext::*;
54
#[cfg(any(unix, windows))]
65
use crate::utils::fs::lchown;
@@ -9,8 +8,8 @@ use crate::{
98
command::{
109
Command, ask_password,
1110
core::{
12-
AclStrategy, KeepOptions, OwnerOptions, PathFilter, PathTransformers, PathnameEditor,
13-
PermissionStrategy, TimeFilterResolver, TimeFilters, TimestampStrategy,
11+
AclStrategy, FflagsStrategy, KeepOptions, OwnerOptions, PathFilter, PathTransformers,
12+
PathnameEditor, PermissionStrategy, TimeFilterResolver, TimeFilters, TimestampStrategy,
1413
TimestampStrategyResolver, XattrStrategy, apply_chroot, collect_split_archives,
1514
path_lock::PathLocks,
1615
re::{bsd::SubstitutionRule, gnu::TransformRule},
@@ -420,6 +419,7 @@ fn extract_archive(args: ExtractCommand) -> anyhow::Result<()> {
420419
),
421420
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
422421
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
422+
fflags_strategy: FflagsStrategy::Never,
423423
};
424424
let owner_options = OwnerOptions::new(
425425
args.uname,
@@ -914,6 +914,22 @@ where
914914
if let AclStrategy::Always = keep_options.acl_strategy {
915915
log::warn!("Please enable `acl` feature and rebuild and install pna.");
916916
}
917+
if let FflagsStrategy::Always = keep_options.fflags_strategy {
918+
let flags = item.fflags();
919+
if !flags.is_empty() {
920+
match utils::fs::set_flags(path, &flags) {
921+
Ok(()) => {}
922+
Err(e) if e.kind() == std::io::ErrorKind::Unsupported => {
923+
log::warn!(
924+
"File flags are not supported on filesystem for '{}': {}",
925+
path.display(),
926+
e
927+
);
928+
}
929+
Err(e) => return Err(e),
930+
}
931+
}
932+
}
917933
Ok(())
918934
}
919935

cli/src/command/stdio.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use crate::{
88
append::{open_archive_then_seek_to_end, run_append_archive},
99
ask_password, check_password,
1010
core::{
11-
AclStrategy, CollectOptions, CreateOptions, KeepOptions, OwnerOptions, PathFilter,
12-
PathTransformers, PathnameEditor, PermissionStrategy, TimeFilterResolver,
11+
AclStrategy, CollectOptions, CreateOptions, FflagsStrategy, KeepOptions, OwnerOptions,
12+
PathFilter, PathTransformers, PathnameEditor, PermissionStrategy, TimeFilterResolver,
1313
TimestampStrategyResolver, TransformStrategyUnSolid, XattrStrategy, apply_chroot,
1414
collect_items_from_paths, collect_split_archives, entry_option,
1515
path_lock::PathLocks,
@@ -60,6 +60,7 @@ use std::{env, io, path::PathBuf, sync::Arc, time::SystemTime};
6060
group(ArgGroup::new("ctime-newer-than-source").args(["newer_ctime", "newer_ctime_than"])),
6161
group(ArgGroup::new("mtime-older-than-source").args(["older_mtime", "older_mtime_than"])),
6262
group(ArgGroup::new("mtime-newer-than-source").args(["newer_mtime", "newer_mtime_than"])),
63+
group(ArgGroup::new("keep-fflags-flag").args(["keep_fflags", "no_keep_fflags"])),
6364
)]
6465
#[cfg_attr(windows, command(
6566
group(ArgGroup::new("windows-unstable-keep-permission").args(["keep_permission", "no_keep_permission"]).requires("unstable")),
@@ -187,6 +188,20 @@ pub(crate) struct StdioCommand {
187188
help = "Do not archive ACLs. This is the inverse option of --keep-acl (unstable)"
188189
)]
189190
no_keep_acl: bool,
191+
#[arg(
192+
long,
193+
visible_aliases = ["preserve-fflags", "fflags"],
194+
requires = "unstable",
195+
help = "Archiving the file flags of the files (unstable)"
196+
)]
197+
keep_fflags: bool,
198+
#[arg(
199+
long,
200+
visible_aliases = ["no-preserve-fflags", "no-fflags"],
201+
requires = "unstable",
202+
help = "Do not archive file flags of files. This is the inverse option of --keep-fflags (unstable)"
203+
)]
204+
no_keep_fflags: bool,
190205
#[arg(
191206
long,
192207
help = "Compress multiple files together for better compression ratio"
@@ -638,6 +653,7 @@ fn run_create_archive(args: StdioCommand) -> anyhow::Result<()> {
638653
),
639654
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
640655
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
656+
fflags_strategy: FflagsStrategy::from_flags(args.keep_fflags, args.no_keep_fflags),
641657
};
642658
let owner_options = resolve_owner_options(
643659
args.owner,
@@ -731,6 +747,7 @@ fn run_extract_archive(args: StdioCommand) -> anyhow::Result<()> {
731747
),
732748
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
733749
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
750+
fflags_strategy: FflagsStrategy::from_flags(args.keep_fflags, args.no_keep_fflags),
734751
},
735752
owner_options: resolve_owner_options(
736753
args.owner,
@@ -889,6 +906,7 @@ fn run_append(args: StdioCommand) -> anyhow::Result<()> {
889906
),
890907
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
891908
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
909+
fflags_strategy: FflagsStrategy::from_flags(args.keep_fflags, args.no_keep_fflags),
892910
};
893911
let owner_options = resolve_owner_options(
894912
args.owner,
@@ -1025,6 +1043,7 @@ fn run_update(args: StdioCommand) -> anyhow::Result<()> {
10251043
),
10261044
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
10271045
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
1046+
fflags_strategy: FflagsStrategy::Never,
10281047
};
10291048
let owner_options = OwnerOptions::new(
10301049
args.uname,

cli/src/command/update.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ use crate::{
1010
command::{
1111
Command, ask_password, check_password,
1212
core::{
13-
AclStrategy, CollectOptions, CollectedItem, CreateOptions, KeepOptions, OwnerOptions,
14-
PathFilter, PathTransformers, PathnameEditor, PermissionStrategy, TimeFilterResolver,
15-
TimestampStrategyResolver, TransformStrategy, TransformStrategyKeepSolid,
16-
TransformStrategyUnSolid, XattrStrategy, collect_items_from_paths,
17-
collect_split_archives, create_entry, entry_option,
13+
AclStrategy, CollectOptions, CollectedItem, CreateOptions, FflagsStrategy, KeepOptions,
14+
OwnerOptions, PathFilter, PathTransformers, PathnameEditor, PermissionStrategy,
15+
TimeFilterResolver, TimestampStrategyResolver, TransformStrategy,
16+
TransformStrategyKeepSolid, TransformStrategyUnSolid, XattrStrategy,
17+
collect_items_from_paths, collect_split_archives, create_entry, entry_option,
1818
re::{bsd::SubstitutionRule, gnu::TransformRule},
1919
read_paths, read_paths_stdin,
2020
},
@@ -400,6 +400,7 @@ fn update_archive(args: UpdateCommand) -> anyhow::Result<()> {
400400
),
401401
xattr_strategy: XattrStrategy::from_flags(args.keep_xattr, args.no_keep_xattr),
402402
acl_strategy: AclStrategy::from_flags(args.keep_acl, args.no_keep_acl),
403+
fflags_strategy: FflagsStrategy::Never,
403404
};
404405
let owner_options = OwnerOptions::new(
405406
args.uname,

cli/src/ext.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(crate) type Acls = HashMap<AcePlatform, Vec<Ace>>;
1313

1414
pub(crate) trait NormalEntryExt {
1515
fn acl(&self) -> io::Result<Acls>;
16+
fn fflags(&self) -> Vec<String>;
1617
}
1718

1819
impl<T> NormalEntryExt for NormalEntry<T>
@@ -43,6 +44,20 @@ where
4344
}
4445
Ok(acls)
4546
}
47+
48+
#[inline]
49+
fn fflags(&self) -> Vec<String> {
50+
self.extra_chunks()
51+
.iter()
52+
.filter_map(|c| {
53+
if c.ty() == chunk::ffLg {
54+
std::str::from_utf8(c.data()).ok().map(str::to_string)
55+
} else {
56+
None
57+
}
58+
})
59+
.collect()
60+
}
4661
}
4762

4863
pub(crate) trait PermissionExt {

cli/src/utils/fs.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,50 @@ pub(crate) fn lchown<P: AsRef<Path>>(
6666
inner(path.as_ref(), owner, group)
6767
}
6868

69+
pub(crate) fn get_flags<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {
70+
#[cfg(any(
71+
target_os = "macos",
72+
target_os = "linux",
73+
target_os = "android",
74+
target_os = "freebsd"
75+
))]
76+
fn inner(path: &Path) -> io::Result<Vec<String>> {
77+
crate::utils::os::unix::fs::get_flags(path)
78+
}
79+
#[cfg(not(any(
80+
target_os = "macos",
81+
target_os = "linux",
82+
target_os = "android",
83+
target_os = "freebsd"
84+
)))]
85+
fn inner(_path: &Path) -> io::Result<Vec<String>> {
86+
Ok(Vec::new())
87+
}
88+
inner(path.as_ref())
89+
}
90+
91+
pub(crate) fn set_flags<P: AsRef<Path>>(path: P, flags: &[String]) -> io::Result<()> {
92+
#[cfg(any(
93+
target_os = "macos",
94+
target_os = "linux",
95+
target_os = "android",
96+
target_os = "freebsd"
97+
))]
98+
fn inner(path: &Path, flags: &[String]) -> io::Result<()> {
99+
crate::utils::os::unix::fs::set_flags(path, flags)
100+
}
101+
#[cfg(not(any(
102+
target_os = "macos",
103+
target_os = "linux",
104+
target_os = "android",
105+
target_os = "freebsd"
106+
)))]
107+
fn inner(_path: &Path, _flags: &[String]) -> io::Result<()> {
108+
Ok(())
109+
}
110+
inner(path.as_ref(), flags)
111+
}
112+
69113
#[inline]
70114
pub(crate) fn file_create(path: impl AsRef<Path>, overwrite: bool) -> io::Result<fs::File> {
71115
if overwrite {

0 commit comments

Comments
 (0)