diff options
author | Sylvestre Ledru <sylvestre@debian.org> | 2020-05-28 21:54:03 +0200 |
---|---|---|
committer | Sylvestre Ledru <sylvestre@debian.org> | 2020-06-20 10:49:15 +0200 |
commit | 2d620978434b37d985e2d2db7136e04983e29c9f (patch) | |
tree | 43f24360efb8e3eba677841d0d3edd342d0cc8de | |
parent | 280fafed8ace0c9764bb5fc3bdd03d15cd74bd30 (diff) |
feature(cp): Manage -P & -R
-rw-r--r-- | src/uu/cp/src/cp.rs | 88 | ||||
-rw-r--r-- | tests/by-util/test_cp.rs | 112 |
2 files changed, 181 insertions, 19 deletions
diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 2a3b2c8ad..090709c00 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -31,10 +31,13 @@ use winapi::um::fileapi::CreateFileW; #[cfg(windows)] use winapi::um::fileapi::GetFileInformationByHandle; +use std::borrow::Cow; + use clap::{App, Arg, ArgMatches}; use filetime::FileTime; use quick_error::ResultExt; use std::collections::HashSet; +use std::env; #[cfg(not(windows))] use std::ffi::CString; #[cfg(windows)] @@ -53,6 +56,7 @@ use std::os::windows::ffi::OsStrExt; use std::path::{Path, PathBuf, StripPrefixError}; use std::str::FromStr; use std::string::ToString; +use uucore::fs::resolve_relative_path; use uucore::fs::{canonicalize, CanonicalizeMode}; use walkdir::WalkDir; @@ -795,8 +799,6 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<() } } - let dont_follow_symbolic_links = options.no_dereference; - let mut hard_links: Vec<(String, u64)> = vec![]; let mut non_fatal_errors = false; @@ -811,19 +813,7 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<() preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap(); } - if dont_follow_symbolic_links && fs::symlink_metadata(&source)?.file_type().is_symlink() - { - // Here, we will copy the symlink itself (actually, just recreate it) - let link = fs::read_link(&source)?; - let dest = if target.is_dir() { - // the target is a directory, we need to keep the filename - let p = Path::new(source.file_name().unwrap()); - target.join(p) - } else { - target.clone() - }; - symlink_file(&link, &dest, &*context_for(&link, target))?; - } else if !found_hard_link { + if !found_hard_link { if let Err(error) = copy_source(source, target, &target_type, options) { show_error!("{}", error); match error { @@ -882,6 +872,27 @@ fn copy_source( } } +#[cfg(target_os = "windows")] +fn adjust_canonicalization<'a>(p: &'a Path) -> Cow<'a, Path> { + // In some cases, \\? can be missing on some Windows paths. Add it at the + // beginning unless the path is prefixed with a device namespace. + const VERBATIM_PREFIX: &str = r#"\\?"#; + const DEVICE_NS_PREFIX: &str = r#"\\."#; + + let has_prefix = p + .components() + .next() + .and_then(|comp| comp.as_os_str().to_str()) + .map(|p_str| p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX)) + .unwrap_or_default(); + + if has_prefix { + p.into() + } else { + Path::new(VERBATIM_PREFIX).join(p).into() + } +} + /// Read the contents of the directory `root` and recursively copy the /// contents to `target`. /// @@ -914,9 +925,35 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult let mut hard_links: Vec<(String, u64)> = vec![]; for path in WalkDir::new(root) { - let path = or_continue!(or_continue!(path).path().canonicalize()); + let p = or_continue!(path); + let is_symlink = fs::symlink_metadata(p.path())?.file_type().is_symlink(); + let path = if options.no_dereference && is_symlink { + // we are dealing with a symlink. Don't follow it + match env::current_dir() { + Ok(cwd) => cwd.join(resolve_relative_path(p.path())), + Err(e) => crash!(1, "failed to get current directory {}", e), + } + } else { + or_continue!(p.path().canonicalize()) + }; + let local_to_root_parent = match root_parent { - Some(parent) => or_continue!(path.strip_prefix(&parent)).to_path_buf(), + Some(parent) => { + #[cfg(windows)] + { + // On Windows, some pathes are starting with \\? + // but not always, so, make sure that we are consistent for strip_prefix + // See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info + let parent_can = adjust_canonicalization(parent); + let path_can = adjust_canonicalization(&path); + + or_continue!(&path_can.strip_prefix(&parent_can)).to_path_buf() + } + #[cfg(not(windows))] + { + or_continue!(path.strip_prefix(&parent)).to_path_buf() + } + } None => path.clone(), }; @@ -1171,9 +1208,26 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> ReflinkMode::Never => {} } } + } else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() { + // Here, we will copy the symlink itself (actually, just recreate it) + let link = fs::read_link(&source)?; + let dest: Cow<'_, Path> = if dest.is_dir() { + match source.file_name() { + Some(name) => dest.join(name).into(), + None => crash!( + EXIT_ERR, + "cannot stat ‘{}’: No such file or directory", + source.display() + ), + } + } else { + dest.into() + }; + symlink_file(&link, &dest, &*context_for(&link, &dest))?; } else { fs::copy(source, dest).context(&*context_for(source, dest))?; } + Ok(()) } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index b13bd1420..f56131a86 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -8,6 +8,9 @@ use std::os::unix::fs; #[cfg(windows)] use std::os::windows::fs::symlink_file; +#[cfg(not(windows))] +use std::env; + static TEST_EXISTING_FILE: &str = "existing_file.txt"; static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt"; static TEST_HELLO_WORLD_SOURCE_SYMLINK: &str = "hello_world.txt.link"; @@ -18,7 +21,7 @@ static TEST_COPY_TO_FOLDER: &str = "hello_dir/"; static TEST_COPY_TO_FOLDER_FILE: &str = "hello_dir/hello_world.txt"; static TEST_COPY_FROM_FOLDER: &str = "hello_dir_with_file/"; static TEST_COPY_FROM_FOLDER_FILE: &str = "hello_dir_with_file/hello_world.txt"; -static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new/"; +static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new"; static TEST_COPY_TO_FOLDER_NEW_FILE: &str = "hello_dir_new/hello_world.txt"; #[test] @@ -351,7 +354,7 @@ fn test_cp_no_deref() { TEST_HELLO_WORLD_SOURCE, at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), ); - //using -t option + //using -P option let result = scene .ucmd() .arg("-P") @@ -379,3 +382,108 @@ fn test_cp_no_deref() { let path_to_check = path_to_new_symlink.to_str().unwrap(); assert_eq!(at.read(&path_to_check), "Hello, World!\n"); } + +#[test] +// For now, disable the test on Windows. Symlinks aren't well support on Windows. +// It works on Unix for now and it works locally when run from a powershell +#[cfg(not(windows))] +fn test_cp_no_deref_folder_to_folder() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let cwd = env::current_dir().unwrap(); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER); + + // Change the cwd to have a correct symlink + assert!(env::set_current_dir(&path_to_new_symlink).is_ok()); + + #[cfg(not(windows))] + let _r = fs::symlink(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK); + #[cfg(windows)] + let _r = symlink_file(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK); + + // Back to the initial cwd (breaks the other tests) + assert!(env::set_current_dir(&cwd).is_ok()); + + //using -P -R option + let result = scene + .ucmd() + .arg("-P") + .arg("-R") + .arg("-v") + .arg(TEST_COPY_FROM_FOLDER) + .arg(TEST_COPY_TO_FOLDER_NEW) + .run(); + println!("cp output {}", result.stdout); + + // Check that the exit code represents a successful copy. + let exit_success = result.success; + assert!(exit_success); + + #[cfg(not(windows))] + { + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); + println!("ls source {}", result.stdout); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); + + let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); + println!("ls dest {}", result.stdout); + } + + #[cfg(windows)] + { + // No action as this test is disabled but kept in case we want to + // try to make it work in the future. + let a = Command::new("cmd").args(&["/C", "dir"]).output(); + println!("output {:#?}", a); + + let a = Command::new("cmd") + .args(&["/C", "dir", &at.as_string()]) + .output(); + println!("output {:#?}", a); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + } + + let path_to_new_symlink = at + .subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join(TEST_HELLO_WORLD_SOURCE_SYMLINK); + assert!(at.is_symlink( + &path_to_new_symlink + .clone() + .into_os_string() + .into_string() + .unwrap() + )); + + let path_to_new = at.subdir.join(TEST_COPY_TO_FOLDER_NEW_FILE); + + // Check the content of the destination file that was copied. + let path_to_check = path_to_new.to_str().unwrap(); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); + + // Check the content of the symlink + let path_to_check = path_to_new_symlink.to_str().unwrap(); + assert_eq!(at.read(&path_to_check), "Hello, World!\n"); +} |