diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 1bbc0e4a79..3052486e5b 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1869,15 +1869,6 @@ pub(crate) fn copy_attributes( let source_metadata = fs::symlink_metadata(source).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; - let mode_explicitly_disabled = matches!(attributes.mode, Preserve::No { explicit: true }); - - // preserve is true by default if the destination is created by us and it's a directory - let mode = if !mode_explicitly_disabled && dest_is_freshly_created_dir { - Preserve::Yes { required: false } - } else { - attributes.mode - }; - // Track whether `chown` to the source's uid succeeded. If it did not // (typical case: non-root user copying a root-owned setuid file), the // mode preservation below must strip setuid/setgid so the destination @@ -1924,7 +1915,7 @@ pub(crate) fn copy_attributes( Ok(()) })?; - handle_preserve(mode, || -> CopyResult<()> { + handle_preserve(attributes.mode, || -> CopyResult<()> { // The `chmod()` system call that underlies the // `fs::set_permissions()` call is unable to change the // permissions of a symbolic link. In that case, we just @@ -1962,6 +1953,24 @@ pub(crate) fn copy_attributes( Ok(()) })?; + #[cfg(unix)] + if dest_is_freshly_created_dir + && matches!(attributes.mode, Preserve::No { .. }) + && !dest.is_symlink() + { + use std::os::unix::fs::PermissionsExt; + let mut perms = source_metadata.permissions(); + let umask = uucore::mode::get_umask(); + let target_mode = match attributes.mode { + Preserve::No { explicit: true } => 0o777 & !umask, + Preserve::No { explicit: false } => (perms.mode() & 0o777) & !umask, + _ => unreachable!(), + }; + perms.set_mode(target_mode); + fs::set_permissions(dest, perms) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; + } + handle_preserve(attributes.timestamps, || -> CopyResult<()> { let (atime, mtime) = source_times(&source_metadata, context)?; // `set_file_times` opens the destination (O_RDONLY) before calling diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 35371d2a75..52e8cf1868 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7595,6 +7595,70 @@ fn test_no_preserve_mode_with_later_preserve() { assert_eq!(dst5_mode, 0o755); } +#[test] +#[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI: no chmod syscall, so required mode/ownership preservation always fails" +)] +fn test_cp_recursive_dir_umask() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("src"); + at.mkdir("src/dir777"); + at.set_mode("src/dir777", 0o777); + at.mkdir("src/dir750"); + at.set_mode("src/dir750", 0o750); + at.mkdir("src/dir700"); + at.set_mode("src/dir700", 0o700); + at.touch("src/dir777/f"); + at.set_mode("src/dir777/f", 0o666); + + // 1. Default cp -r under umask 077: apply source_mode & !umask + scene + .ucmd() + .umask(0o077) + .args(&["-r", "src", "dst1"]) + .succeeds(); + assert_eq!(at.metadata("dst1/dir777").mode() & 0o777, 0o700); + assert_eq!(at.metadata("dst1/dir750").mode() & 0o777, 0o700); + assert_eq!(at.metadata("dst1/dir700").mode() & 0o777, 0o700); + assert_eq!(at.metadata("dst1/dir777/f").mode() & 0o777, 0o600); + + // 2. Default cp -r under umask 022: apply source_mode & !umask + scene + .ucmd() + .umask(0o022) + .args(&["-r", "src", "dst2"]) + .succeeds(); + assert_eq!(at.metadata("dst2/dir777").mode() & 0o777, 0o755); + assert_eq!(at.metadata("dst2/dir750").mode() & 0o777, 0o750); + assert_eq!(at.metadata("dst2/dir700").mode() & 0o777, 0o700); + assert_eq!(at.metadata("dst2/dir777/f").mode() & 0o777, 0o644); + + // 3. cp -r -a under umask 077: mode is preserved and umask is bypassed + scene + .ucmd() + .umask(0o077) + .args(&["-a", "src", "dst3"]) + .succeeds(); + assert_eq!(at.metadata("dst3/dir777").mode() & 0o777, 0o777); + assert_eq!(at.metadata("dst3/dir750").mode() & 0o777, 0o750); + assert_eq!(at.metadata("dst3/dir700").mode() & 0o777, 0o700); + assert_eq!(at.metadata("dst3/dir777/f").mode() & 0o777, 0o666); + + // 4. cp -r --no-preserve=mode under umask 022: directories get 0777 & !umask + scene + .ucmd() + .umask(0o022) + .args(&["-r", "--no-preserve=mode", "src", "dst4"]) + .succeeds(); + assert_eq!(at.metadata("dst4/dir777").mode() & 0o777, 0o755); + assert_eq!(at.metadata("dst4/dir750").mode() & 0o777, 0o755); + assert_eq!(at.metadata("dst4/dir700").mode() & 0o777, 0o755); +} + /// Test the behavior of preserving permissions when copying through a symlink #[test] #[cfg(unix)]