summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSylvestre Ledru <sylvestre@debian.org>2020-05-28 21:54:03 +0200
committerSylvestre Ledru <sylvestre@debian.org>2020-06-20 10:49:15 +0200
commit2d620978434b37d985e2d2db7136e04983e29c9f (patch)
tree43f24360efb8e3eba677841d0d3edd342d0cc8de
parent280fafed8ace0c9764bb5fc3bdd03d15cd74bd30 (diff)
feature(cp): Manage -P & -R
-rw-r--r--src/uu/cp/src/cp.rs88
-rw-r--r--tests/by-util/test_cp.rs112
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");
+}