From 7dafb649d5e37b5656e8b0c92bc838c2681efe8b Mon Sep 17 00:00:00 2001 From: Joshua Miller Date: Wed, 28 Dec 2016 02:08:43 +0100 Subject: [PATCH] implement many copy flags - Refactored towards extensibility --- Cargo.lock | 2 + src/cp/Cargo.toml | 9 +- src/cp/README.md | 38 + src/cp/cp.rs | 1039 +++++++++++++++++++++------ tests/fixtures/cp/existing_file.txt | 1 + tests/fixtures/cp/how_are_you.txt | 1 + tests/test_cp.rs | 221 +++++- 7 files changed, 1103 insertions(+), 208 deletions(-) create mode 100644 src/cp/README.md create mode 100644 tests/fixtures/cp/existing_file.txt create mode 100644 tests/fixtures/cp/how_are_you.txt diff --git a/Cargo.lock b/Cargo.lock index 0e3a113b3..c80206ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,8 +305,10 @@ dependencies = [ name = "cp" version = "0.0.1" dependencies = [ + "clap 2.25.0 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "uucore 0.0.1", "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/src/cp/Cargo.toml b/src/cp/Cargo.toml index 58fdb831f..bdfde5a69 100644 --- a/src/cp/Cargo.toml +++ b/src/cp/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "cp" version = "0.0.1" -authors = [] +authors = [ + "Jordy Dickinson ", + "Joshua S. Miller ", +] [lib] name = "uu_cp" @@ -10,8 +13,10 @@ path = "cp.rs" [dependencies] getopts = "0.2.14" libc = "0.2.26" -uucore = { path="../uucore" } walkdir = "1.0.7" +clap = "2.20.0" +quick-error = "1.1.0" +uucore = { path="../uucore" } [[bin]] name = "cp" diff --git a/src/cp/README.md b/src/cp/README.md new file mode 100644 index 000000000..d536e5c47 --- /dev/null +++ b/src/cp/README.md @@ -0,0 +1,38 @@ +## Feature list + +### To Do + +- [ ] archive +- [ ] attributes-only +- [ ] copy-contents +- [ ] no-dereference-preserve-linkgs +- [ ] dereference +- [ ] no-dereference +- [ ] preserve-default-attributes +- [ ] preserve +- [ ] no-preserve +- [ ] parents +- [ ] reflink +- [ ] sparse +- [ ] strip-trailing-slashes +- [ ] update +- [ ] one-file-system +- [ ] context +- [ ] cli-symbolic-links + +### Completed + +- [x] backup +- [x] force (Not implemented on Windows) +- [x] interactive +- [x] link +- [x] no-clobber +- [x] no-target-directory +- [x] paths +- [x] recursive +- [x] remove-destination (On Windows, current only works for writeable files) +- [x] suffix +- [x] symbolic-link +- [x] target-directory +- [x] verbose +- [x] version diff --git a/src/cp/cp.rs b/src/cp/cp.rs index 4b190307d..354a9cf0f 100644 --- a/src/cp/cp.rs +++ b/src/cp/cp.rs @@ -4,245 +4,884 @@ * This file is part of the uutils coreutils package. * * (c) Jordy Dickinson + * (c) Joshua S. Miller * * For the full copyright and license information, please view the LICENSE file * that was distributed with this source code. */ -extern crate getopts; - -#[macro_use] -extern crate uucore; - +extern crate clap; extern crate walkdir; +#[macro_use] extern crate uucore; +#[macro_use] extern crate quick_error; +use clap::{Arg, App, ArgMatches}; +use quick_error::ResultExt; +use std::collections::HashSet; +use std::fs; +use std::io::{BufReader, BufRead, stdin, Write}; +use std::io; +use std::path::{Path, PathBuf, StripPrefixError}; +use std::str::FromStr; +use uucore::fs::{canonicalize, CanonicalizeMode}; use walkdir::WalkDir; -use getopts::Options; -use std::fs; -use std::io::{ErrorKind, Result, Write}; -use std::path::Path; -use uucore::fs::{canonicalize, CanonicalizeMode}; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; + +quick_error! { + #[derive(Debug)] + pub enum Error { + /// Simple io::Error wrapper + IoErr(err: io::Error) { from() cause(err) display("{}", err) } + + /// Wrapper for io::Error with path context + IoErrContext(err: io::Error, path: String) { + display("{}: {}", path, err) + context(path: &'a str, err: io::Error) -> (err, path.to_owned()) + cause(err) + } + + /// General copy error + Error(err: String) { + display("{}", err) + from(err: String) -> (err) + from(err: &'static str) -> (err.to_string()) + } + + /// Represents the state when a non-fatal error has occured + /// and not all files were copied. + NotAllFilesCopied {} + + /// Simple walkdir::Error wrapper + WalkDirErr(err: walkdir::Error) { from() display("{}", err) cause(err) } + + /// Simple std::path::StripPrefixError wrapper + StripPrefixError(err: StripPrefixError) { from() } + + /// Result of a skipped file + Skipped(reason: String) { display("{}", reason) } + + /// Result of a skipped file + InvalidArgument(description: String) { display("{}", description) } + + /// All standard options are included as an an implementation + /// path, but those that are not implemented yet should return + /// a NotImplemented error. + NotImplemented(opt: String) { display("Option '{}' not yet implemented.", opt) } + } +} + + +/// Continue next iteration of loop if result of expression is error +macro_rules! or_continue( + ($expr:expr) => (match $expr { + Ok(temp) => temp, + Err(error) => { + show_error!("{}", error); + continue + }, + }) +); + + +/// Prompts the user yes/no and returns `true` they if successfully +/// answered yes. +macro_rules! prompt_yes( + ($($args:tt)+) => ({ + pipe_write!(&mut ::std::io::stdout(), $($args)+); + pipe_write!(&mut ::std::io::stdout(), " [y/N]: "); + pipe_flush!(); + let mut s = String::new(); + match BufReader::new(stdin()).read_line(&mut s) { + Ok(_) => match s.char_indices().nth(0) { + Some((_, x)) => x == 'y' || x == 'Y', + _ => false + }, + _ => false + } + }) +); + +pub type CopyResult = Result; +pub type Source = PathBuf; +pub type Target = PathBuf; + +/// Specifies whether when overwrite files +#[derive (Clone, Eq, PartialEq)] +pub enum ClobberMode { + Force, + RemoveDestination, + Standard, +} + +/// Specifies whether when overwrite files +#[derive (Clone, Eq, PartialEq)] +pub enum OverwriteMode { + /// [Default] Always overwrite existing files + Clobber(ClobberMode), + /// Prompt before overwriting a file + Interactive(ClobberMode), + /// Never overwrite a file + NoClobber, +} + +/// Specifies the expected file type of copy target +pub enum TargetType { + Directory, + File, +} #[derive(Clone, Eq, PartialEq)] -pub enum Mode { - Copy, - Help, - Version, +pub enum BackupMode { + ExistingBackup, + NoBackup, + NumberedBackup, + SimpleBackup, } -static NAME: &'static str = "cp"; -static VERSION: &'static str = env!("CARGO_PKG_VERSION"); +pub enum CopyMode { + Link, + SymLink, + Sparse, + Copy, +} + +#[derive(Clone)] +pub enum Attribute { + #[cfg(unix)] Mode, + Ownership, + Timestamps, + Context, + Links, + Xattr, + All, +} + +/// Re-usable, extensible copy options +#[allow(dead_code)] +pub struct Options { + attributes_only: bool, + backup: bool, + copy_contents: bool, + copy_mode: CopyMode, + dereference: bool, + no_target_dir: bool, + one_file_system: bool, + overwrite: OverwriteMode, + parents: bool, + preserve_attributes: Vec, + recursive: bool, + backup_suffix: String, + target_dir: Option, + update: bool, + verbose: bool, +} + +static VERSION: &str = env!("CARGO_PKG_VERSION"); +static USAGE: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; +static EXIT_OK: i32 = 0; +static EXIT_ERR: i32 = 1; + +/// Prints the version +fn print_version() { + println!("{} {}", executable!(), VERSION); +} + +/// Prints usage/help +fn get_about(usage: &str) -> String { + format!("Usage: {0} [OPTION]... [-T] SOURCE DEST + or: {0} [OPTION]... SOURCE... DIRECTORY + or: {0} [OPTION]... -t DIRECTORY SOURCE... +{1}", executable!(), usage) +} + + +// Argument constants +static OPT_ARCHIVE: &str = "archive"; +static OPT_ATTRIBUTES_ONLY: &str = "attributes-only"; +static OPT_BACKUP: &str = "backup"; +static OPT_CLI_SYMBOLIC_LINKS: &str = "cli-symbolic-links"; +static OPT_CONTEXT: &str = "context"; +static OPT_COPY_CONTENTS: &str = "copy-contents"; +static OPT_DEREFERENCE: &str = "dereference"; +static OPT_FORCE: &str = "force"; +static OPT_INTERACTIVE: &str = "interactive"; +static OPT_LINK: &str = "link"; +static OPT_NO_CLOBBER: &str = "no-clobber"; +static OPT_NO_DEREFERENCE: &str = "no-dereference"; +static OPT_NO_DEREFERENCE_PRESERVE_LINKS: &str = "no-dereference-preserve-linkgs"; +static OPT_NO_PRESERVE: &str = "no-preserve"; +static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; +static OPT_ONE_FILE_SYSTEM: &str = "one-file-system"; +static OPT_PARENTS: &str = "parents"; +static OPT_PATHS: &str = "paths"; +static OPT_PRESERVE: &str = "preserve"; +static OPT_PRESERVE_DEFUALT_ATTRIBUTES: &str = "preserve-default-attributes"; +static OPT_RECURSIVE: &str = "recursive"; +static OPT_RECURSIVE_ALIAS: &str = "recursive_alias"; +static OPT_REFLINK: &str = "reflink"; +static OPT_REMOVE_DESTINATION: &str = "remove-destination"; +static OPT_SPARSE: &str = "sparse"; +static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; +static OPT_SUFFIX: &str = "suffix"; +static OPT_SYMBOLIC_LINK: &str = "symbolic-link"; +static OPT_TARGET_DIRECTORY: &str = "target-directory"; +static OPT_UPDATE: &str = "update"; +static OPT_VERBOSE: &str = "verbose"; +static OPT_VERSION: &str = "version"; + +#[cfg(unix)] +static PRESERVABLE_ATTRIBUTES: &[&str] = &["mode", "ownership", "timestamps", "context", "links", "xattr", "all"]; + +#[cfg(not(unix))] +static PRESERVABLE_ATTRIBUTES: &[&str] = &["ownership", "timestamps", "context", "links", "xattr", "all"]; + +static DEFAULT_ATTRIBUTES: &[Attribute] = &[ + #[cfg(unix)] Attribute::Mode, + Attribute::Ownership, + Attribute::Timestamps, +]; + pub fn uumain(args: Vec) -> i32 { - let mut opts = Options::new(); + let about = get_about(USAGE); + let matches = App::new(executable!()) + .version(VERSION) + .about(&about[..]) + .arg(Arg::with_name(OPT_TARGET_DIRECTORY) + .short("t") + .conflicts_with(OPT_NO_TARGET_DIRECTORY) + .long(OPT_TARGET_DIRECTORY) + .value_name(OPT_TARGET_DIRECTORY) + .takes_value(true) + .help("copy all SOURCE arguments into target-directory")) + .arg(Arg::with_name(OPT_NO_TARGET_DIRECTORY) + .short("T") + .long(OPT_NO_TARGET_DIRECTORY) + .conflicts_with(OPT_TARGET_DIRECTORY) + .help("Treat DEST as a regular file and not a directory")) + .arg(Arg::with_name(OPT_VERSION) + .short("V") + .long(OPT_VERSION) + .help("output version information and exit")) + .arg(Arg::with_name(OPT_INTERACTIVE) + .short("i") + .long(OPT_INTERACTIVE) + .conflicts_with(OPT_NO_CLOBBER) + .help("ask before overwriting files")) + .arg(Arg::with_name(OPT_LINK) + .short("l") + .long(OPT_LINK) + .help("hard-link files instead of copying")) + .arg(Arg::with_name(OPT_NO_CLOBBER) + .short("n") + .long(OPT_NO_CLOBBER) + .conflicts_with(OPT_INTERACTIVE) + .help("don't overwrite a file that already exists")) + .arg(Arg::with_name(OPT_RECURSIVE) + .short("r") + .long(OPT_RECURSIVE) + .help("copy directories recursively")) + .arg(Arg::with_name(OPT_RECURSIVE_ALIAS) + .short("R") + .help("same as -r")) + .arg(Arg::with_name(OPT_VERBOSE) + .short("v") + .long(OPT_VERBOSE) + .help("explicitly state what is being done")) + .arg(Arg::with_name(OPT_SYMBOLIC_LINK) + .short("s") + .long(OPT_SYMBOLIC_LINK) + .conflicts_with(OPT_LINK) + .help("make symbolic links instead of copying")) + .arg(Arg::with_name(OPT_FORCE) + .short("f") + .long(OPT_FORCE) + .help("if an existing destination file cannot be opened, remove it and \ + try again (this option is ignored when the -n option is also used). \ + Currently not implemented for Windows.")) + .arg(Arg::with_name(OPT_REMOVE_DESTINATION) + .long(OPT_REMOVE_DESTINATION) + .conflicts_with(OPT_FORCE) + .help("remove each existing destination file before attempting to open it \ + (contrast with --force). On Windows, current only works for writeable files.")) + .arg(Arg::with_name(OPT_BACKUP) + .short("b") + .long(OPT_BACKUP) + .help("make a backup of each existing destination file")) + .arg(Arg::with_name(OPT_SUFFIX) + .short("S") + .long(OPT_SUFFIX) + .takes_value(true) + .default_value("~") + .value_name("SUFFIX") + .help("override the usual backup suffix")) - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("r", "recursive", "copy directories recursively"); - opts.optflag("", "version", "output version information and exit"); - opts.optopt("t", - "target-directory", - "copy all SOURCE arguments into DIRECTORY", - "DEST"); - opts.optflag("T", - "no-target-directory", - "Treat DEST as a regular file and not a directory"); - opts.optflag("v", "verbose", "explicitly state what is being done"); + // TODO: implement the following args + .arg(Arg::with_name(OPT_ARCHIVE) + .short("a") + .long(OPT_ARCHIVE) + .conflicts_with_all(&[OPT_PRESERVE_DEFUALT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE]) + .help("NotImplemented: same as -dR --preserve=all")) + .arg(Arg::with_name(OPT_ATTRIBUTES_ONLY) + .long(OPT_ATTRIBUTES_ONLY) + .conflicts_with(OPT_COPY_CONTENTS) + .help("NotImplemented: don't copy the file data, just the attributes")) + .arg(Arg::with_name(OPT_COPY_CONTENTS) + .long(OPT_COPY_CONTENTS) + .conflicts_with(OPT_ATTRIBUTES_ONLY) + .help("NotImplemented: copy contents of special files when recursive")) + .arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS) + .short("d") + .help("NotImplemented: same as --no-dereference --preserve=links")) + .arg(Arg::with_name(OPT_DEREFERENCE) + .short("L") + .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_PRESERVE_DEFUALT_ATTRIBUTES) + .short("-p") + .long(OPT_PRESERVE_DEFUALT_ATTRIBUTES) + .conflicts_with_all(&[OPT_PRESERVE, OPT_NO_PRESERVE, OPT_ARCHIVE]) + .help("NotImplemented: same as --preserve=mode(unix only),ownership,timestamps")) + .arg(Arg::with_name(OPT_PRESERVE) + .long(OPT_PRESERVE) + .takes_value(true) + .multiple(true) + .possible_values(PRESERVABLE_ATTRIBUTES) + .value_name("ATTR_LIST") + .conflicts_with_all(&[OPT_PRESERVE_DEFUALT_ATTRIBUTES, OPT_NO_PRESERVE, OPT_ARCHIVE]) + .help("NotImplemented: preserve the specified attributes (default: mode(unix only),ownership,timestamps),\ + if possible additional attributes: context, links, xattr, all")) + .arg(Arg::with_name(OPT_NO_PRESERVE) + .long(OPT_NO_PRESERVE) + .takes_value(true) + .value_name("ATTR_LIST") + .conflicts_with_all(&[OPT_PRESERVE_DEFUALT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE]) + .help("NotImplemented: don't preserve the specified attributes")) + .arg(Arg::with_name(OPT_PARENTS) + .long(OPT_PARENTS) + .help("NotImplemented: use full source file name under DIRECTORY")) + .arg(Arg::with_name(OPT_REFLINK) + .long(OPT_REFLINK) + .takes_value(true) + .value_name("WHEN") + .help("NotImplemented: control clone/CoW copies. See below")) + .arg(Arg::with_name(OPT_SPARSE) + .long(OPT_SPARSE) + .takes_value(true) + .value_name("WHEN") + .help("NotImplemented: control creation of sparse files. See below")) + .arg(Arg::with_name(OPT_STRIP_TRAILING_SLASHES) + .long(OPT_STRIP_TRAILING_SLASHES) + .help("NotImplemented: remove any trailing slashes from each SOURCE argument")) + .arg(Arg::with_name(OPT_UPDATE) + .short("u") + .long(OPT_UPDATE) + .help("NotImplemented: copy only when the SOURCE file is newer than the destination file\ + or when the destination file is missing")) + .arg(Arg::with_name(OPT_ONE_FILE_SYSTEM) + .short("x") + .long(OPT_ONE_FILE_SYSTEM) + .help("NotImplemented: stay on this file system")) + .arg(Arg::with_name(OPT_CONTEXT) + .long(OPT_CONTEXT) + .takes_value(true) + .value_name("CTX") + .help("NotImplemented: set SELinux security context of destination file to default type")) + .arg(Arg::with_name(OPT_CLI_SYMBOLIC_LINKS) + .short("H") + .help("NotImplemented: follow command-line symbolic links in SOURCE")) + // END TODO - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(e) => { - show_error!("{}", e); - panic!() - } - }; - let usage = opts.usage("Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."); - let mode = if matches.opt_present("version") { - Mode::Version - } else if matches.opt_present("help") { - Mode::Help - } else { - Mode::Copy - }; + .arg(Arg::with_name(OPT_PATHS) + .multiple(true)) + .get_matches_from(&args); - match mode { - Mode::Copy => copy(matches), - Mode::Help => help(&usage), - Mode::Version => version(), + if matches.is_present(OPT_VERSION) { + print_version(); + return EXIT_OK; } - 0 + let options = crash_if_err!(EXIT_ERR, Options::from_matches(&matches)); + let paths: Vec = matches.values_of("paths") + .map(|v| v.map(|p| p.to_string()).collect()) + .unwrap_or_default(); + + let (sources, target) = crash_if_err!(EXIT_ERR, parse_path_args(&paths, &options)); + + if let Err(error) = copy(&sources, &target, &options) { + match error { + // Error::NotAllFilesCopied is non-fatal, but the error + // code should still be EXIT_ERR as does GNU cp + Error::NotAllFilesCopied => {} + // Else we caught a fatal bubbled-up error, log it to stderr + _ => show_error!("{}", error), + }; + return EXIT_ERR; + } + + EXIT_OK } -fn version() { - println!("{} {}", NAME, VERSION); -} - -fn help(usage: &str) { - let msg = format!("{0} {1}\n\n\ - Usage: {0} SOURCE DEST\n \ - or: {0} SOURCE... DIRECTORY\n \ - or: {0} -t DIRECTORY SOURCE...\n\ - \n\ - {2}", - NAME, - VERSION, - usage); - println!("{}", msg); -} - -fn copy(matches: getopts::Matches) { - let verbose = matches.opt_present("verbose"); - let sources: Vec = if matches.free.is_empty() { - show_error!("Missing SOURCE or DEST argument. Try --help."); - panic!() - } else if !matches.opt_present("target-directory") { - matches.free[..matches.free.len() - 1] - .iter() - .cloned() - .collect() - } else { - matches.free.iter().cloned().collect() - }; - let recursive: bool = matches.opt_present("recursive"); - - let dest_str = if matches.opt_present("target-directory") { - matches - .opt_str("target-directory") - .expect("Option -t/--target-directory requires an argument") - } else { - matches.free[matches.free.len() - 1].clone() - }; - let dest = if matches.free.len() < 2 && !matches.opt_present("target-directory") { - show_error!("Missing DEST argument. Try --help."); - panic!() - } else { - //the argument to the -t/--target-directory= options - let path = Path::new(&dest_str); - if !path.is_dir() && matches.opt_present("target-directory") { - show_error!("Target {} is not a directory", - matches.opt_str("target-directory").unwrap()); - panic!() +impl ClobberMode { + fn from_matches(matches: &ArgMatches) -> ClobberMode { + if matches.is_present(OPT_FORCE) { + ClobberMode::Force + } else if matches.is_present(OPT_REMOVE_DESTINATION) { + ClobberMode::RemoveDestination } else { - path + ClobberMode::Standard + } + } +} + +impl OverwriteMode { + fn from_matches(matches: &ArgMatches) -> OverwriteMode { + if matches.is_present(OPT_INTERACTIVE) { + OverwriteMode::Interactive(ClobberMode::from_matches(matches)) + } else if matches.is_present(OPT_NO_CLOBBER) { + OverwriteMode::NoClobber + } else { + OverwriteMode::Clobber(ClobberMode::from_matches(matches)) + } + } +} + +impl CopyMode { + fn from_matches(matches: &ArgMatches) -> CopyMode { + if matches.is_present(OPT_LINK) { + CopyMode::Link + } else if matches.is_present(OPT_SYMBOLIC_LINK) { + CopyMode::SymLink + } else if matches.is_present(OPT_SPARSE) { + CopyMode::Sparse + } else { + CopyMode::Copy + } + } +} + +impl FromStr for Attribute { + type Err = Error; + + fn from_str(value: &str) -> CopyResult { + Ok(match &*value.to_lowercase() { + #[cfg(unix)] "mode" => Attribute::Mode, + "ownership" => Attribute::Ownership, + "timestamps" => Attribute::Timestamps, + "context" => Attribute::Context, + "links" => Attribute::Links, + "xattr" => Attribute::Xattr, + "all" => Attribute::All, + _ => return Err(Error::InvalidArgument(format!("invalid attribute '{}'", value))) + }) + } +} + +impl Options { + fn from_matches(matches: &ArgMatches) -> CopyResult { + let not_implemented_opts = vec![ + OPT_ARCHIVE, + OPT_ATTRIBUTES_ONLY, + OPT_COPY_CONTENTS, + OPT_NO_DEREFERENCE_PRESERVE_LINKS, + OPT_DEREFERENCE, + OPT_NO_DEREFERENCE, + OPT_PRESERVE_DEFUALT_ATTRIBUTES, + OPT_PRESERVE, + OPT_NO_PRESERVE, + OPT_PARENTS, + OPT_REFLINK, + OPT_SPARSE, + OPT_STRIP_TRAILING_SLASHES, + OPT_UPDATE, + OPT_ONE_FILE_SYSTEM, + OPT_CONTEXT, + #[cfg(windows)] OPT_FORCE, + ]; + + for not_implemented_opt in not_implemented_opts { + if matches.is_present(not_implemented_opt) { + return Err(Error::NotImplemented(not_implemented_opt.to_string())) + } } - }; + let recursive = matches.is_present(OPT_RECURSIVE) + || matches.is_present(OPT_RECURSIVE_ALIAS) + || matches.is_present(OPT_ARCHIVE); - assert!(sources.len() >= 1); - if matches.opt_present("no-target-directory") && dest.is_dir() { - show_error!("Can't overwrite directory {} with non-directory", - dest.display()); - panic!() + let backup = matches.is_present(OPT_BACKUP) + || matches.is_present(OPT_SUFFIX); + + // Parse target directory options + let no_target_dir = matches.is_present(OPT_NO_TARGET_DIRECTORY); + let target_dir = matches.value_of(OPT_TARGET_DIRECTORY).map(|v| v.to_string()); + + // Parse attributes to preserve + let preserve_attributes: Vec = if matches.is_present(OPT_PRESERVE) { + match matches.values_of(OPT_PRESERVE) { + None => DEFAULT_ATTRIBUTES.to_vec(), + Some(attribute_strs) => { + let mut attributes = Vec::new(); + for attribute_str in attribute_strs { + attributes.push(Attribute::from_str(attribute_str)?); + } + attributes + } + } + } else if matches.is_present(OPT_PRESERVE_DEFUALT_ATTRIBUTES) { + DEFAULT_ATTRIBUTES.to_vec() + } else { + vec![] + }; + + let options = Options { + attributes_only: matches.is_present(OPT_ATTRIBUTES_ONLY), + copy_contents: matches.is_present(OPT_COPY_CONTENTS), + copy_mode: CopyMode::from_matches(matches), + dereference: matches.is_present(OPT_DEREFERENCE), + one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM), + overwrite: OverwriteMode::from_matches(matches), + parents: matches.is_present(OPT_PARENTS), + backup_suffix: matches.value_of(OPT_SUFFIX).unwrap().to_string(), + update: matches.is_present(OPT_UPDATE), + verbose: matches.is_present(OPT_VERBOSE), + backup, + no_target_dir, + preserve_attributes, + recursive, + target_dir, + }; + + Ok(options) + } +} + + +impl TargetType { + /// Return TargetType required for `target`. + /// + /// Treat target as a dir if we have multiple sources or the target + /// exists and already is a directory + fn determine(sources: &[Source], target: &Target) -> TargetType { + if sources.len() > 1 || target.is_dir() { + TargetType::Directory + } else { + TargetType::File + } + } +} + + +/// Returns tuple of (Source paths, Target) +fn parse_path_args(path_args: &[String], options: &Options) -> CopyResult<(Vec, Target)> { + let mut paths = path_args.iter().map(PathBuf::from).collect::>(); + + if paths.len() < 1 { + // No files specified + return Err("missing file operand".into()); } - if sources.len() == 1 { - let source = Path::new(&sources[0]); - let same_file = - paths_refer_to_same_file(source, dest).unwrap_or_else(|err| match err.kind() { - ErrorKind::NotFound => false, - _ => { - show_error!("{}", err); - panic!() - } - }); + // Return an error if the user requested to copy more than one + // file source to a file target + if options.no_target_dir && !options.target_dir.is_some() && paths.len() > 2 { + return Err(format!("extra operand {:?}", paths[2]).into()); + } - if same_file { - show_error!("\"{}\" and \"{}\" are the same file", - source.display(), - dest.display()); - panic!(); + let (sources, target) = match options.target_dir { + Some(ref target) => { + // All path arges are sources, and the target dir was + // specified separately + (paths, PathBuf::from(target)) } - let mut full_dest = dest.to_path_buf(); - if recursive { - for entry in WalkDir::new(source) { - let entry = entry.unwrap(); - if entry.path().is_dir() { - let mut dst_path = full_dest.clone(); - dst_path.push(entry.path()); - if let Err(err) = fs::create_dir(dst_path) { - show_error!("{}", err); - panic!(); - } + None => { + // If there was no explicit target-dir, then use the last + // path_arg + let target = paths.pop().unwrap(); + (paths, target) + } + }; + + Ok((sources, target)) +} + + +/// Copy all `sources` to `target`. Returns an +/// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was +/// encountered. +/// +/// Behavior depends on `options`, see [`Options`] for details. +/// +/// [`Options`]: ./struct.Options.html +fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()> { + let target_type = TargetType::determine(sources, target); + verify_target_type(target, &target_type)?; + + let mut non_fatal_errors = false; + let mut seen_sources = HashSet::with_capacity(sources.len()); + + for source in sources { + if seen_sources.contains(source) { + show_warning!("source '{}' specified more than once", source.display()); + + } else if let Err(error) = copy_source(source, target, &target_type, options) { + show_error!("{}", error); + match error { + Error::Skipped(_) => (), + _ => non_fatal_errors = true, + } + } + seen_sources.insert(source); + } + + if non_fatal_errors { + Err(Error::NotAllFilesCopied) + } else { + Ok(()) + } +} + + +fn construct_dest_path(source_path: &Path, target: &Target, target_type: &TargetType, options: &Options) + -> CopyResult +{ + if options.no_target_dir && target.is_dir() { + return Err(format!("cannot overwrite directory '{}' with non-directory", target.display()).into()) + } + + Ok(match *target_type { + TargetType::Directory => { + let root = source_path.parent().unwrap_or(source_path); + localize_to_target(root, source_path, target)? + }, + TargetType::File => target.to_path_buf(), + }) +} + +fn copy_source(source: &Source, target: &Target, target_type: &TargetType, options: &Options) + -> CopyResult<()> +{ + let source_path = Path::new(&source); + + if source_path.is_dir() { + // Copy as directory + copy_directory(source, target, options) + } else { + // Copy as file + let dest = construct_dest_path(source_path, target, target_type, options)?; + copy_file(source_path, dest.as_path(), options) + } +} + + +/// Read the contents of the directory `root` and recursively copy the +/// contents to `target`. +/// +/// Any errors encounted copying files in the tree will be logged but +/// will not cause a short-circuit. +fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult<()> { + if !options.recursive { + return Err(format!("omitting directory '{}'", root.display()).into()); + } + + let root_path = Path::new(&root).canonicalize()?; + let root_parent = root_path.parent(); + + for path in WalkDir::new(root) { + let path = or_continue!(or_continue!(path).path().canonicalize()); + let local_to_root_parent = match root_parent { + Some(parent) => or_continue!(path.strip_prefix(&parent)).to_path_buf(), + None => path.clone(), + }; + + let local_to_target = target.join(&local_to_root_parent); + + if path.is_dir() && !local_to_target.exists() { + or_continue!(fs::create_dir_all(local_to_target.clone())); + } else if !path.is_dir() { + copy_file(path.as_path(), local_to_target.as_path(), options)?; + } + } + + Ok(()) +} + + +impl OverwriteMode { + fn verify(&self, path: &Path) -> CopyResult<()> { + match *self { + OverwriteMode::NoClobber => { + Err(Error::Skipped(format!("Not overwriting {} because of option '{}'", path.display(), OPT_NO_CLOBBER))) + }, + OverwriteMode::Interactive(_) => { + if prompt_yes!("{}: overwrite {}? ", executable!(), path.display()) { + Ok(()) } else { - let mut dst_path = full_dest.clone(); - dst_path.push(entry.path()); - if let Err(err) = fs::copy(entry.path(), dst_path) { - show_error!("{}", err); - panic!(); - } + Err(Error::Skipped(format!("Not overwriting {} at user request", path.display()))) } - } - } else { - if dest.is_dir() { - full_dest.push(source.file_name().unwrap()); //the destination path is the destination - } // directory + the file name we're copying - if verbose { - println!("{} -> {}", source.display(), full_dest.display()); - } - if let Err(err) = fs::copy(source, full_dest) { - show_error!("{}", err); - panic!(); - } - } - } else { - if !dest.is_dir() { - show_error!("TARGET must be a directory"); - panic!(); - } - for src in &sources { - let source = Path::new(&src); - - if !recursive { - if !source.is_file() { - show_error!("\"{}\" is not a file", source.display()); - continue; - } - - let mut full_dest = dest.to_path_buf(); - - full_dest.push(source.file_name().unwrap()); - - if verbose { - println!("{} -> {}", source.display(), full_dest.display()); - } - - let io_result = fs::copy(source, full_dest); - - if let Err(err) = io_result { - show_error!("{}", err); - panic!() - } - } else { - for entry in WalkDir::new(source) { - let entry = entry.unwrap(); - let full_dest = dest.to_path_buf(); - - if entry.path().is_dir() { - let mut dst_path = full_dest.clone(); - dst_path.push(entry.path()); - if let Err(err) = fs::create_dir(dst_path) { - show_error!("{}", err); - panic!(); - } - } else { - let mut dst_path = full_dest.clone(); - dst_path.push(entry.path()); - if let Err(err) = fs::copy(entry.path(), dst_path) { - show_error!("{}", err); - panic!(); - } - } - } - } + }, + OverwriteMode::Clobber(_) => Ok(()), } } } -pub fn paths_refer_to_same_file(p1: &Path, p2: &Path) -> Result { + +fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResult<()> { + let context = &*format!("'{}' -> '{}'", source.display().to_string(), dest.display()); + Ok(match *attribute { + #[cfg(unix)] + Attribute::Mode => { + let mode = fs::metadata(source).context(context)?.permissions().mode(); + let mut dest_metadata = fs::metadata(source).context(context)?.permissions(); + dest_metadata.set_mode(mode); + }, + Attribute::Ownership => { + let metadata = fs::metadata(source).context(context)?; + fs::set_permissions(dest, metadata.permissions()).context(context)?; + }, + Attribute::Timestamps => return Err(Error::NotImplemented("preserving timestamp not implemented".to_string())), + Attribute::Context => return Err(Error::NotImplemented("preserving context not implemented".to_string())), + Attribute::Links => return Err(Error::NotImplemented("preserving links not implemented".to_string())), + Attribute::Xattr => return Err(Error::NotImplemented("preserving xattr not implemented".to_string())), + Attribute::All => return Err(Error::NotImplemented("preserving a not implemented".to_string())), + }) +} + +#[cfg(not(windows))] +fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { + Ok(std::os::unix::fs::symlink(source, dest).context(context)?) +} + +#[cfg(windows)] +fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { + Ok(std::os::windows::fs::symlink_file(source, dest).context(context)?) +} + +fn context_for(src: &Path, dest: &Path) -> String { + format!("'{}' -> '{}'", src.display(), dest.display()) +} + +/// Implements a relatively naive backup that is not as full featured +/// as GNU cp. No CONTROL version control method argument is taken +/// for backups. +/// TODO: Add version control methods +fn backup_file(path: &Path, suffix: &str) -> CopyResult { + let mut backup_path = path.to_path_buf().into_os_string(); + backup_path.push(suffix); + fs::copy(path, &backup_path)?; + Ok(backup_path.into()) +} + +fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { + if paths_refer_to_same_file(source, dest)? { + return Err(format!("{}: same file", context_for(source, dest)).into()); + } + + options.overwrite.verify(dest)?; + + if options.backup { + backup_file(dest, &options.backup_suffix)?; + } + + match options.overwrite { + OverwriteMode::Clobber(ClobberMode::Force) => { + if fs::metadata(dest)?.permissions().readonly() { + fs::remove_file(dest)?; + } + }, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) => { + fs::remove_file(dest)?; + }, + _ => (), + }; + + Ok(()) +} + +/// Copy the a file from `source` to `dest`. No path manipulation is +/// done on either `source` or `dest`, the are used as provieded. +/// +/// Behavior when copying to existing files is contingent on the +/// `options.overwrite` mode. If a file is skipped, the return type +/// should be `Error:Skipped` +/// +/// The original permissions of `source` will be copied to `dest` +/// after a successful copy. +fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { + if dest.exists() { + handle_existing_dest(source, dest, options)?; + } + + if options.verbose { + println!("{}", context_for(source, dest)); + } + + match options.copy_mode { + CopyMode::Link => { fs::hard_link(source, dest).context(&*context_for(source, dest))?; }, + CopyMode::Copy => { fs::copy(source, dest).context(&*context_for(source, dest))?; }, + CopyMode::SymLink => { symlink_file(source, dest, &*context_for(source, dest))?; }, + CopyMode::Sparse => return Err(Error::NotImplemented(OPT_SPARSE.to_string())), + }; + + for attribute in &options.preserve_attributes { + copy_attribute(source, dest, attribute)?; + } + + Ok(()) +} + + +/// Generate an error message if `target` is not the correct `target_type` +pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { + match (target_type, target.is_dir()) { + (&TargetType::Directory, false) => { + Err(format!("target: '{}' is not a directory", target.display()).into()) + } + (&TargetType::File, true) => { + Err(format!("cannot overwrite directory '{}' with non-directory", target.display()).into()) + } + _ => Ok(()), + } +} + + +/// Remove the `root` prefix from `source` and prefix it with `target` +/// to create a file that is local to `target` +/// # Examples +/// +/// ```ignore +/// assert!(uu_cp::localize_to_target( +/// &Path::new("a/source/"), +/// &Path::new("a/source/c.txt"), +/// &Path::new("target/"), +/// ).unwrap() == Path::new("target/c.txt")) +/// ``` +pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResult { + let local_to_root = source.strip_prefix(&root)?; + Ok(target.join(&local_to_root)) +} + + +pub fn paths_refer_to_same_file(p1: &Path, p2: &Path) -> io::Result { // We have to take symlinks and relative paths into account. let pathbuf1 = try!(canonicalize(p1, CanonicalizeMode::Normal)); let pathbuf2 = try!(canonicalize(p2, CanonicalizeMode::Normal)); Ok(pathbuf1 == pathbuf2) } + + +#[test] +fn test_cp_localize_to_target() { + assert!(localize_to_target( + &Path::new("a/source/"), + &Path::new("a/source/c.txt"), + &Path::new("target/") + ).unwrap() == Path::new("target/c.txt")) +} diff --git a/tests/fixtures/cp/existing_file.txt b/tests/fixtures/cp/existing_file.txt new file mode 100644 index 000000000..651b4c7b0 --- /dev/null +++ b/tests/fixtures/cp/existing_file.txt @@ -0,0 +1 @@ +Cogito ergo sum. diff --git a/tests/fixtures/cp/how_are_you.txt b/tests/fixtures/cp/how_are_you.txt new file mode 100644 index 000000000..d18c6b11f --- /dev/null +++ b/tests/fixtures/cp/how_are_you.txt @@ -0,0 +1 @@ +How are you? diff --git a/tests/test_cp.rs b/tests/test_cp.rs index 7aa01b5c3..02f7c3b5a 100644 --- a/tests/test_cp.rs +++ b/tests/test_cp.rs @@ -1,11 +1,15 @@ use common::util::*; +use std::fs::set_permissions; -static TEST_HELLO_WORLD_SOURCE: &'static str = "hello_world.txt"; -static TEST_HELLO_WORLD_DEST: &'static str = "copy_of_hello_world.txt"; -static TEST_COPY_TO_FOLDER: &'static str = "hello_dir/"; -static TEST_COPY_TO_FOLDER_FILE: &'static str = "hello_dir/hello_world.txt"; -static TEST_COPY_FROM_FOLDER: &'static str = "hello_dir_with_file/"; -static TEST_COPY_FROM_FOLDER_FILE: &'static str = "hello_dir_with_file/hello_world.txt"; +static TEST_EXISTING_FILE: &str = "existing_file.txt"; +static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt"; +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"; #[test] fn test_cp_cp() { @@ -23,6 +27,58 @@ fn test_cp_cp() { assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); } + +#[test] +fn test_cp_duplicate_files() { + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_COPY_TO_FOLDER) + .run(); + + assert!(result.success); + assert!(result.stderr.contains("specified more than once")); + assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); +} + + +#[test] +fn test_cp_multiple_files_target_is_file() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_EXISTING_FILE) + .run(); + + assert!(!result.success); + assert!(result.stderr.contains("not a directory")); +} + +#[test] +fn test_cp_directory_not_recursive() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_COPY_TO_FOLDER) + .arg(TEST_HELLO_WORLD_DEST) + .run(); + + assert!(!result.success); + assert!(result.stderr.contains("omitting directory")); +} + + +#[test] +fn test_cp_multiple_files() { + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg(TEST_COPY_TO_FOLDER) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); + assert_eq!(at.read(TEST_HOW_ARE_YOU_DEST), "How are you?\n"); +} + #[test] fn test_cp_recurse() { //let (at, mut ucmd) = at_and_ucmd!(); @@ -82,3 +138,156 @@ fn test_cp_with_dirs() { assert!(result_from_dir.success); assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); } + +#[test] +fn test_cp_arg_target_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("-t") + .arg(TEST_COPY_TO_FOLDER) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); +} + +#[test] +fn test_cp_arg_no_target_directory() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("-v") + .arg("-T") + .arg(TEST_COPY_TO_FOLDER) + .run(); + + assert!(!result.success); + assert!(result.stderr.contains("cannot overwrite directory")); +} + +#[test] +fn test_cp_arg_interactive() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg("-i") + .pipe_in("N\n") + .run(); + + assert!(result.success); + assert!(result.stderr.contains("Not overwriting")); +} + +#[test] +#[cfg(target_os="unix")] +fn test_cp_arg_link() { + use std::os::linux::fs::MetadataExt; + + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--link") + .arg(TEST_HELLO_WORLD_DEST) + .run(); + + assert!(result.success); + assert_eq!(at.metadata(TEST_HELLO_WORLD_SOURCE).st_nlink(), 2); +} + +#[test] +fn test_cp_arg_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--symbolic-link") + .arg(TEST_HELLO_WORLD_DEST) + .run(); + + assert!(result.success); + assert!(at.is_symlink(TEST_HELLO_WORLD_DEST)); +} + + +#[test] +fn test_cp_arg_no_clobber() { + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--no-clobber") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n"); + assert!(result.stderr.contains("Not overwriting")); +} + +#[test] +#[cfg(not(windows))] +fn test_cp_arg_force() { + let (at, mut ucmd) = at_and_ucmd!(); + + // create dest without write permissions + let mut permissions = at.make_file(TEST_HELLO_WORLD_DEST).metadata().unwrap().permissions(); + permissions.set_readonly(true); + set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap(); + + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--force") + .arg(TEST_HELLO_WORLD_DEST) + .run(); + + println!("{:?}", result.stderr); + println!("{:?}", result.stdout); + + assert!(result.success); + assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); +} + +/// TODO: write a better test that differentiates --remove-destination +/// from --force. Also this test currently doesn't work on +/// Windows. This test originally checked file timestamps, which +/// proved to be unreliable per target / CI platform +#[test] +#[cfg(not(windows))] +fn test_cp_arg_remove_destination() { + let (at, mut ucmd) = at_and_ucmd!(); + + // create dest without write permissions + let mut permissions = at.make_file(TEST_HELLO_WORLD_DEST).metadata().unwrap().permissions(); + permissions.set_readonly(true); + set_permissions(at.plus(TEST_HELLO_WORLD_DEST), permissions).unwrap(); + + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--remove-destination") + .arg(TEST_HELLO_WORLD_DEST) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HELLO_WORLD_DEST), "Hello, World!\n"); +} + +#[test] +fn test_cp_arg_backup() { + let (at, mut ucmd) = at_and_ucmd!(); + + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--backup") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + assert_eq!(at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)), "How are you?\n"); +} + +#[test] +fn test_cp_arg_suffix() { + let (at, mut ucmd) = at_and_ucmd!(); + + let result = ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg("--suffix") + .arg(".bak") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + assert_eq!(at.read(&*format!("{}.bak", TEST_HOW_ARE_YOU_SOURCE)), "How are you?\n"); +}