Skip to content

Commit

Permalink
feature(cp): Manage -P & -R
Browse files Browse the repository at this point in the history
  • Loading branch information
sylvestre committed Jun 9, 2020
1 parent 795815a commit eb43bd2
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 20 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 78 additions & 18 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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 @@ -50,7 +51,7 @@ use std::mem;
use std::os::unix::io::IntoRawFd;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
use std::path::{Path, PathBuf, StripPrefixError};
use std::path::{Component, Path, PathBuf, StripPrefixError};
use std::str::FromStr;
use std::string::ToString;
use uucore::fs::{canonicalize, CanonicalizeMode};
Expand Down Expand Up @@ -795,8 +796,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;
Expand All @@ -811,19 +810,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 {
Expand Down Expand Up @@ -882,6 +869,44 @@ fn copy_source(
}
}

pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
let mut components = path.as_ref().components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};

for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}

#[cfg(target_os = "windows")]
fn adjust_canonicalization<P: AsRef<Path>>(p: P) -> PathBuf {
const VERBATIM_PREFIX: &str = r#"\\?\"#;
let p = p.as_ref().display().to_string();
if p.starts_with(VERBATIM_PREFIX) {
Path::new(&p[VERBATIM_PREFIX.len()..].to_string()).to_path_buf()
} else {
Path::new(&p).to_path_buf()
}
}

/// Read the contents of the directory `root` and recursively copy the
/// contents to `target`.
///
Expand Down Expand Up @@ -914,9 +939,32 @@ 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
env::current_dir().unwrap().join(normalize_path(p.path()))
} 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.clone());

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 @@ -1171,9 +1219,21 @@ 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 = if dest.is_dir() {
// the target is a directory, we need to keep the filename
let p = Path::new(source.file_name().unwrap());
dest.join(p)
} else {
dest.to_path_buf()
};
symlink_file(&link, &dest, &*context_for(&link, &dest))?;
} else {
fs::copy(source, dest).context(&*context_for(source, dest))?;
}

Ok(())
}

Expand Down
112 changes: 110 additions & 2 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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]
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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");
}

0 comments on commit eb43bd2

Please sign in to comment.