Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1869,15 +1869,6 @@
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
Expand Down Expand Up @@ -1924,7 +1915,7 @@
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
Expand Down Expand Up @@ -1962,6 +1953,24 @@
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
Expand Down Expand Up @@ -3228,8 +3237,8 @@
timestamps: Preserve::Yes { required: true },
..Attributes::NONE
};
let unioned = explicit_no_mode.union(&timestamps_yes);

Check warning on line 3240 in src/uu/cp/src/cp.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

WARNING: `cspell`: Unknown word 'unioned' (file:'src/uu/cp/src/cp.rs', line:3240)
assert_eq!(unioned.mode, Preserve::No { explicit: true });

Check warning on line 3241 in src/uu/cp/src/cp.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

WARNING: `cspell`: Unknown word 'unioned' (file:'src/uu/cp/src/cp.rs', line:3241)
assert_eq!(unioned.timestamps, Preserve::Yes { required: true });

Check warning on line 3242 in src/uu/cp/src/cp.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

WARNING: `cspell`: Unknown word 'unioned' (file:'src/uu/cp/src/cp.rs', line:3242)
}
}
64 changes: 64 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading