Skip to content

Commit be462bf

Browse files
committed
cp: fix recursive copy of readonly directories
1 parent da1d3a1 commit be462bf

3 files changed

Lines changed: 132 additions & 15 deletions

File tree

src/uu/cp/src/copydir.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ impl Entry {
237237

238238
#[allow(clippy::too_many_arguments)]
239239
/// Copy a single entry during a directory traversal.
240+
/// Returns a bool value indicating whether this function created a directory or not
240241
fn copy_direntry(
241242
progress_bar: Option<&ProgressBar>,
242243
entry: &Entry,
@@ -248,7 +249,7 @@ fn copy_direntry(
248249
copied_destinations: &HashSet<PathBuf>,
249250
copied_files: &mut HashMap<FileInformation, PathBuf>,
250251
created_parent_dirs: &mut HashSet<PathBuf>,
251-
) -> CopyResult<()> {
252+
) -> CopyResult<bool> {
252253
let source_is_symlink = entry_is_symlink;
253254
let source_is_dir = if source_is_symlink && !options.dereference {
254255
false
@@ -276,7 +277,7 @@ fn copy_direntry(
276277
context_for(&entry.source_relative, &entry.local_to_target)
277278
);
278279
}
279-
Ok(())
280+
Ok(true)
280281
};
281282
}
282283

@@ -326,7 +327,7 @@ fn copy_direntry(
326327

327328
// In any other case, there is nothing to do, so we just return to
328329
// continue the traversal.
329-
Ok(())
330+
Ok(false)
330331
}
331332

332333
/// Read the contents of the directory `root` and recursively copy the
@@ -421,7 +422,7 @@ pub(crate) fn copy_directory(
421422
let mut last_iter: Option<DirEntry> = None;
422423

423424
// Keep track of all directories we've created that need permission fixes
424-
let mut dirs_needing_permissions: Vec<(PathBuf, PathBuf)> = Vec::new();
425+
let mut dirs_needing_permissions: Vec<(PathBuf, PathBuf, bool)> = Vec::new();
425426

426427
// Traverse the contents of the directory, copying each one.
427428
for direntry_result in WalkDir::new(root)
@@ -442,7 +443,7 @@ pub(crate) fn copy_directory(
442443
};
443444
let entry = Entry::new(&context, direntry_path, options.no_target_dir)?;
444445

445-
copy_direntry(
446+
let created = copy_direntry(
446447
progress_bar,
447448
&entry,
448449
entry_is_symlink,
@@ -475,12 +476,16 @@ pub(crate) fn copy_directory(
475476
&entry.source_absolute,
476477
&entry.local_to_target,
477478
&options.attributes,
479+
false,
478480
)?;
479481
continue;
480482
}
481483
// Add this directory to our list for permission fixing later
482-
dirs_needing_permissions
483-
.push((entry.source_absolute.clone(), entry.local_to_target.clone()));
484+
dirs_needing_permissions.push((
485+
entry.source_absolute.clone(),
486+
entry.local_to_target.clone(),
487+
created,
488+
));
484489

485490
// If true, last_iter is not a parent of this iter.
486491
// The means we just exited a directory.
@@ -513,6 +518,7 @@ pub(crate) fn copy_directory(
513518
&entry.source_absolute,
514519
&entry.local_to_target,
515520
&options.attributes,
521+
false,
516522
)?;
517523
}
518524
}
@@ -528,8 +534,8 @@ pub(crate) fn copy_directory(
528534

529535
// Fix permissions for all directories we created
530536
// This ensures that even sibling directories get their permissions fixed
531-
for (source_path, dest_path) in dirs_needing_permissions {
532-
copy_attributes(&source_path, &dest_path, &options.attributes)?;
537+
for (source_path, dest_path, created) in dirs_needing_permissions {
538+
copy_attributes(&source_path, &dest_path, &options.attributes, created)?;
533539
}
534540

535541
// Also fix permissions for parent directories,
@@ -538,7 +544,7 @@ pub(crate) fn copy_directory(
538544
let dest = target.join(root.file_name().unwrap());
539545
for (x, y) in aligned_ancestors(root, dest.as_path()) {
540546
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
541-
copy_attributes(&src, y, &options.attributes)?;
547+
copy_attributes(&src, y, &options.attributes, false)?;
542548
}
543549
}
544550
}

src/uu/cp/src/cp.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,7 +1550,7 @@ fn copy_source(
15501550
if options.parents {
15511551
for (x, y) in aligned_ancestors(source, dest.as_path()) {
15521552
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
1553-
copy_attributes(&src, y, &options.attributes)?;
1553+
copy_attributes(&src, y, &options.attributes, false)?;
15541554
}
15551555
}
15561556
}
@@ -1717,11 +1717,21 @@ pub(crate) fn copy_attributes(
17171717
source: &Path,
17181718
dest: &Path,
17191719
attributes: &Attributes,
1720+
created: bool,
17201721
) -> CopyResult<()> {
17211722
let context = &*format!("{} -> {}", source.quote(), dest.quote());
17221723
let source_metadata =
17231724
fs::symlink_metadata(source).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?;
17241725

1726+
let is_explicit = matches!(attributes.mode, Preserve::No { explicit: true });
1727+
1728+
// preserve is true by default if the destination is created by us and it's a directory
1729+
let mode = if !is_explicit && dest.is_dir() && created {
1730+
Preserve::Yes { required: false }
1731+
} else {
1732+
attributes.mode
1733+
};
1734+
17251735
// Ownership must be changed first to avoid interfering with mode change.
17261736
#[cfg(unix)]
17271737
handle_preserve(attributes.ownership, || -> CopyResult<()> {
@@ -1759,7 +1769,7 @@ pub(crate) fn copy_attributes(
17591769
Ok(())
17601770
})?;
17611771

1762-
handle_preserve(attributes.mode, || -> CopyResult<()> {
1772+
handle_preserve(mode, || -> CopyResult<()> {
17631773
// The `chmod()` system call that underlies the
17641774
// `fs::set_permissions()` call is unable to change the
17651775
// permissions of a symbolic link. In that case, we just
@@ -2573,14 +2583,14 @@ fn copy_file(
25732583
.ok()
25742584
.filter(|p| p.exists())
25752585
.unwrap_or_else(|| source.to_path_buf());
2576-
copy_attributes(&src_for_attrs, dest, &options.attributes)?;
2586+
copy_attributes(&src_for_attrs, dest, &options.attributes, false)?;
25772587
} else if source_is_stream && !source.exists() {
25782588
// Some stream files may not exist after we have copied it,
25792589
// like anonymous pipes. Thus, we can't really copy its
25802590
// attributes. However, this is already handled in the stream
25812591
// copy function (see `copy_stream` under platform/linux.rs).
25822592
} else {
2583-
copy_attributes(source, dest, &options.attributes)?;
2593+
copy_attributes(source, dest, &options.attributes, false)?;
25842594
}
25852595

25862596
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
@@ -2762,7 +2772,7 @@ fn copy_link(
27622772
delete_path(dest, options)?;
27632773
}
27642774
symlink_file(&link, dest, symlinked_files)?;
2765-
copy_attributes(source, dest, &options.attributes)
2775+
copy_attributes(source, dest, &options.attributes, false)
27662776
}
27672777

27682778
/// Generate an error message if `target` is not the correct `target_type`

tests/by-util/test_cp.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7571,3 +7571,104 @@ fn test_cp_xattr_enotsup_handling() {
75717571
std::fs::remove_file(f).ok();
75727572
}
75737573
}
7574+
7575+
#[test]
7576+
#[cfg(not(target_os = "windows"))]
7577+
fn test_cp_preserve_directory_permissions_by_default() {
7578+
let scene = TestScenario::new(util_name!());
7579+
let at = &scene.fixtures;
7580+
7581+
let dir = "a/b/c/d";
7582+
let file = "foo.txt";
7583+
7584+
at.mkdir_all(dir);
7585+
7586+
let file_path = format!("{dir}/{file}");
7587+
7588+
at.touch(file_path);
7589+
7590+
scene.cmd("chmod").arg("-R").arg("555").arg("a").succeeds();
7591+
scene.cmd("cp").arg("-r").arg("a").arg("b").succeeds();
7592+
7593+
scene.ucmd().arg("-r").arg("a").arg("c").succeeds();
7594+
7595+
// only verify owner bits on Android
7596+
if cfg!(target_os = "android") {
7597+
assert_eq!(at.metadata("b").mode() & 0o700, 0o500);
7598+
assert_eq!(at.metadata("b/b").mode() & 0o700, 0o500);
7599+
assert_eq!(at.metadata("b/b/c").mode() & 0o700, 0o500);
7600+
assert_eq!(at.metadata("b/b/c/d").mode() & 0o700, 0o500);
7601+
7602+
assert_eq!(at.metadata("c").mode() & 0o700, 0o500);
7603+
assert_eq!(at.metadata("c/b").mode() & 0o700, 0o500);
7604+
assert_eq!(at.metadata("c/b/c").mode() & 0o700, 0o500);
7605+
assert_eq!(at.metadata("c/b/c/d").mode() & 0o700, 0o500);
7606+
} else {
7607+
assert_eq!(at.metadata("b").mode(), 0o40555);
7608+
assert_eq!(at.metadata("b/b").mode(), 0o40555);
7609+
assert_eq!(at.metadata("b/b/c").mode(), 0o40555);
7610+
assert_eq!(at.metadata("b/b/c/d").mode(), 0o40555);
7611+
7612+
assert_eq!(at.metadata("c").mode(), 0o40555);
7613+
assert_eq!(at.metadata("c/b").mode(), 0o40555);
7614+
assert_eq!(at.metadata("c/b/c").mode(), 0o40555);
7615+
assert_eq!(at.metadata("c/b/c/d").mode(), 0o40555);
7616+
}
7617+
}
7618+
7619+
#[test]
7620+
#[cfg(not(target_os = "windows"))]
7621+
fn test_cp_existing_perm_dir() {
7622+
let scene = TestScenario::new(util_name!());
7623+
let at = &scene.fixtures;
7624+
7625+
scene
7626+
.cmd("mkdir")
7627+
.arg("-p")
7628+
.arg("-m")
7629+
.arg("ug-s,u=rwx,g=rwx,o=rx")
7630+
.arg("src/dir")
7631+
.umask(0o022)
7632+
.succeeds();
7633+
scene
7634+
.cmd("mkdir")
7635+
.arg("-p")
7636+
.arg("-m")
7637+
.arg("ug-s,u=rwx,g=,o=")
7638+
.arg("dst/dir")
7639+
.umask(0o022)
7640+
.succeeds();
7641+
7642+
scene.ucmd().arg("-r").arg("src/.").arg("dst/").succeeds();
7643+
7644+
let mode = at.metadata("dst/dir").mode();
7645+
7646+
assert_eq!(mode, 0o40700);
7647+
}
7648+
7649+
#[test]
7650+
#[cfg(not(target_os = "windows"))]
7651+
fn test_cp_gnu_preserve_mode() {
7652+
use std::io;
7653+
7654+
let scene = TestScenario::new(util_name!());
7655+
let at = &scene.fixtures;
7656+
7657+
scene.cmd("mkdir").arg("d1").succeeds();
7658+
scene.cmd("mkdir").arg("d2").succeeds();
7659+
scene.cmd("chmod").arg("705").arg("d2").succeeds();
7660+
7661+
scene
7662+
.ucmd()
7663+
.arg("--no-preserve=mode")
7664+
.arg("-r")
7665+
.arg("d2")
7666+
.arg("d3")
7667+
.set_stdout(io::stdout())
7668+
.succeeds();
7669+
7670+
let d1_mode = at.metadata("d1").mode();
7671+
let d3_mode = at.metadata("d3").mode();
7672+
7673+
assert_eq!(d1_mode, d3_mode);
7674+
}

0 commit comments

Comments
 (0)