diff --git a/Cargo.lock b/Cargo.lock index 931098e4d..3b17b9250 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ dependencies = [ "basename 0.0.1", "cat 0.0.1", "chmod 0.0.1", + "chown 0.0.1", "chroot 0.0.1", "cksum 0.0.1", "comm 0.0.1", @@ -31,6 +32,7 @@ dependencies = [ "hostid 0.0.1", "hostname 0.0.1", "id 0.0.1", + "install 0.0.1", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kill 0.0.1", "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -177,6 +179,17 @@ dependencies = [ "walker 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "chown" +version = "0.0.1" +dependencies = [ + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "uucore 0.0.1", + "walkdir 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "chroot" version = "0.0.1" @@ -408,6 +421,15 @@ dependencies = [ "uucore 0.0.1", ] +[[package]] +name = "install" +version = "0.0.1" +dependencies = [ + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "uucore 0.0.1", +] + [[package]] name = "itertools" version = "0.4.15" @@ -1204,6 +1226,15 @@ name = "vec_map" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "walkdir" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "walker" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5aea9e268..20c086236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ unix = [ "hostid", "hostname", "id", + "install", "kill", "logname", "mkfifo", @@ -126,6 +127,7 @@ head = { optional=true, path="src/head" } hostid = { optional=true, path="src/hostid" } hostname = { optional=true, path="src/hostname" } id = { optional=true, path="src/id" } +install = { optional=true, path="src/install" } kill = { optional=true, path="src/kill" } link = { optional=true, path="src/link" } ln = { optional=true, path="src/ln" } diff --git a/Makefile b/Makefile index f43ddbb9d..556b56e0a 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,7 @@ UNIX_PROGS := \ hostid \ hostname \ id \ + install \ kill \ logname \ mkfifo \ @@ -152,6 +153,7 @@ TEST_PROGS := \ fold \ hashsum \ head \ + install \ link \ ln \ ls \ diff --git a/README.md b/README.md index 43b885be5..f4d0801b5 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,6 @@ To do - df - expr (almost done, no regular expressions) - getlimits -- install - join - ls - mv (almost done, one more option) diff --git a/src/install/Cargo.toml b/src/install/Cargo.toml new file mode 100644 index 000000000..d2059c88e --- /dev/null +++ b/src/install/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "install" +version = "0.0.1" +authors = ["Ben Eills "] + +[lib] +name = "uu_install" +path = "install.rs" + +[dependencies] +getopts = "*" +libc = ">= 0.2" +uucore = { path="../uucore" } + +[dev-dependencies] +time = "*" + +[[bin]] +name = "install" +path = "main.rs" diff --git a/src/install/install.rs b/src/install/install.rs new file mode 100644 index 000000000..7aa97ae3d --- /dev/null +++ b/src/install/install.rs @@ -0,0 +1,427 @@ +#![crate_name = "uu_install"] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Ben Eills + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +extern crate getopts; +extern crate libc; + +mod mode; + +#[macro_use] +extern crate uucore; + +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::result::Result; + +static NAME: &'static str = "install"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +const DEFAULT_MODE: libc::mode_t = 755; + +pub struct Behaviour { + main_function: MainFunction, + specified_mode: Option, + suffix: String, + verbose: bool +} + +#[derive(Clone, Eq, PartialEq)] +pub enum MainFunction { + /// Display version information. + Version, + /// Display help, including command line arguments. + Help, + /// Create directories + Directory, + /// Install files to locations (primary functionality) + Standard +} + +impl Behaviour { + /// Determine the mode for chmod after copy. + pub fn mode(&self) -> libc::mode_t { + match self.specified_mode { + Some(x) => x, + None => DEFAULT_MODE + } + } +} + +/// Main install utility function, called from main.rs. +/// +/// Returns a program return code. +/// +pub fn uumain(args: Vec) -> i32 { + let opts = opts(); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + show_error!("Invalid options\n{}", f); + return 1; + } + }; + + let usage = opts.usage("Copy SOURCE to DEST or multiple SOURCE(s) to the existing\n \ + DIRECTORY, while setting permission modes and owner/group"); + + if let Err(s) = check_unimplemented(&matches) { + show_error!("Unimplemented feature: {}", s); + return 2; + } + + let behaviour = match behaviour(&matches) { + Ok(x) => x, + Err(ret) => { + return ret; + } + }; + + let paths: Vec = { + fn string_to_path<'a>(s: &'a String) -> &'a Path { + Path::new(s) + }; + let to_owned = |p: &Path| p.to_owned(); + let arguments = matches.free.iter().map(string_to_path); + + arguments.map(to_owned).collect() + }; + + match behaviour.main_function { + MainFunction::Version => { + println!("{} {}", NAME, VERSION); + 0 + }, + MainFunction::Help => { + help(&usage); + 0 + }, + MainFunction::Directory => { + directory(&paths[..], behaviour) + }, + MainFunction::Standard => { + standard(&paths[..], behaviour) + } + } +} + +/// Build a specification of the comamnd line. +/// +/// Returns a getopts::Options struct. +/// +fn opts() -> getopts::Options { + let mut opts = getopts::Options::new(); + + // TODO implement flag + opts.optflagopt("", "backup", "(unimplemented) make a backup of each existing destination\n \ + file", "CONTROL"); + + // TODO implement flag + opts.optflag("b", "", "(unimplemented) like --backup but does not accept an argument"); + + // TODO implement flag + opts.optflag("C", "compare", "(unimplemented) compare each pair of source and destination\n \ + files, and in some cases, do not modify the destination at all"); + + opts.optflag("d", "directory", "treat all arguments as directory names;\n \ + create all components of the specified directories"); + + + // TODO implement flag + opts.optflag("D", "", "(unimplemented) create all leading components of DEST except the\n \ + last, then copy SOURCE to DEST"); + + // TODO implement flag + opts.optflagopt("g", "group", "(unimplemented) set group ownership, instead of process'\n \ + current group", "GROUP"); + + opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead\n \ + of rwxr-xr-x", "MODE"); + + // TODO implement flag + opts.optflagopt("o", "owner", "(unimplemented) set ownership (super-user only)", + "OWNER"); + + // TODO implement flag + opts.optflag("p", "preserve-timestamps", "(unimplemented) apply access/modification times\n \ + of SOURCE files to corresponding destination files"); + + // TODO implement flag + opts.optflag("s", "strip", "(unimplemented) strip symbol tables"); + + // TODO implement flag + opts.optflagopt("", "strip-program", "(unimplemented) program used to strip binaries", + "PROGRAM"); + + // TODO implement flag + opts.optopt("S", "suffix", "(unimplemented) override the usual backup suffix", "SUFFIX"); + + // TODO implement flag + opts.optopt("t", "target-directory", "(unimplemented) move all SOURCE arguments into\n \ + DIRECTORY", "DIRECTORY"); + + // TODO implement flag + opts.optflag("T", "no-target-directory", "(unimplemented) treat DEST as a normal file"); + + // TODO implement flag + opts.optflag("v", "verbose", "(unimplemented) explain what is being done"); + + // TODO implement flag + opts.optflag("P", "preserve-context", "(unimplemented) preserve security context"); + + // TODO implement flag + opts.optflagopt("Z", "context", "(unimplemented) set security context of files and\n \ + directories", "CONTEXT"); + + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("V", "version", "output version information and exit"); + + opts +} + +/// Check for unimplemented command line arguments. +/// +/// Either return the degenerate Ok value, or an Err with string. +/// +/// # Errors +/// +/// Error datum is a string of the unimplemented argument. +/// +fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> { + if matches.opt_present("backup") { + Err("--backup") + } else if matches.opt_present("b") { + Err("-b") + } else if matches.opt_present("compare") { + Err("--compare, -C") + } else if matches.opt_present("D") { + Err("-D") + } else if matches.opt_present("group") { + Err("--group, -g") + } else if matches.opt_present("owner") { + Err("--owner, -o") + } else if matches.opt_present("preserve-timestamps") { + Err("--preserve-timestamps, -p") + } else if matches.opt_present("strip") { + Err("--strip, -s") + } else if matches.opt_present("strip-program") { + Err("--strip-program") + } else if matches.opt_present("suffix") { + Err("--suffix, -S") + } else if matches.opt_present("target-directory") { + Err("--target-directory, -t") + } else if matches.opt_present("no-target-directory") { + Err("--no-target-directory, -T") + } else if matches.opt_present("verbose") { + Err("--verbose, -v") + } else if matches.opt_present("preserve-context") { + Err("--preserve-context, -P") + } else if matches.opt_present("context") { + Err("--context, -Z") + } else { + Ok(()) + } +} + +/// Determine behaviour, given command line arguments. +/// +/// If successful, returns a filled-out Behaviour struct. +/// +/// # Errors +/// +/// In event of failure, returns an integer intended as a program return code. +/// +fn behaviour(matches: &getopts::Matches) -> Result { + let main_function = if matches.opt_present("version") { + MainFunction::Version + } else if matches.opt_present("help") { + MainFunction::Help + } else if matches.opt_present("directory") { + MainFunction::Directory + } else { + MainFunction::Standard + }; + + let considering_dir: bool = MainFunction::Directory == main_function; + + let specified_mode: Option = if matches.opt_present("mode") { + match matches.opt_str("mode") { + Some(x) => { + match mode::parse(&x[..], considering_dir) { + Ok(y) => Some(y), + Err(err) => { + show_error!("Invalid mode string: {}", err); + return Err(1); + } + } + }, + None => { + show_error!("option '--mode' requires an argument\n \ + Try '{} --help' for more information.", NAME); + return Err(1); + } + } + } else { + None + }; + + let backup_suffix = if matches.opt_present("suffix") { + match matches.opt_str("suffix") { + Some(x) => x, + None => { + show_error!("option '--suffix' requires an argument\n\ + Try '{} --help' for more information.", NAME); + return Err(1); + } + } + } else { + "~".to_owned() + }; + + Ok(Behaviour { + main_function: main_function, + specified_mode: specified_mode, + suffix: backup_suffix, + verbose: matches.opt_present("v"), + }) +} + +/// Print utility help to stdout. +/// +fn help(usage: &str) { + println!("{0} {1}\n\n\ + Usage: {0} SOURCE DEST\n \ + or: {0} SOURCE... DIRECTORY\n\n\ + {2}", NAME, VERSION, usage); +} + +/// Creates directories. +/// +/// GNU man pages describe this functionality as creating 'all components of +/// the specified directories'. +/// +/// Returns an integer intended as a program return code. +/// +fn directory(paths: &[PathBuf], b: Behaviour) -> i32 { + if paths.len() < 1 { + println!("{} with -d requires at least one argument.", NAME); + 1 + } else { + let mut all_successful = true; + + for directory in paths.iter() { + let path = directory.as_path(); + + if path.exists() { + show_info!("cannot create directory '{}': File exists", path.display()); + all_successful = false; + } + + if let Err(e) = fs::create_dir(directory) { + show_info!("{}: {}", path.display(), e.to_string()); + all_successful = false; + } + + if mode::chmod(&path, b.mode()).is_err() { + all_successful = false; + } + + if b.verbose { + show_info!("created directory '{}'", path.display()); + } + } + if all_successful { 0 } else { 1 } + } +} + +/// Perform an install, given a list of paths and behaviour. +/// +/// Returns an integer intended as a program return code. +/// +fn standard(paths: &[PathBuf], b: Behaviour) -> i32 { + if paths.len() < 2 { + println!("{} requires at least 2 arguments.", NAME); + 1 + } else { + let sources = &paths[0..paths.len() - 1]; + let target_directory = &paths[paths.len() - 1]; + + copy_files_into_dir(sources, target_directory, &b) + } +} + +/// Copy some files into a directory. +/// +/// Prints verbose information and error messages. +/// Returns an integer intended as a program return code. +/// +/// # Parameters +/// +/// _files_ must all exist as non-directories. +/// _target_dir_ must be a directory. +/// +fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behaviour) -> i32 { + if !target_dir.is_dir() { + show_error!("target ‘{}’ is not a directory", target_dir.display()); + return 1; + } + + let mut all_successful = true; + for sourcepath in files.iter() { + let targetpath = match sourcepath.as_os_str().to_str() { + Some(name) => target_dir.join(name), + None => { + show_error!("cannot stat ‘{}’: No such file or directory", + sourcepath.display()); + + all_successful = false; + continue; + } + }; + + if copy(sourcepath, &targetpath, b).is_err() { + all_successful = false; + } + }; + if all_successful { 0 } else { 1 } +} + +/// Copy one file to a new location, changing metadata. +/// +/// # Parameters +/// +/// _from_ must exist as a non-directory. +/// _to_ must be a non-existent file, whose parent directory exists. +/// +/// # Errors +/// +/// If the copy system call fails, we print a verbose error and return an empty error value. +/// +fn copy(from: &PathBuf, to: &PathBuf, b: &Behaviour) -> Result<(), ()> { + let io_result = fs::copy(from, to); + + if let Err(err) = io_result { + show_error!("install: cannot install ‘{}’ to ‘{}’: {}", + from.display(), to.display(), err); + return Err(()); + } + + if mode::chmod(&to, b.mode()).is_err() { + return Err(()); + } + + if b.verbose { + print!("‘{}’ -> ‘{}’", from.display(), to.display()); + } + + Ok(()) +} diff --git a/src/install/main.rs b/src/install/main.rs new file mode 100644 index 000000000..fa1e578d8 --- /dev/null +++ b/src/install/main.rs @@ -0,0 +1,5 @@ +extern crate uu_install; + +fn main() { + std::process::exit(uu_install::uumain(std::env::args().collect())); +} diff --git a/src/install/mode.rs b/src/install/mode.rs new file mode 100644 index 000000000..57040cd4d --- /dev/null +++ b/src/install/mode.rs @@ -0,0 +1,166 @@ +extern crate libc; + +use std::io::Write; +use std::path::{Path, PathBuf}; + +/// Takes a user-supplied string and tries to parse to u16 mode bitmask. +pub fn parse(mode_string: &str, considering_dir: bool) -> Result { + let numbers: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + + // Passing 000 as the existing permissions seems to mirror GNU behaviour. + if mode_string.contains(numbers) { + chmod_rs::parse_numeric(0, mode_string) + } else { + chmod_rs::parse_symbolic(0, mode_string, considering_dir) + } +} + +/// chmod a file or directory on UNIX. +/// +/// Adapted from mkdir.rs. Handles own error printing. +/// +#[cfg(unix)] +pub fn chmod(path: &Path, mode: libc::mode_t) -> Result<(), ()> { + use std::ffi::CString; + use std::io::Error; + + let file = CString::new(path.as_os_str().to_str().unwrap()). + unwrap_or_else(|e| crash!(1, "{}", e)); + let mode = mode as libc::mode_t; + + if unsafe { libc::chmod(file.as_ptr(), mode) } != 0 { + show_info!("{}: chmod failed with errno {}", path.display(), + Error::last_os_error().raw_os_error().unwrap()); + return Err(()); + } + Ok(()) +} + +/// chmod a file or directory on Windows. +/// +/// Adapted from mkdir.rs. +/// +#[cfg(windows)] +pub fn chmod(path: &Path, mode: libc::mode_t) -> Result<(), ()> { + // chmod on Windows only sets the readonly flag, which isn't even honored on directories + Ok(()) +} + +/// Parsing functions taken from chmod.rs. +/// +/// We keep these in a dedicated module to minimize debt of duplicated code. +/// +mod chmod_rs { + extern crate libc; + + pub fn parse_numeric(fperm: libc::mode_t, mut mode: &str) -> Result { + let (op, pos) = try!(parse_op(mode, Some('='))); + mode = mode[pos..].trim_left_matches('0'); + if mode.len() > 4 { + Err(format!("mode is too large ({} > 7777)", mode)) + } else { + match libc::mode_t::from_str_radix(mode, 8) { + Ok(change) => { + Ok(match op { + '+' => fperm | change, + '-' => fperm & !change, + '=' => change, + _ => unreachable!() + }) + } + Err(err) => Err(String::from("numeric parsing error")) + } + } + } + + pub fn parse_symbolic(mut fperm: libc::mode_t, mut mode: &str, considering_dir: bool) -> Result { + let (mask, pos) = parse_levels(mode); + if pos == mode.len() { + return Err(format!("invalid mode ({})", mode)); + } + let respect_umask = pos == 0; + let last_umask = unsafe { + libc::umask(0) + }; + mode = &mode[pos..]; + while mode.len() > 0 { + let (op, pos) = try!(parse_op(mode, None)); + mode = &mode[pos..]; + let (mut srwx, pos) = parse_change(mode, fperm, considering_dir); + if respect_umask { + srwx &= !last_umask; + } + mode = &mode[pos..]; + match op { + '+' => fperm |= srwx & mask, + '-' => fperm &= !(srwx & mask), + '=' => fperm = (fperm & !mask) | (srwx & mask), + _ => unreachable!() + } + } + unsafe { + libc::umask(last_umask); + } + Ok(fperm) + } + + fn parse_levels(mode: &str) -> (libc::mode_t, usize) { + let mut mask = 0; + let mut pos = 0; + for ch in mode.chars() { + mask |= match ch { + 'u' => 0o7700, + 'g' => 0o7070, + 'o' => 0o7007, + 'a' => 0o7777, + _ => break + }; + pos += 1; + } + if pos == 0 { + mask = 0o7777; // default to 'a' + } + (mask, pos) + } + + fn parse_op(mode: &str, default: Option) -> Result<(char, usize), String> { + match mode.chars().next() { + Some(ch) => match ch { + '+' | '-' | '=' => Ok((ch, 1)), + _ => match default { + Some(ch) => Ok((ch, 0)), + None => Err(format!("invalid operator (expected +, -, or =, but found {})", ch)) + } + }, + None => Err("unexpected end of mode".to_owned()) + } + } + + fn parse_change(mode: &str, fperm: libc::mode_t, considering_dir: bool) -> (libc::mode_t, usize) { + let mut srwx = fperm & 0o7000; + let mut pos = 0; + for ch in mode.chars() { + match ch { + 'r' => srwx |= 0o444, + 'w' => srwx |= 0o222, + 'x' => srwx |= 0o111, + 'X' => { + if considering_dir || (fperm & 0o0111) != 0 { + srwx |= 0o111 + } + } + 's' => srwx |= 0o4000 | 0o2000, + 't' => srwx |= 0o1000, + 'u' => srwx = (fperm & 0o700) | ((fperm >> 3) & 0o070) | ((fperm >> 6) & 0o007), + 'g' => srwx = ((fperm << 3) & 0o700) | (fperm & 0o070) | ((fperm >> 3) & 0o007), + 'o' => srwx = ((fperm << 6) & 0o700) | ((fperm << 3) & 0o070) | (fperm & 0o007), + _ => break + }; + pos += 1; + } + if pos == 0 { + srwx = 0; + } + (srwx, pos) + } +} diff --git a/tests/test_install.rs b/tests/test_install.rs new file mode 100644 index 000000000..b223d6339 --- /dev/null +++ b/tests/test_install.rs @@ -0,0 +1,170 @@ +extern crate libc; +extern crate time; +extern crate kernel32; +extern crate winapi; +extern crate filetime; + +use self::filetime::*; +use common::util::*; +use std::os::unix::fs::PermissionsExt; + +static UTIL_NAME: &'static str = "install"; + +#[test] +fn test_install_help() { + let (at, mut ucmd) = testing(UTIL_NAME); + + let result = ucmd.arg("--help").run(); + + assert!(result.success); + assert_empty_stderr!(result); + + assert!(result.stdout.contains("Usage:")); +} + +#[test] +fn test_install_basic() { + let (at, mut ucmd) = testing(UTIL_NAME); + let dir = "test_install_target_dir_dir_a"; + let file1 = "test_install_target_dir_file_a1"; + let file2 = "test_install_target_dir_file_a2"; + + at.touch(file1); + at.touch(file2); + at.mkdir(dir); + let result = ucmd.arg(file1).arg(file2).arg(dir).run(); + + assert!(result.success); + assert_empty_stderr!(result); + + assert!(at.file_exists(file1)); + assert!(at.file_exists(file2)); + assert!(at.file_exists(&format!("{}/{}", dir, file1))); + assert!(at.file_exists(&format!("{}/{}", dir, file2))); +} + +#[test] +fn test_install_unimplemented_arg() { + let (at, mut ucmd) = testing(UTIL_NAME); + let dir = "test_install_target_dir_dir_b"; + let file = "test_install_target_dir_file_b"; + let context_arg = "--context"; + + at.touch(file); + at.mkdir(dir); + let result = ucmd.arg(context_arg).arg(file).arg(dir).run(); + + assert!(!result.success); + assert!(result.stderr.contains("Unimplemented")); + + assert!(!at.file_exists(&format!("{}/{}", dir, file))); +} + +#[test] +fn test_install_component_directories() { + let (at, mut ucmd) = testing(UTIL_NAME); + let component1 = "test_install_target_dir_component_c1"; + let component2 = "test_install_target_dir_component_c2"; + let component3 = "test_install_target_dir_component_c3"; + let directories_arg = "-d"; + + let result = ucmd.arg(directories_arg).arg(component1).arg(component2).arg(component3).run(); + + assert!(result.success); + assert_empty_stderr!(result); + + assert!(at.dir_exists(component1)); + assert!(at.dir_exists(component2)); + assert!(at.dir_exists(component3)); +} + +#[test] +fn test_install_component_directories_failing() { + let (at, mut ucmd) = testing(UTIL_NAME); + let component = "test_install_target_dir_component_d1"; + let directories_arg = "-d"; + + at.mkdir(component); + let result = ucmd.arg(directories_arg).arg(component).run(); + + assert!(!result.success); + assert!(result.stderr.contains("File exists")); +} + +#[test] +fn test_install_mode_numeric() { + let (at, mut ucmd) = testing(UTIL_NAME); + let dir = "test_install_target_dir_dir_e"; + let file = "test_install_target_dir_file_e"; + let mode_arg = "--mode=333"; + + at.touch(file); + at.mkdir(dir); + let result = ucmd.arg(file).arg(dir).arg(mode_arg).run(); + + assert!(result.success); + assert_empty_stderr!(result); + + let dest_file = &format!("{}/{}", dir, file); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + assert_eq!(0o333 as u32, PermissionsExt::mode(&permissions)); +} + +#[test] +fn test_install_mode_symbolic() { + let (at, mut ucmd) = testing(UTIL_NAME); + let dir = "test_install_target_dir_dir_f"; + let file = "test_install_target_dir_file_f"; + let mode_arg = "--mode=o+wx"; + + at.touch(file); + at.mkdir(dir); + let result = ucmd.arg(file).arg(dir).arg(mode_arg).run(); + + assert!(result.success); + assert_empty_stderr!(result); + + let dest_file = &format!("{}/{}", dir, file); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + assert_eq!(0o003 as u32, PermissionsExt::mode(&permissions)); +} + +#[test] +fn test_install_mode_failing() { + let (at, mut ucmd) = testing(UTIL_NAME); + let dir = "test_install_target_dir_dir_g"; + let file = "test_install_target_dir_file_g"; + let mode_arg = "--mode=999"; + + at.touch(file); + at.mkdir(dir); + let result = ucmd.arg(file).arg(dir).arg(mode_arg).run(); + + assert!(!result.success); + assert!(result.stderr.contains("Invalid mode string: numeric parsing error")); + + let dest_file = &format!("{}/{}", dir, file); + assert!(at.file_exists(file)); + assert!(!at.file_exists(dest_file)); +} + +#[test] +fn test_install_mode_directories() { + let (at, mut ucmd) = testing(UTIL_NAME); + let component = "test_install_target_dir_component_h"; + let directories_arg = "-d"; + let mode_arg = "--mode=333"; + + let result = ucmd.arg(directories_arg).arg(component).arg(mode_arg).run(); + + assert!(result.success); + assert_empty_stderr!(result); + + assert!(at.dir_exists(component)); + let permissions = at.metadata(component).permissions(); + assert_eq!(0o333 as u32, PermissionsExt::mode(&permissions)); +} diff --git a/tests/tests.rs b/tests/tests.rs index d867a934f..4a88f2d91 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -27,6 +27,7 @@ macro_rules! unix_only { unix_only! { "chmod", test_chmod; "chown", test_chown; + "install", test_install; "mv", test_mv; "pathchk", test_pathchk; "stdbuf", test_stdbuf;