Skip to content

Commit d33d0cd

Browse files
Address Windows symlink cp review feedback
1 parent e184213 commit d33d0cd

2 files changed

Lines changed: 72 additions & 15 deletions

File tree

src/uu/cp/src/cp.rs

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,8 +1936,7 @@ pub(crate) fn copy_attributes(
19361936
fn symlink_file(
19371937
source: &Path,
19381938
dest: &Path,
1939-
#[cfg(windows)] source_type_hint: Option<&Path>,
1940-
#[cfg(not(windows))] _source_type_hint: Option<&Path>,
1939+
#[cfg(windows)] source_metadata: &Metadata,
19411940
#[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet<FileInformation>,
19421941
#[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet<FileInformation>,
19431942
) -> CopyResult<()> {
@@ -1963,7 +1962,7 @@ fn symlink_file(
19631962
}
19641963
#[cfg(windows)]
19651964
{
1966-
let create_symlink = if source_type_hint.is_some_and(source_is_directory_for_symlink) {
1965+
let create_symlink = if source_is_directory_for_symlink(source_metadata) {
19671966
std::os::windows::fs::symlink_dir
19681967
} else {
19691968
std::os::windows::fs::symlink_file
@@ -1987,18 +1986,12 @@ fn symlink_file(
19871986
}
19881987
}
19891988

1989+
/// Returns true when `source_metadata` should be recreated as a directory symlink.
19901990
#[cfg(windows)]
1991-
fn source_is_directory_for_symlink(source: &Path) -> bool {
1991+
fn source_is_directory_for_symlink(source_metadata: &Metadata) -> bool {
19921992
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
19931993

1994-
source.symlink_metadata().is_ok_and(|metadata| {
1995-
let file_type = metadata.file_type();
1996-
if file_type.is_symlink() {
1997-
metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0
1998-
} else {
1999-
file_type.is_dir()
2000-
}
2001-
})
1994+
source_metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0
20021995
}
20031996

20041997
fn context_for(src: &Path, dest: &Path) -> String {
@@ -2346,7 +2339,13 @@ fn handle_copy_mode(
23462339
if dest.exists() && options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) {
23472340
fs::remove_file(dest)?;
23482341
}
2349-
symlink_file(source, dest, Some(source), symlinked_files)?;
2342+
symlink_file(
2343+
source,
2344+
dest,
2345+
#[cfg(windows)]
2346+
source_metadata,
2347+
symlinked_files,
2348+
)?;
23502349
}
23512350
CopyMode::Update => {
23522351
if dest.exists() {
@@ -2821,7 +2820,14 @@ fn copy_helper(
28212820
}
28222821

28232822
if source_metadata.is_symlink() {
2824-
copy_link(source, dest, symlinked_files, options)?;
2823+
copy_link(
2824+
source,
2825+
dest,
2826+
#[cfg(windows)]
2827+
source_metadata,
2828+
symlinked_files,
2829+
options,
2830+
)?;
28252831
} else {
28262832
// Use O_NOFOLLOW on the source open iff cp is in no-dereference mode.
28272833
// In that case source_metadata was obtained via lstat, so a path swap
@@ -2900,6 +2906,7 @@ fn copy_node(
29002906
fn copy_link(
29012907
source: &Path,
29022908
dest: &Path,
2909+
#[cfg(windows)] source_metadata: &Metadata,
29032910
symlinked_files: &mut HashSet<FileInformation>,
29042911
options: &Options,
29052912
) -> CopyResult<()> {
@@ -2910,7 +2917,13 @@ fn copy_link(
29102917
if dest.is_symlink() || dest.is_file() {
29112918
delete_path(dest, options)?;
29122919
}
2913-
symlink_file(&link, dest, Some(source), symlinked_files)?;
2920+
symlink_file(
2921+
&link,
2922+
dest,
2923+
#[cfg(windows)]
2924+
source_metadata,
2925+
symlinked_files,
2926+
)?;
29142927
copy_attributes(
29152928
source,
29162929
dest,
@@ -2986,6 +2999,8 @@ fn disk_usage_directory(p: &Path) -> io::Result<u64> {
29862999
#[cfg(test)]
29873000
mod tests {
29883001

3002+
#[cfg(windows)]
3003+
use crate::source_is_directory_for_symlink;
29893004
use crate::{Attributes, Preserve, aligned_ancestors, localize_to_target};
29903005
use std::path::Path;
29913006

@@ -3056,4 +3071,36 @@ mod tests {
30563071
}
30573072
);
30583073
}
3074+
3075+
#[test]
3076+
#[cfg(windows)]
3077+
fn test_source_is_directory_for_symlink_detects_directory_symlinks() {
3078+
let temp_dir = tempfile::tempdir().unwrap();
3079+
let target = temp_dir.path().join("target");
3080+
let symlink = temp_dir.path().join("target-link");
3081+
3082+
std::fs::create_dir(&target).unwrap();
3083+
if std::os::windows::fs::symlink_dir(&target, &symlink).is_err() {
3084+
return;
3085+
}
3086+
3087+
let metadata = symlink.symlink_metadata().unwrap();
3088+
assert!(source_is_directory_for_symlink(&metadata));
3089+
}
3090+
3091+
#[test]
3092+
#[cfg(windows)]
3093+
fn test_source_is_directory_for_symlink_rejects_file_symlinks() {
3094+
let temp_dir = tempfile::tempdir().unwrap();
3095+
let target = temp_dir.path().join("target");
3096+
let symlink = temp_dir.path().join("target-link");
3097+
3098+
std::fs::write(&target, b"data").unwrap();
3099+
if std::os::windows::fs::symlink_file(&target, &symlink).is_err() {
3100+
return;
3101+
}
3102+
3103+
let metadata = symlink.symlink_metadata().unwrap();
3104+
assert!(!source_is_directory_for_symlink(&metadata));
3105+
}
30593106
}

tests/by-util/test_cp.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,6 +2874,16 @@ fn test_copy_dir_symlink() {
28742874
at.symlink_dir("dir", "dir-link");
28752875
ucmd.args(&["-r", "dir-link", "copy"]).succeeds();
28762876
assert_eq!(at.resolve_link("copy"), "dir");
2877+
}
2878+
2879+
#[test]
2880+
#[cfg(windows)]
2881+
fn test_copy_windows_dir_symlink_preserves_directory_type() {
2882+
let (at, mut ucmd) = at_and_ucmd!();
2883+
at.mkdir("dir");
2884+
at.symlink_dir("dir", "dir-link");
2885+
ucmd.args(&["-r", "dir-link", "copy"]).succeeds();
2886+
assert_eq!(at.resolve_link("copy"), "dir");
28772887
assert!(at.plus("copy").is_dir());
28782888
}
28792889

0 commit comments

Comments
 (0)