Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(cp) implement -P/--no-deference #1528

Merged
merged 2 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
87 changes: 78 additions & 9 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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;

Expand Down Expand Up @@ -207,6 +211,7 @@ pub struct Options {
copy_contents: bool,
copy_mode: CopyMode,
dereference: bool,
no_dereference: bool,
no_target_dir: bool,
one_file_system: bool,
overwrite: OverwriteMode,
Expand Down Expand Up @@ -414,6 +419,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.value_name("ATTR_LIST")
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE])
.help("don't preserve the specified attributes"))
.arg(Arg::with_name(OPT_NO_DEREFERENCE)
.short("-P")
.long(OPT_NO_DEREFERENCE)
.conflicts_with(OPT_DEREFERENCE)
.help("never follow symbolic links in SOURCE"))

// TODO: implement the following args
.arg(Arg::with_name(OPT_ARCHIVE)
Expand All @@ -433,11 +443,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(OPT_DEREFERENCE)
.conflicts_with(OPT_NO_DEREFERENCE)
.help("NotImplemented: always follow symbolic links in SOURCE"))
.arg(Arg::with_name(OPT_NO_DEREFERENCE)
.short("-P")
.long(OPT_NO_DEREFERENCE)
.conflicts_with(OPT_DEREFERENCE)
.help("NotImplemented: never follow symbolic links in SOURCE"))
.arg(Arg::with_name(OPT_PARENTS)
.long(OPT_PARENTS)
.help("NotImplemented: use full source file name under DIRECTORY"))
Expand Down Expand Up @@ -565,7 +570,6 @@ impl Options {
OPT_COPY_CONTENTS,
OPT_NO_DEREFERENCE_PRESERVE_LINKS,
OPT_DEREFERENCE,
OPT_NO_DEREFERENCE,
OPT_PARENTS,
OPT_SPARSE,
OPT_STRIP_TRAILING_SLASHES,
Expand Down Expand Up @@ -627,6 +631,7 @@ impl Options {
copy_contents: matches.is_present(OPT_COPY_CONTENTS),
copy_mode: CopyMode::from_matches(matches),
dereference: matches.is_present(OPT_DEREFERENCE),
no_dereference: matches.is_present(OPT_NO_DEREFERENCE),
one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM),
overwrite: OverwriteMode::from_matches(matches),
parents: matches.is_present(OPT_PARENTS),
Expand Down Expand Up @@ -807,6 +812,7 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()
let dest = construct_dest_path(source, target, &target_type, options)?;
preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap();
}

if !found_hard_link {
if let Err(error) = copy_source(source, target, &target_type, options) {
show_error!("{}", error);
Expand Down Expand Up @@ -866,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`.
///
Expand Down Expand Up @@ -898,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())),
sylvestre marked this conversation as resolved.
Show resolved Hide resolved
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
sylvestre marked this conversation as resolved.
Show resolved Hide resolved
// 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(),
};

Expand Down Expand Up @@ -1070,7 +1123,6 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
}
}
}

match options.copy_mode {
CopyMode::Link => {
fs::hard_link(source, dest).context(&*context_for(source, dest))?;
Expand Down Expand Up @@ -1156,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(())
}

Expand Down
161 changes: 160 additions & 1 deletion tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ use crate::common::util::*;
#[cfg(not(windows))]
use std::fs::set_permissions;

#[cfg(not(windows))]
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";
static TEST_HELLO_WORLD_DEST: &str = "copy_of_hello_world.txt";
static TEST_HOW_ARE_YOU_SOURCE: &str = "how_are_you.txt";
static TEST_HOW_ARE_YOU_DEST: &str = "hello_dir/how_are_you.txt";
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]
Expand Down Expand Up @@ -328,3 +338,152 @@ fn test_cp_arg_suffix() {
"How are you?\n"
);
}

#[test]
fn test_cp_no_deref() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

#[cfg(not(windows))]
let _r = fs::symlink(
TEST_HELLO_WORLD_SOURCE,
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
);
#[cfg(windows)]
let _r = symlink_file(
TEST_HELLO_WORLD_SOURCE,
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
);
//using -P option
let result = scene
.ucmd()
.arg("-P")
.arg(TEST_HELLO_WORLD_SOURCE)
.arg(TEST_HELLO_WORLD_SOURCE_SYMLINK)
.arg(TEST_COPY_TO_FOLDER)
.run();

// Check that the exit code represents a successful copy.
let exit_success = result.success;
assert!(exit_success);
let path_to_new_symlink = at
.subdir
.join(TEST_COPY_TO_FOLDER)
.join(TEST_HELLO_WORLD_SOURCE_SYMLINK);
assert!(at.is_symlink(
&path_to_new_symlink
.clone()
.into_os_string()
.into_string()
.unwrap()
));
// Check the content of the destination file that was copied.
assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n");
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");
}