Skip to content

Commit 5eee453

Browse files
committed
💥 Set missing chmod and chown metadata
1 parent b5edfa7 commit 5eee453

4 files changed

Lines changed: 162 additions & 20 deletions

File tree

cli/src/command/chmod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use nom::{
1818
combinator::{map, opt},
1919
multi::{many0, many1, separated_list1},
2020
};
21-
use pna::NormalEntry;
21+
use pna::{DataKind, NormalEntry};
2222
use std::{ops::BitOr, path::PathBuf, str::FromStr};
2323

2424
#[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
@@ -100,9 +100,9 @@ fn archive_chmod(args: ChmodCommand) -> anyhow::Result<()> {
100100
fn transform_entry<T>(entry: NormalEntry<T>, mode: &Mode) -> NormalEntry<T> {
101101
let metadata = entry.metadata().clone();
102102
let own = crate::ext::ResolvedOwnership::from_metadata(&metadata);
103-
let Some(cur_mode) = own.mode else {
104-
return entry.with_metadata(metadata);
105-
};
103+
let cur_mode = own
104+
.mode
105+
.unwrap_or_else(|| default_permission_mode(entry.header().data_kind()));
106106
let new_mode = mode.apply_to(cur_mode);
107107
let metadata = metadata
108108
.with_permission(None)
@@ -132,6 +132,14 @@ fn transform_entry<T>(entry: NormalEntry<T>, mode: &Mode) -> NormalEntry<T> {
132132
entry.with_metadata(metadata)
133133
}
134134

135+
#[inline]
136+
const fn default_permission_mode(kind: DataKind) -> u16 {
137+
match kind {
138+
DataKind::Directory => 0o755,
139+
DataKind::File | DataKind::SymbolicLink | DataKind::HardLink => 0o644,
140+
}
141+
}
142+
135143
bitflags! {
136144
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
137145
pub(crate) struct Who: u8 {

cli/src/command/chown.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,6 @@ fn archive_chown(args: ChownCommand) -> anyhow::Result<()> {
109109
fn transform_entry<T>(entry: NormalEntry<T>, owner: &Ownership) -> NormalEntry<T> {
110110
let metadata = entry.metadata().clone();
111111
let own = crate::ext::ResolvedOwnership::from_metadata(&metadata);
112-
if own.is_empty() {
113-
return entry.with_metadata(metadata);
114-
}
115112
let (uid, uname): (Option<u64>, String) = match owner.user.as_ref() {
116113
Some(OwnerSpecifier::Name(uname)) => (Some(u64::MAX), uname.clone()),
117114
Some(OwnerSpecifier::ID(uid)) => (Some(*uid), String::new()),

cli/tests/cli/chmod.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,118 @@ fn archive_chmod_drops_legacy_fprm() {
120120
})
121121
.unwrap();
122122
}
123+
124+
/// Precondition: An archive entry has no permission metadata.
125+
/// Action: Run `pna experimental chmod 600` on that entry.
126+
/// Expectation: The requested mode is emitted as fMOd without generating legacy fPRM.
127+
#[test]
128+
#[allow(deprecated)]
129+
fn archive_chmod_numeric_sets_missing_permission_mode() {
130+
setup();
131+
let path = "chmod_missing_mode_numeric.pna";
132+
{
133+
let mut archive = Archive::write_header(File::create(path).unwrap()).unwrap();
134+
let mut builder = EntryBuilder::new_file(
135+
EntryName::from_utf8_preserve_root(ENTRY_PATH),
136+
WriteOptions::store(),
137+
)
138+
.unwrap();
139+
builder.write_all(ENTRY_CONTENT).unwrap();
140+
archive.add_entry(builder.build().unwrap()).unwrap();
141+
archive.finalize().unwrap();
142+
}
143+
144+
cli::Cli::try_parse_from([
145+
"pna",
146+
"--quiet",
147+
"experimental",
148+
"chmod",
149+
"-f",
150+
path,
151+
"600",
152+
ENTRY_PATH,
153+
])
154+
.unwrap()
155+
.execute()
156+
.unwrap();
157+
158+
archive::for_each_entry(path, |entry| {
159+
assert!(
160+
entry.metadata().permission().is_none(),
161+
"legacy fPRM must not be generated by chmod"
162+
);
163+
assert_eq!(entry.metadata().permission_mode().unwrap().get(), 0o600);
164+
})
165+
.unwrap();
166+
}
167+
168+
/// Precondition: Archive entries of each supported kind have no permission metadata.
169+
/// Action: Run symbolic `pna experimental chmod a-r` on all entries.
170+
/// Expectation: Missing modes use default bases: directory 0755, all other
171+
/// entry kinds 0644.
172+
#[test]
173+
fn archive_chmod_symbolic_uses_default_mode_for_missing_permission() {
174+
setup();
175+
let path = "chmod_missing_mode_symbolic.pna";
176+
{
177+
let mut archive = Archive::write_header(File::create(path).unwrap()).unwrap();
178+
179+
let mut file = EntryBuilder::new_file("file.txt".into(), WriteOptions::store()).unwrap();
180+
file.write_all(b"file").unwrap();
181+
archive.add_entry(file.build().unwrap()).unwrap();
182+
183+
archive
184+
.add_entry(EntryBuilder::new_dir("dir".into()).build().unwrap())
185+
.unwrap();
186+
187+
archive
188+
.add_entry(
189+
EntryBuilder::new_symlink("link.txt".into(), "file.txt".into())
190+
.unwrap()
191+
.build()
192+
.unwrap(),
193+
)
194+
.unwrap();
195+
196+
archive
197+
.add_entry(
198+
EntryBuilder::new_hard_link("hard.txt".into(), "file.txt".into())
199+
.unwrap()
200+
.build()
201+
.unwrap(),
202+
)
203+
.unwrap();
204+
205+
archive.finalize().unwrap();
206+
}
207+
208+
cli::Cli::try_parse_from([
209+
"pna",
210+
"--quiet",
211+
"experimental",
212+
"chmod",
213+
"-f",
214+
path,
215+
"a-r",
216+
"file.txt",
217+
"dir",
218+
"link.txt",
219+
"hard.txt",
220+
])
221+
.unwrap()
222+
.execute()
223+
.unwrap();
224+
225+
archive::for_each_entry(path, |entry| {
226+
let expected = match entry.header().path().as_str() {
227+
"dir" => 0o311,
228+
"file.txt" | "link.txt" | "hard.txt" => 0o200,
229+
other => panic!("unexpected entry: {other}"),
230+
};
231+
assert_eq!(
232+
entry.metadata().permission_mode().unwrap().get() & 0o777,
233+
expected
234+
);
235+
})
236+
.unwrap();
237+
}

cli/tests/cli/chown/preserve_owner_absence.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::io::Write;
99
/// uid/gid/name), plus an entry with no ownership metadata at all.
1010
/// Action: `pna experimental chown` changing only the user.
1111
/// Expectation: The un-overridden group side stays absent (no gid facet, never
12-
/// a synthesized 0); the overridden user side is set; mode is preserved; the
13-
/// no-ownership entry is left untouched.
12+
/// a synthesized 0); the overridden user side is set even for a metadata-empty
13+
/// entry; existing mode is preserved; absent mode stays absent.
1414
#[test]
1515
fn chown_user_only_preserves_missing_gid() {
1616
setup();
@@ -44,6 +44,7 @@ fn chown_user_only_preserves_missing_gid() {
4444
path,
4545
"new_user",
4646
"mode_only.txt",
47+
"bare.txt",
4748
"--no-owner-lookup",
4849
])
4950
.unwrap()
@@ -69,9 +70,9 @@ fn chown_user_only_preserves_missing_gid() {
6970
assert_eq!(m.permission_mode().unwrap().get(), 0o644);
7071
}
7172
"bare.txt" => {
72-
assert!(m.owner_uid().is_none());
73+
assert_eq!(m.owner_user_name().unwrap().as_str(), "new_user");
74+
assert_eq!(m.owner_uid().unwrap().get(), u64::MAX);
7375
assert!(m.owner_gid().is_none());
74-
assert!(m.owner_user_name().is_none());
7576
assert!(m.owner_group_name().is_none());
7677
assert!(m.permission_mode().is_none());
7778
}
@@ -82,7 +83,8 @@ fn chown_user_only_preserves_missing_gid() {
8283
assert_eq!(count, 2, "archive should contain exactly 2 entries");
8384
}
8485

85-
/// Precondition: An archive entry carries only a permission mode (no owner uid/gid).
86+
/// Precondition: Archive entries carry either only a permission mode or no
87+
/// ownership metadata at all.
8688
/// Action: `pna experimental chown` changing only the group (`:new_grp`).
8789
/// Expectation: The un-overridden user side stays absent (no uid facet, never a synthesized 0).
8890
#[test]
@@ -99,6 +101,13 @@ fn chown_group_only_preserves_missing_uid() {
99101
mo.permission_mode(pna::PermissionMode::from(0o600));
100102
mo.write_all(b"m").unwrap();
101103
a.add_entry(mo.build().unwrap()).unwrap();
104+
let mut bare = EntryBuilder::new_file(
105+
EntryName::from_utf8_preserve_root("bare.txt"),
106+
WriteOptions::store(),
107+
)
108+
.unwrap();
109+
bare.write_all(b"x").unwrap();
110+
a.add_entry(bare.build().unwrap()).unwrap();
102111
a.finalize().unwrap();
103112
}
104113

@@ -111,6 +120,7 @@ fn chown_group_only_preserves_missing_uid() {
111120
path,
112121
":new_grp",
113122
"mode_only.txt",
123+
"bare.txt",
114124
"--no-owner-lookup",
115125
])
116126
.unwrap()
@@ -119,14 +129,26 @@ fn chown_group_only_preserves_missing_uid() {
119129

120130
archive::for_each_entry(path, |entry| {
121131
let m = entry.metadata();
122-
assert_eq!(m.owner_group_name().unwrap().as_str(), "new_grp");
123-
assert_eq!(m.owner_gid().unwrap().get(), u64::MAX);
124-
assert!(
125-
m.owner_uid().is_none(),
126-
"un-overridden uid must stay absent, not synthesized to 0"
127-
);
128-
assert!(m.owner_user_name().is_none());
129-
assert_eq!(m.permission_mode().unwrap().get(), 0o600);
132+
match entry.header().path().as_str() {
133+
"mode_only.txt" => {
134+
assert_eq!(m.owner_group_name().unwrap().as_str(), "new_grp");
135+
assert_eq!(m.owner_gid().unwrap().get(), u64::MAX);
136+
assert!(
137+
m.owner_uid().is_none(),
138+
"un-overridden uid must stay absent, not synthesized to 0"
139+
);
140+
assert!(m.owner_user_name().is_none());
141+
assert_eq!(m.permission_mode().unwrap().get(), 0o600);
142+
}
143+
"bare.txt" => {
144+
assert_eq!(m.owner_group_name().unwrap().as_str(), "new_grp");
145+
assert_eq!(m.owner_gid().unwrap().get(), u64::MAX);
146+
assert!(m.owner_uid().is_none());
147+
assert!(m.owner_user_name().is_none());
148+
assert!(m.permission_mode().is_none());
149+
}
150+
other => panic!("unexpected entry: {other}"),
151+
}
130152
})
131153
.unwrap();
132154
}

0 commit comments

Comments
 (0)