Skip to content

Commit 302f082

Browse files
committed
cp: fix directory permission preservation for sibling directories with -a (Closes: #8407)
1 parent f36bf1d commit 302f082

File tree

2 files changed

+57
-20
lines changed

2 files changed

+57
-20
lines changed

src/uu/cp/src/copydir.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ pub(crate) fn copy_directory(
377377
// The directory we were in during the previous iteration
378378
let mut last_iter: Option<DirEntry> = None;
379379

380+
// Keep track of all directories we've created that need permission fixes
381+
let mut dirs_needing_permissions: Vec<(PathBuf, PathBuf)> = Vec::new();
382+
380383
// Traverse the contents of the directory, copying each one.
381384
for direntry_result in WalkDir::new(root)
382385
.same_file_system(options.one_file_system)
@@ -408,6 +411,14 @@ pub(crate) fn copy_directory(
408411
// `./a/b/c` into `./a/`, in which case we'll need to fix the
409412
// permissions of both `./a/b/c` and `./a/b`, in that order.)
410413
if direntry.file_type().is_dir() {
414+
// Add this directory to our list for permission fixing later
415+
let entry_for_tracking =
416+
Entry::new(&context, direntry.path(), options.no_target_dir)?;
417+
dirs_needing_permissions.push((
418+
entry_for_tracking.source_absolute,
419+
entry_for_tracking.local_to_target,
420+
));
421+
411422
// If true, last_iter is not a parent of this iter.
412423
// The means we just exited a directory.
413424
let went_up = if let Some(last_iter) = &last_iter {
@@ -452,25 +463,10 @@ pub(crate) fn copy_directory(
452463
}
453464
}
454465

455-
// Handle final directory permission fixes.
456-
// This is almost the same as the permission-fixing code above,
457-
// with minor differences (commented)
458-
if let Some(last_iter) = last_iter {
459-
let diff = last_iter.path().strip_prefix(root).unwrap();
460-
461-
// Do _not_ skip `.` this time, since we know we're done.
462-
// This is where we fix the permissions of the top-level
463-
// directory we just copied.
464-
for p in diff.ancestors() {
465-
let src = root.join(p);
466-
let entry = Entry::new(&context, &src, options.no_target_dir)?;
467-
468-
copy_attributes(
469-
&entry.source_absolute,
470-
&entry.local_to_target,
471-
&options.attributes,
472-
)?;
473-
}
466+
// Fix permissions for all directories we created
467+
// This ensures that even sibling directories get their permissions fixed
468+
for (source_path, dest_path) in dirs_needing_permissions {
469+
copy_attributes(&source_path, &dest_path, &options.attributes)?;
474470
}
475471

476472
// Also fix permissions for parent directories,

tests/by-util/test_cp.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// file that was distributed with this source code.
55

66
// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR outfile uufs xattrs
7-
// spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel prwx doesnotexist
7+
// spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel prwx doesnotexist reftests subdirs
88
use uucore::display::Quotable;
99
#[cfg(feature = "feat_selinux")]
1010
use uucore::selinux::get_getfattr_output;
@@ -6279,6 +6279,47 @@ fn test_cp_preserve_xattr_readonly_source() {
62796279
);
62806280
}
62816281

6282+
#[test]
6283+
#[cfg(unix)]
6284+
fn test_cp_archive_preserves_directory_permissions() {
6285+
// Test for issue #8407
6286+
let (at, mut ucmd) = at_and_ucmd!();
6287+
6288+
at.mkdir("test-images");
6289+
6290+
let subdirs = ["fail", "gif-test-suite", "randomly-modified", "reftests"];
6291+
let mode = 0o755;
6292+
6293+
for (i, subdir) in subdirs.iter().enumerate() {
6294+
let path = format!("test-images/{subdir}");
6295+
at.mkdir(&path);
6296+
at.set_mode(&path, mode);
6297+
at.write(&format!("{path}/test{}.txt", i + 1), "test content");
6298+
}
6299+
6300+
ucmd.arg("-a")
6301+
.arg("test-images")
6302+
.arg("test-images-copy")
6303+
.succeeds();
6304+
6305+
let check_mode = |path: &str| {
6306+
let metadata = at.metadata(path);
6307+
let mode = metadata.permissions().mode();
6308+
// Check that the permissions are 755 (only checking the last 9 bits)
6309+
assert_eq!(
6310+
mode & 0o777,
6311+
0o755,
6312+
"Directory {} has incorrect permissions: {:o}",
6313+
path,
6314+
mode & 0o777
6315+
);
6316+
};
6317+
6318+
for subdir in subdirs {
6319+
check_mode(&format!("test-images-copy/{subdir}"));
6320+
}
6321+
}
6322+
62826323
#[test]
62836324
#[cfg(unix)]
62846325
fn test_cp_from_stdin() {

0 commit comments

Comments
 (0)