core-utils 0.1.2

A reimplementation of the GNU core utils in the Rust programming language
// Copyright (C) 2015, Alberto Corona <ac@albertocorona.com>
// All rights reserved. This file is part of core-utils, distributed under the
// GPL v3 license. For full terms please see the LICENSE file.

#![crate_type = "bin"]
#![crate_name = "cp"]
#![feature(path_ext,slice_splits)]

static UTIL: Prog = Prog { name: "cp", vers: "0.1.0", yr: "2015", };

extern crate pgetopts;
extern crate rpf;

use pgetopts::{Options};
use rpf::{Status,Styled,Color,PathMod,Prog};

use std::env;
use std::fs;
use std::io;
#[cfg(target_family = "unix")]
use std::os;
use std::fs::{PathExt};
use std::path::{PathBuf,Path};
#[cfg(target_family = "windows")]
use std::os::windows;

struct Opts {
    verbose: bool,
    force: bool,
    inter: bool,
}

// Copy types, either copy regular files, physical links, or symbolic links
enum CopyType {
    Norm,
    Phys,
    Sym,
}

// EntryMode for traversal, Norm for normal entry and `Sym` for following
// symbolic links
#[derive(PartialEq)]
enum EntryMode {
    Norm,
    Sym,
}

// Unix family specific symbolic link function
#[cfg(target_family = "unix")]
fn sym_link<F: AsRef<Path>, T: AsRef<Path>>
(from: &F, to: &T, options: &Opts) {
    if options.force && to.as_ref().exists() { fs::remove_file(to).ok(); }
    match os::unix::fs::symlink(from, to) {
        Ok(_) => {
            if options.verbose {
                println!("{} {} {}", from.as_ref().as_str().bold(),
                "->".paint(Color::Green), to.as_ref().as_str().bold());
            }
        },
        Err(e) => {
            Status::Error.err(UTIL.name, e.to_string());
        }
    };
}

// Windows specific symbolic link function
#[cfg(target_family = "windows")]
fn sym_link<F: AsRef<Path>, T: AsRef<Path>>
(from: &F, to: &T, options: &Opts) {
    if from.as_ref().is_file() {
        if options.force { fs::remove_file(to).ok(); }
        match windows::fs::symlink_file(from, to) {
            Ok(_) => {
                if options.verbose {
                    println!("{} {} {}", from.as_ref().as_str().bold(),
                    "->".paint(Color::Green), to.as_ref().as_str().bold());
                }
            },
            Err(e) => {
                Status::Error.err(UTIL.name, e.to_string());
            }
        };
    } else if from.as_ref().is_dir() {
        if options.force {
            fs::remove_dir_all(to).ok();
        }
        match windows::fs::symlink_dir(from, to) {
            Ok(_) => {
                if options.verbose {
                    println!("{} {} {}", from.as_ref().as_str().bold(),
                    "->".paint(Color::Green), to.as_ref().as_str().bold());
                }
            },
            Err(e) => {
                Status::Error.err(UTIL.name, e.to_string());
            }
        };
    }
}

fn hard_link<F: AsRef<Path>, T: AsRef<Path>>
(from: &F, to: &T, options: &Opts) {
    if !from.as_ref().exists() {
        Status::Error.err(UTIL.name, String::from("\'".to_string() +
                                                  from.as_ref().as_str() +
                                                  "\' does not exist"));
    }
    match fs::hard_link(from, to) {
        Ok(_) => {
            if options.force { fs::remove_file(to).ok(); }
            if options.verbose {
                println!("{} {} {}", from.as_ref().as_str().bold(),
                "->".paint(Color::Green), to.as_ref().as_str().bold());
            }
        },
        Err(e) => {
            Status::Error.err(UTIL.name, e.to_string());
        }
    };
}

// Prompt for interactive mode
fn ask<T: AsRef<Path>>(to: &T) -> bool {
    if to.as_ref().exists() {
        println!("cp: Overwrite '{}'?", &to.as_ref().as_str().bold());
        let mut ans = String::new();
        io::stdin().read_line(&mut ans).ok();
        // `ans` is trimmed as `read_line` also reads a newline
        ans = ans.trim().to_lowercase();
        if ans == "y" || ans == "yes" {
            fs::remove_file(to).ok();
            return true;
        }
    }
    return false;
}

fn copy<F: AsRef<Path>, T: AsRef<Path>>(from: &F, to: &T, options: &Opts) {
    match fs::copy(from, to) {
        Ok(_) => {
            if options.verbose {
                println!("{} {} {}", &from.as_ref().as_str().bold(),
                "->".paint(Color::Green), to.as_ref().as_str().bold());
            }
        },
        Err(e) => {
            Status::Error.err(UTIL.name, e.to_string());
            panic!();
        }
    };
}

fn match_copytype<T: AsRef<Path>>
(file: &String, to: &T, options: &Opts, copytype: &CopyType) {
    match copytype {
        &CopyType::Norm  => { copy(&file, &to, options) },
        &CopyType::Sym   => { sym_link(&file, &to, options) },
        &CopyType::Phys  => { hard_link(&file, &to, options) },
    };
}

fn copy_files<T: AsRef<Path>>
(from: Vec<String>, to: &T, options: &Opts, copytype: &CopyType) {
    for file in from.iter() {
        let dest = Path::new(file).file_name().unwrap();
        if !to.as_ref().is_dir() {
            Status::Error.err(UTIL.name, String::from(
                    to.as_ref().as_str())
                + " is not a directory");
        } else if !options.inter {
            match_copytype(file, &to.as_ref().join(dest), options, copytype);
        } else if options.inter && !to.as_ref().join(dest).exists() {
            match_copytype(file, &to.as_ref().join(dest), options, copytype);
        } else if options.inter {
            if ask(&to.as_ref().join(dest)) {
                match_copytype(file, &to.as_ref().join(dest), options, copytype);
            }
        }
    }
}

fn create_dir<F: AsRef<Path>, T: AsRef<Path>>
(from: &F, to: &T, options: &Opts) {
    match fs::create_dir_all(&to.as_ref()) {
        Ok(_) => {
            if options.verbose {
                println!("{} {} {}", from.as_ref().as_str().bold(),
                "->".paint(Color::Green), to.as_ref().as_str().bold());
            }
        },
        Err(e) => {
            panic!(e.to_string());
        }
    };
}

fn copy_dir<F: AsRef<Path>>
(from: &F, to: &mut PathBuf, options: &Opts, copytype: &CopyType, mode: &EntryMode) {
    if !from.as_ref().exists() {
        Status::Error.err(UTIL.name,
                          String::from(from.as_ref().as_string() +
                                       " does not exist"));
    }
    let dest = from.as_ref().last_component();
    if is_symlink(from) && mode == &EntryMode::Sym {
        if !to.join(&dest).exists() {
            create_dir(&from, &to.join(&dest), options);
        }
        walk_dir_copy(&from, to.join(&dest), options, &copytype).ok();
    } else if !is_symlink(from) {
        walk_dir_copy(&from, to.join(&dest), options, &copytype).ok();
    }
}

// Checks for endless recursive copying. Only returns false when there is no
// collision, otherwise errors using `rpf::Status::Error.err`
fn check_endless_copy<F: AsRef<Path>, T: AsRef<Path>>(from: &F, to: &T) {
    let mut to_clone = to.as_ref().clone().to_path_buf();
    // Pop twice to check if we're recursively copying.
    // Ex: `cp -rv /test/items /test/items/things` would error
    to_clone.pop(); to_clone.pop();
    if to_clone == from.as_ref().to_path_buf() {
        Status::Error.err(UTIL.name,
                          "cannot copy a directory into itself".to_string());
    }
    if from.as_ref().last_component() == to.as_ref().first_component() {
        Status::Error.err(UTIL.name,
                          "cannot copy a directory into itself".to_string());
    }
}

fn is_symlink<P: AsRef<Path>>(path: &P) -> bool {
    match fs::symlink_metadata(&path) {
        Ok(file) => {
            if file.file_type().is_symlink() { return true; }
            else if file.file_type().is_dir() { return false; }
            else if file.file_type().is_file() { return false; }
        }
        Err(_) => { Status::Error.err(UTIL.name, "could not stat file".to_string()); }
    }
    return false;
}

fn walk_dir_copy<F: AsRef<Path>>
(from: &F, mut to: PathBuf, options: &Opts, copytype: &CopyType) -> io::Result<()> {
    check_endless_copy(&from, &to);
    if from.as_ref().is_dir() {
        if !to.exists() { create_dir(&from, &to, options); }
        for entry in try!(fs::read_dir(&from)) {
            let entry = match entry {
                Ok(s) => { s },
                Err(e) => {
                    Status::Error.err(UTIL.name, e.to_string());
                    panic!();
                }
            };
            let dest = to.clone().join(entry.path().file_name().unwrap());
            let src = entry.path();
            if src.is_file() && !options.inter {
                match_copytype(&src.as_string(), &dest, options, copytype);
            } else if src.is_file() && !dest.exists() && options.inter {
                match_copytype(&src.as_string(), &dest, options, copytype);
            } else if src.is_file() && dest.exists() && options.inter {
                if ask(&dest) {
                    match_copytype(&src.as_string(), &dest, options, copytype);
                }
            } else if src.is_dir() {
                to.push(src.rel_to(&from));
                match walk_dir_copy(&src.clone(), to.clone(), options, &copytype) {
                    Ok(s) => { s },
                    Err(e) => {
                        Status::Error.err(UTIL.name, e.to_string());
                        panic!();
                    }
                };
                to.pop();
            }
        }
    }
    return Ok(());
}

fn print_usage(opts: Options) {
    print!("{0}: {1} {2}... {3} {4}\
    \n  or:  {1} {2}... {3}... {4}\n\
    Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.", "Usage".bold(),
    UTIL.name.bold(), "[OPTION]".underline(), "SOURCE".underline(),
    "DEST".underline());
    println!("{}", opts.options());
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut opts = Options::new();

    opts.optflag("f", "force", "Force action");
    opts.optflag("i", "interactive", "Prompt before overwriting an existing file");
    opts.optflag("l", "link", "Create hard links instead of copying");
    opts.optflag("L", "dereference", "Always follow symbolic links in SOURCE");
    opts.optflag("P", "no-dereference", "Never follow symbolic links in SOURCE");
    opts.optflag("r", "recursive", "Recursively copy a directory");
    opts.optflag("s", "symlink", "Create symbolic links instead of copying");
    opts.optflag("v", "verbose", "Print operation verbosely");
    opts.optflag("h", "help", "Print help information");
    opts.optflag("", "version", "Print version information");

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => { m },
        Err(e) => {
            Status::OptError.err(UTIL.name, e.to_string());
            Status::OptError.exit();
            panic!();
        }
    };

    if matches.opt_present("h") {
        print_usage(opts);
    } else if matches.opt_present("version") {
        UTIL.copyright("Copyright (C) 2015 core-utils developers\n\
                       License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n\
                       This is free software: you are free to change and redistribute it.\n\
                       There is NO WARRANTY, to the extent permitted by law.\n\n",
                       vec!["Alberto Corona"]);
    } else {
        let mut copytype = CopyType::Norm;
        let mut mode = EntryMode::Norm;
        if matches.opt_present("l") {
            copytype = CopyType::Phys;
        } else if matches.opt_present("s") {
            copytype = CopyType::Sym;
        }
        if matches.opt_present("L") {
            mode = EntryMode::Sym;
        } else if matches.opt_present("P") {
            mode = EntryMode::Norm;
        }
        let options = &Opts {
            verbose: matches.opt_present("v"),
            inter: matches.opt_present("i"),
            force: matches.opt_present("f"),
        };
        let src = match matches.free.first().clone() {
            Some(s) => { s },
            None => {
                println!("{}: no arguments given", UTIL.name);
                Status::OptError.exit();
                panic!();
            }
        };
        if matches.free.len() == 1 {
            println!("{}: no destination for {}", UTIL.name, src);
            Status::ArgError.exit();
        }
        if matches.opt_present("r") {
            for items in matches.free.split_first().unwrap().1.to_vec() {
                copy_dir(src, &mut PathBuf::from(items), options, &copytype, &mode);
            }
        } else {
            let dest = matches.free.last().unwrap();
            if matches.free.len() > 2 {
                copy_files(matches.free.split_last().unwrap().1.to_vec(), &dest, options, &copytype);
            } else if !options.inter {
                copy(src, &matches.free[1], options);
            } else if options.inter && ask(&matches.free[1]) {
                copy(src, &matches.free[1], options);
            }
        }
    }
}