Skip to content

Commit 2d62097

Browse files
committed
feature(cp): Manage -P & -R
1 parent 280fafe commit 2d62097

File tree

2 files changed

+181
-19
lines changed

2 files changed

+181
-19
lines changed

src/uu/cp/src/cp.rs

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ use winapi::um::fileapi::CreateFileW;
3131
#[cfg(windows)]
3232
use winapi::um::fileapi::GetFileInformationByHandle;
3333

34+
use std::borrow::Cow;
35+
3436
use clap::{App, Arg, ArgMatches};
3537
use filetime::FileTime;
3638
use quick_error::ResultExt;
3739
use std::collections::HashSet;
40+
use std::env;
3841
#[cfg(not(windows))]
3942
use std::ffi::CString;
4043
#[cfg(windows)]
@@ -53,6 +56,7 @@ use std::os::windows::ffi::OsStrExt;
5356
use std::path::{Path, PathBuf, StripPrefixError};
5457
use std::str::FromStr;
5558
use std::string::ToString;
59+
use uucore::fs::resolve_relative_path;
5660
use uucore::fs::{canonicalize, CanonicalizeMode};
5761
use walkdir::WalkDir;
5862

@@ -795,8 +799,6 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()
795799
}
796800
}
797801

798-
let dont_follow_symbolic_links = options.no_dereference;
799-
800802
let mut hard_links: Vec<(String, u64)> = vec![];
801803

802804
let mut non_fatal_errors = false;
@@ -811,19 +813,7 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()
811813
preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap();
812814
}
813815

814-
if dont_follow_symbolic_links && fs::symlink_metadata(&source)?.file_type().is_symlink()
815-
{
816-
// Here, we will copy the symlink itself (actually, just recreate it)
817-
let link = fs::read_link(&source)?;
818-
let dest = if target.is_dir() {
819-
// the target is a directory, we need to keep the filename
820-
let p = Path::new(source.file_name().unwrap());
821-
target.join(p)
822-
} else {
823-
target.clone()
824-
};
825-
symlink_file(&link, &dest, &*context_for(&link, target))?;
826-
} else if !found_hard_link {
816+
if !found_hard_link {
827817
if let Err(error) = copy_source(source, target, &target_type, options) {
828818
show_error!("{}", error);
829819
match error {
@@ -882,6 +872,27 @@ fn copy_source(
882872
}
883873
}
884874

875+
#[cfg(target_os = "windows")]
876+
fn adjust_canonicalization<'a>(p: &'a Path) -> Cow<'a, Path> {
877+
// In some cases, \\? can be missing on some Windows paths. Add it at the
878+
// beginning unless the path is prefixed with a device namespace.
879+
const VERBATIM_PREFIX: &str = r#"\\?"#;
880+
const DEVICE_NS_PREFIX: &str = r#"\\."#;
881+
882+
let has_prefix = p
883+
.components()
884+
.next()
885+
.and_then(|comp| comp.as_os_str().to_str())
886+
.map(|p_str| p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX))
887+
.unwrap_or_default();
888+
889+
if has_prefix {
890+
p.into()
891+
} else {
892+
Path::new(VERBATIM_PREFIX).join(p).into()
893+
}
894+
}
895+
885896
/// Read the contents of the directory `root` and recursively copy the
886897
/// contents to `target`.
887898
///
@@ -914,9 +925,35 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult
914925
let mut hard_links: Vec<(String, u64)> = vec![];
915926

916927
for path in WalkDir::new(root) {
917-
let path = or_continue!(or_continue!(path).path().canonicalize());
928+
let p = or_continue!(path);
929+
let is_symlink = fs::symlink_metadata(p.path())?.file_type().is_symlink();
930+
let path = if options.no_dereference && is_symlink {
931+
// we are dealing with a symlink. Don't follow it
932+
match env::current_dir() {
933+
Ok(cwd) => cwd.join(resolve_relative_path(p.path())),
934+
Err(e) => crash!(1, "failed to get current directory {}", e),
935+
}
936+
} else {
937+
or_continue!(p.path().canonicalize())
938+
};
939+
918940
let local_to_root_parent = match root_parent {
919-
Some(parent) => or_continue!(path.strip_prefix(&parent)).to_path_buf(),
941+
Some(parent) => {
942+
#[cfg(windows)]
943+
{
944+
// On Windows, some pathes are starting with \\?
945+
// but not always, so, make sure that we are consistent for strip_prefix
946+
// See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info
947+
let parent_can = adjust_canonicalization(parent);
948+
let path_can = adjust_canonicalization(&path);
949+
950+
or_continue!(&path_can.strip_prefix(&parent_can)).to_path_buf()
951+
}
952+
#[cfg(not(windows))]
953+
{
954+
or_continue!(path.strip_prefix(&parent)).to_path_buf()
955+
}
956+
}
920957
None => path.clone(),
921958
};
922959

@@ -1171,9 +1208,26 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()>
11711208
ReflinkMode::Never => {}
11721209
}
11731210
}
1211+
} else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() {
1212+
// Here, we will copy the symlink itself (actually, just recreate it)
1213+
let link = fs::read_link(&source)?;
1214+
let dest: Cow<'_, Path> = if dest.is_dir() {
1215+
match source.file_name() {
1216+
Some(name) => dest.join(name).into(),
1217+
None => crash!(
1218+
EXIT_ERR,
1219+
"cannot stat ‘{}’: No such file or directory",
1220+
source.display()
1221+
),
1222+
}
1223+
} else {
1224+
dest.into()
1225+
};
1226+
symlink_file(&link, &dest, &*context_for(&link, &dest))?;
11741227
} else {
11751228
fs::copy(source, dest).context(&*context_for(source, dest))?;
11761229
}
1230+
11771231
Ok(())
11781232
}
11791233

tests/by-util/test_cp.rs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use std::os::unix::fs;
88
#[cfg(windows)]
99
use std::os::windows::fs::symlink_file;
1010

11+
#[cfg(not(windows))]
12+
use std::env;
13+
1114
static TEST_EXISTING_FILE: &str = "existing_file.txt";
1215
static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt";
1316
static TEST_HELLO_WORLD_SOURCE_SYMLINK: &str = "hello_world.txt.link";
@@ -18,7 +21,7 @@ static TEST_COPY_TO_FOLDER: &str = "hello_dir/";
1821
static TEST_COPY_TO_FOLDER_FILE: &str = "hello_dir/hello_world.txt";
1922
static TEST_COPY_FROM_FOLDER: &str = "hello_dir_with_file/";
2023
static TEST_COPY_FROM_FOLDER_FILE: &str = "hello_dir_with_file/hello_world.txt";
21-
static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new/";
24+
static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new";
2225
static TEST_COPY_TO_FOLDER_NEW_FILE: &str = "hello_dir_new/hello_world.txt";
2326

2427
#[test]
@@ -351,7 +354,7 @@ fn test_cp_no_deref() {
351354
TEST_HELLO_WORLD_SOURCE,
352355
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
353356
);
354-
//using -t option
357+
//using -P option
355358
let result = scene
356359
.ucmd()
357360
.arg("-P")
@@ -379,3 +382,108 @@ fn test_cp_no_deref() {
379382
let path_to_check = path_to_new_symlink.to_str().unwrap();
380383
assert_eq!(at.read(&path_to_check), "Hello, World!\n");
381384
}
385+
386+
#[test]
387+
// For now, disable the test on Windows. Symlinks aren't well support on Windows.
388+
// It works on Unix for now and it works locally when run from a powershell
389+
#[cfg(not(windows))]
390+
fn test_cp_no_deref_folder_to_folder() {
391+
let scene = TestScenario::new(util_name!());
392+
let at = &scene.fixtures;
393+
394+
let cwd = env::current_dir().unwrap();
395+
396+
let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER);
397+
398+
// Change the cwd to have a correct symlink
399+
assert!(env::set_current_dir(&path_to_new_symlink).is_ok());
400+
401+
#[cfg(not(windows))]
402+
let _r = fs::symlink(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK);
403+
#[cfg(windows)]
404+
let _r = symlink_file(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK);
405+
406+
// Back to the initial cwd (breaks the other tests)
407+
assert!(env::set_current_dir(&cwd).is_ok());
408+
409+
//using -P -R option
410+
let result = scene
411+
.ucmd()
412+
.arg("-P")
413+
.arg("-R")
414+
.arg("-v")
415+
.arg(TEST_COPY_FROM_FOLDER)
416+
.arg(TEST_COPY_TO_FOLDER_NEW)
417+
.run();
418+
println!("cp output {}", result.stdout);
419+
420+
// Check that the exit code represents a successful copy.
421+
let exit_success = result.success;
422+
assert!(exit_success);
423+
424+
#[cfg(not(windows))]
425+
{
426+
let scene2 = TestScenario::new("ls");
427+
let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run();
428+
println!("ls source {}", result.stdout);
429+
430+
let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW);
431+
432+
let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run();
433+
println!("ls dest {}", result.stdout);
434+
}
435+
436+
#[cfg(windows)]
437+
{
438+
// No action as this test is disabled but kept in case we want to
439+
// try to make it work in the future.
440+
let a = Command::new("cmd").args(&["/C", "dir"]).output();
441+
println!("output {:#?}", a);
442+
443+
let a = Command::new("cmd")
444+
.args(&["/C", "dir", &at.as_string()])
445+
.output();
446+
println!("output {:#?}", a);
447+
448+
let a = Command::new("cmd")
449+
.args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()])
450+
.output();
451+
println!("output {:#?}", a);
452+
453+
let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER);
454+
455+
let a = Command::new("cmd")
456+
.args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()])
457+
.output();
458+
println!("output {:#?}", a);
459+
460+
let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW);
461+
462+
let a = Command::new("cmd")
463+
.args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()])
464+
.output();
465+
println!("output {:#?}", a);
466+
}
467+
468+
let path_to_new_symlink = at
469+
.subdir
470+
.join(TEST_COPY_TO_FOLDER_NEW)
471+
.join(TEST_HELLO_WORLD_SOURCE_SYMLINK);
472+
assert!(at.is_symlink(
473+
&path_to_new_symlink
474+
.clone()
475+
.into_os_string()
476+
.into_string()
477+
.unwrap()
478+
));
479+
480+
let path_to_new = at.subdir.join(TEST_COPY_TO_FOLDER_NEW_FILE);
481+
482+
// Check the content of the destination file that was copied.
483+
let path_to_check = path_to_new.to_str().unwrap();
484+
assert_eq!(at.read(path_to_check), "Hello, World!\n");
485+
486+
// Check the content of the symlink
487+
let path_to_check = path_to_new_symlink.to_str().unwrap();
488+
assert_eq!(at.read(&path_to_check), "Hello, World!\n");
489+
}

0 commit comments

Comments
 (0)