From e72ec4a5bba6f680630da8828aa6ff8b9212cab8 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Tue, 12 Jul 2016 20:56:21 +0200 Subject: [PATCH 1/9] Implement skeleton install utility Add install utility skeleton source, based on mv, including the getopts setup mirroring GNU's `man install` documentation. Also add a single test and build system code. --- Cargo.lock | 31 ++++++++++++++ Cargo.toml | 2 + Makefile | 2 + src/install/Cargo.toml | 20 +++++++++ src/install/install.rs | 92 ++++++++++++++++++++++++++++++++++++++++++ src/install/main.rs | 5 +++ tests/test_install.rs | 21 ++++++++++ tests/tests.rs | 1 + 8 files changed, 174 insertions(+) create mode 100644 src/install/Cargo.toml create mode 100644 src/install/install.rs create mode 100644 src/install/main.rs create mode 100644 tests/test_install.rs diff --git a/Cargo.lock b/Cargo.lock index 931098e4d..4c0640ad4 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.1.12 (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..77fa9222a 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/src/install/Cargo.toml b/src/install/Cargo.toml new file mode 100644 index 000000000..87fe3d1f4 --- /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 = "*" +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..db1724178 --- /dev/null +++ b/src/install/install.rs @@ -0,0 +1,92 @@ +#![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; + +#[macro_use] +extern crate uucore; + +use std::io::{Write}; +use std::path::{Path, PathBuf}; + +static NAME: &'static str = "install"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +pub fn uumain(args: Vec) -> i32 { + let mut opts = getopts::Options::new(); + + opts.optflagopt("", "backup", "make a backup of each existing destination file", "CONTROL"); + opts.optflag("b", "", "like --backup but does not accept an argument"); + opts.optflag("C", "compare", "compare each pair of source and destination files, and in\n \ + some cases, do not modify the destination at all"); + opts.optflag("d", "directory", "treat all arguments as directory names; create all\n \ + components of the specified directories"); + + opts.optflag("D", "", "create all leading components of DEST except the last, then copy\n \ + SOURCE to DEST"); + opts.optflagopt("g", "group", "set group ownership, instead of process' current group", + "GROUP"); + opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead of rwxr-xr-x", + "MODE"); + opts.optflagopt("o", "owner", "set ownership (super-user only)", + "OWNER"); + opts.optflag("p", "preserve-timestamps", "apply access/modification times of SOURCE files\n \ + to corresponding destination files"); + opts.optflag("s", "strip", "strip symbol tables"); + opts.optflagopt("", "strip-program", "program used to strip binaries", "PROGRAM"); + opts.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"); + opts.optopt("t", "target-directory", "move all SOURCE arguments into DIRECTORY", "DIRECTORY"); + opts.optflag("T", "no-target-directory", "treat DEST as a normal file"); + opts.optflag("v", "verbose", "explain what is being done"); + opts.optflag("P", "preserve-context", "preserve security context"); + opts.optflagopt("Z", "context", "set security context of files and directories", "CONTEXT"); + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("V", "version", "output version information and exit"); + + 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"); + + 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() + }; + + if matches.opt_present("version") { + println!("{} {}", NAME, VERSION); + 0 + } else if matches.opt_present("help") { + help(&usage); + 0 + } else { + println!("Not implemented."); + 1 + } +} + +fn help(usage: &str) { + println!("{0} {1}\n\n\ + Usage: {0} SOURCE DEST\n \ + or: {0} SOURCE... DIRECTORY\n\n\ + {2}", NAME, VERSION, usage); +} 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/tests/test_install.rs b/tests/test_install.rs new file mode 100644 index 000000000..e7fa061fc --- /dev/null +++ b/tests/test_install.rs @@ -0,0 +1,21 @@ +extern crate libc; +extern crate time; +extern crate kernel32; +extern crate winapi; +extern crate filetime; + +use self::filetime::*; +use common::util::*; + +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_empty_stderr!(result); + assert!(result.success); + +// assert!(result.stdout.contains("Usage:")); +} 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; From 8c1f11bfda9c8978af619ce4d38d018e0772601d Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Tue, 12 Jul 2016 20:59:59 +0200 Subject: [PATCH 2/9] Remove install utility from README list --- README.md | 1 - 1 file changed, 1 deletion(-) 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) From 8a5719561d70273fde30beb780f2a23a6c832c70 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Tue, 12 Jul 2016 22:58:44 +0200 Subject: [PATCH 3/9] Implement basic installation of file to directory Bare minimum functionality of `install file dir` implemented. Also added TODO markers in code for outstanding parameters and split main function into smaller logical chunks. --- src/install/install.rs | 223 ++++++++++++++++++++++++++++++++++------- tests/test_install.rs | 17 ++++ 2 files changed, 202 insertions(+), 38 deletions(-) diff --git a/src/install/install.rs b/src/install/install.rs index db1724178..fdefac429 100644 --- a/src/install/install.rs +++ b/src/install/install.rs @@ -15,42 +15,29 @@ extern crate libc; #[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"); +pub struct Behaviour { + main_function: MainFunction, + suffix: String, + verbose: bool, +} + +#[derive(Clone, Eq, PartialEq)] +pub enum MainFunction { + Version, + Help, + Standard +} + pub fn uumain(args: Vec) -> i32 { - let mut opts = getopts::Options::new(); - - opts.optflagopt("", "backup", "make a backup of each existing destination file", "CONTROL"); - opts.optflag("b", "", "like --backup but does not accept an argument"); - opts.optflag("C", "compare", "compare each pair of source and destination files, and in\n \ - some cases, do not modify the destination at all"); - opts.optflag("d", "directory", "treat all arguments as directory names; create all\n \ - components of the specified directories"); - - opts.optflag("D", "", "create all leading components of DEST except the last, then copy\n \ - SOURCE to DEST"); - opts.optflagopt("g", "group", "set group ownership, instead of process' current group", - "GROUP"); - opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead of rwxr-xr-x", - "MODE"); - opts.optflagopt("o", "owner", "set ownership (super-user only)", - "OWNER"); - opts.optflag("p", "preserve-timestamps", "apply access/modification times of SOURCE files\n \ - to corresponding destination files"); - opts.optflag("s", "strip", "strip symbol tables"); - opts.optflagopt("", "strip-program", "program used to strip binaries", "PROGRAM"); - opts.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"); - opts.optopt("t", "target-directory", "move all SOURCE arguments into DIRECTORY", "DIRECTORY"); - opts.optflag("T", "no-target-directory", "treat DEST as a normal file"); - opts.optflag("v", "verbose", "explain what is being done"); - opts.optflag("P", "preserve-context", "preserve security context"); - opts.optflagopt("Z", "context", "set security context of files and directories", "CONTEXT"); - opts.optflag("h", "help", "display this help and exit"); - opts.optflag("V", "version", "output version information and exit"); + let opts = opts(); let matches = match opts.parse(&args[1..]) { Ok(m) => m, @@ -59,9 +46,17 @@ pub fn uumain(args: Vec) -> i32 { 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"); + 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) @@ -72,21 +67,173 @@ pub fn uumain(args: Vec) -> i32 { arguments.map(to_owned).collect() }; - if matches.opt_present("version") { - println!("{} {}", NAME, VERSION); - 0 - } else if matches.opt_present("help") { - help(&usage); - 0 - } else { - println!("Not implemented."); - 1 + match behaviour.main_function { + MainFunction::Version => { + println!("{} {}", NAME, VERSION); + 0 + }, + MainFunction::Help => { + help(&usage); + 0 + }, + MainFunction::Standard => { + exec(&paths[..], behaviour) + } } } +fn opts() -> getopts::Options { + let mut opts = getopts::Options::new(); + + // TODO implement flag + opts.optflagopt("", "backup", "make a backup of each existing destination file", "CONTROL"); + + // TODO implement flag + opts.optflag("b", "", "like --backup but does not accept an argument"); + + // TODO implement flag + opts.optflag("C", "compare", "compare each pair of source and destination files, and in\n \ + some cases, do not modify the destination at all"); + + // TODO implement flag + opts.optflag("d", "directory", "treat all arguments as directory names; create all\n \ + components of the specified directories"); + + + // TODO implement flagd + opts.optflag("D", "", "create all leading components of DEST except the last, then copy\n \ + SOURCE to DEST"); + + // TODO implement flag + opts.optflagopt("g", "group", "set group ownership, instead of process' current group", + "GROUP"); + + // TODO implement flag + opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead of rwxr-xr-x", + "MODE"); + + // TODO implement flag + opts.optflagopt("o", "owner", "set ownership (super-user only)", + "OWNER"); + + // TODO implement flag + opts.optflag("p", "preserve-timestamps", "apply access/modification times of SOURCE files\n \ + to corresponding destination files"); + + // TODO implement flag + opts.optflag("s", "strip", "strip symbol tables"); + + // TODO implement flag + opts.optflagopt("", "strip-program", "program used to strip binaries", "PROGRAM"); + + // TODO implement flag + opts.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"); + + // TODO implement flag + opts.optopt("t", "target-directory", "move all SOURCE arguments into DIRECTORY", "DIRECTORY"); + + // TODO implement flag + opts.optflag("T", "no-target-directory", "treat DEST as a normal file"); + + // TODO implement flag + opts.optflag("v", "verbose", "explain what is being done"); + + // TODO implement flag + opts.optflag("P", "preserve-context", "preserve security context"); + + // TODO implement flag + opts.optflagopt("Z", "context", "set security context of files and directories", "CONTEXT"); + + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("V", "version", "output version information and exit"); + + opts +} + +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 { + MainFunction::Standard + }; + + 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, + suffix: backup_suffix, + verbose: matches.opt_present("v"), + }) +} + fn help(usage: &str) { println!("{0} {1}\n\n\ Usage: {0} SOURCE DEST\n \ or: {0} SOURCE... DIRECTORY\n\n\ {2}", NAME, VERSION, usage); } + +fn exec(files: &[PathBuf], b: Behaviour) -> i32 { + if 2 == files.len() { + move_files_into_dir(&files[0..1], &files[1], &b) + } else { + println!("Not implemented."); + 1 + } +} + +fn move_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 let Err(e) = rename(sourcepath, &targetpath, b) { + show_error!("mv: cannot move ‘{}’ to ‘{}’: {}", + sourcepath.display(), targetpath.display(), e); + all_successful = false; + } + }; + if all_successful { 0 } else { 1 } +} + +fn rename(from: &PathBuf, to: &PathBuf, b: &Behaviour) -> std::io::Result<()> { + let backup_path: Option = None; + + try!(fs::rename(from, to)); + + if b.verbose { + print!("‘{}’ -> ‘{}’", from.display(), to.display()); + match backup_path { + Some(path) => println!(" (backup: ‘{}’)", path.display()), + None => println!("") + } + } + Ok(()) +} diff --git a/tests/test_install.rs b/tests/test_install.rs index e7fa061fc..2105708a5 100644 --- a/tests/test_install.rs +++ b/tests/test_install.rs @@ -19,3 +19,20 @@ fn test_install_help() { // assert!(result.stdout.contains("Usage:")); } + +#[test] +fn test_install_basic() { + let (at, mut ucmd) = testing(UTIL_NAME); + let dir = "test_install_target_dir_dir"; + let file = "test_install_target_dir_file_a"; + + at.touch(file); + at.mkdir(dir); + let result = ucmd.arg(file).arg(dir).run(); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!at.file_exists(file)); + assert!(at.file_exists(&format!("{}/{}", dir, file))); +} From a5d97323db8d4f1c806264133ca876cc4ae113d8 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Wed, 13 Jul 2016 10:37:08 +0200 Subject: [PATCH 4/9] Test for unimplemented command line arguments We check if the user has given one of the (many) not yet implemented command line arguments. Upon catching this, we display the specific transgressor to stderr and exit with return code 2. This behaviour is tested in one new integration test. --- src/install/install.rs | 47 +++++++++++++++++++++++++++++++++++++++++- tests/test_install.rs | 21 +++++++++++++++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/install/install.rs b/src/install/install.rs index fdefac429..a52f82846 100644 --- a/src/install/install.rs +++ b/src/install/install.rs @@ -50,6 +50,11 @@ pub fn uumain(args: Vec) -> i32 { 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) => { @@ -100,7 +105,7 @@ fn opts() -> getopts::Options { components of the specified directories"); - // TODO implement flagd + // TODO implement flag opts.optflag("D", "", "create all leading components of DEST except the last, then copy\n \ SOURCE to DEST"); @@ -150,6 +155,46 @@ fn opts() -> getopts::Options { opts } +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("directory") { + Err("--directory, -d") + } else if matches.opt_present("D") { + Err("-D") + } else if matches.opt_present("group") { + Err("--group, -g") + } else if matches.opt_present("mode") { + Err("--mode, -m") + } 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(()) + } +} + fn behaviour(matches: &getopts::Matches) -> Result { let main_function = if matches.opt_present("version") { MainFunction::Version diff --git a/tests/test_install.rs b/tests/test_install.rs index 2105708a5..817f6d397 100644 --- a/tests/test_install.rs +++ b/tests/test_install.rs @@ -17,13 +17,13 @@ fn test_install_help() { assert_empty_stderr!(result); assert!(result.success); -// assert!(result.stdout.contains("Usage:")); + assert!(result.stdout.contains("Usage:")); } #[test] fn test_install_basic() { let (at, mut ucmd) = testing(UTIL_NAME); - let dir = "test_install_target_dir_dir"; + let dir = "test_install_target_dir_dir_a"; let file = "test_install_target_dir_file_a"; at.touch(file); @@ -36,3 +36,20 @@ fn test_install_basic() { assert!(!at.file_exists(file)); assert!(at.file_exists(&format!("{}/{}", dir, file))); } + +#[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))); +} From 823ffbd1dd7707c1f7feae9f1828e7b351eeb137 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Wed, 13 Jul 2016 12:27:11 +0200 Subject: [PATCH 5/9] Copy rather than move files Test this with one integration test. Also document functions and add an '(unimplemented)' marker to some arguments in help. --- src/install/install.rs | 137 +++++++++++++++++++++++++++++------------ tests/test_install.rs | 14 +++-- 2 files changed, 106 insertions(+), 45 deletions(-) diff --git a/src/install/install.rs b/src/install/install.rs index a52f82846..efb4f28bd 100644 --- a/src/install/install.rs +++ b/src/install/install.rs @@ -36,6 +36,10 @@ pub enum MainFunction { Standard } +/// Main install utility function, called from main.rs. +/// +/// Returns a program return code. +/// pub fn uumain(args: Vec) -> i32 { let opts = opts(); @@ -82,72 +86,80 @@ pub fn uumain(args: Vec) -> i32 { 0 }, MainFunction::Standard => { - exec(&paths[..], behaviour) + 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", "make a backup of each existing destination file", "CONTROL"); + opts.optflagopt("", "backup", "(unimplemented) make a backup of each existing destination\n \ + file", "CONTROL"); // TODO implement flag - opts.optflag("b", "", "like --backup but does not accept an argument"); + opts.optflag("b", "", "(unimplemented) like --backup but does not accept an argument"); // TODO implement flag - opts.optflag("C", "compare", "compare each pair of source and destination files, and in\n \ - some cases, do not modify the destination at all"); + 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"); // TODO implement flag - opts.optflag("d", "directory", "treat all arguments as directory names; create all\n \ - components of the specified directories"); + opts.optflag("d", "directory", "(unimplemented) treat all arguments as directory names;\n \ + create all components of the specified directories"); // TODO implement flag - opts.optflag("D", "", "create all leading components of DEST except the last, then copy\n \ - SOURCE to DEST"); + 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", "set group ownership, instead of process' current group", - "GROUP"); + opts.optflagopt("g", "group", "(unimplemented) set group ownership, instead of process'\n \ + current group", "GROUP"); // TODO implement flag - opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead of rwxr-xr-x", - "MODE"); + opts.optflagopt("m", "mode", "(unimplemented) set permission mode (as in chmod), instead\n \ + of rwxr-xr-x", "MODE"); // TODO implement flag - opts.optflagopt("o", "owner", "set ownership (super-user only)", + opts.optflagopt("o", "owner", "(unimplemented) set ownership (super-user only)", "OWNER"); // TODO implement flag - opts.optflag("p", "preserve-timestamps", "apply access/modification times of SOURCE files\n \ - to corresponding destination files"); + 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", "strip symbol tables"); + opts.optflag("s", "strip", "(unimplemented) strip symbol tables"); // TODO implement flag - opts.optflagopt("", "strip-program", "program used to strip binaries", "PROGRAM"); + opts.optflagopt("", "strip-program", "(unimplemented) program used to strip binaries", + "PROGRAM"); // TODO implement flag - opts.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"); + opts.optopt("S", "suffix", "(unimplemented) override the usual backup suffix", "SUFFIX"); // TODO implement flag - opts.optopt("t", "target-directory", "move all SOURCE arguments into DIRECTORY", "DIRECTORY"); + opts.optopt("t", "target-directory", "(unimplemented) move all SOURCE arguments into\n \ + DIRECTORY", "DIRECTORY"); // TODO implement flag - opts.optflag("T", "no-target-directory", "treat DEST as a normal file"); + opts.optflag("T", "no-target-directory", "(unimplemented) treat DEST as a normal file"); // TODO implement flag - opts.optflag("v", "verbose", "explain what is being done"); + opts.optflag("v", "verbose", "(unimplemented) explain what is being done"); // TODO implement flag - opts.optflag("P", "preserve-context", "preserve security context"); + opts.optflag("P", "preserve-context", "(unimplemented) preserve security context"); // TODO implement flag - opts.optflagopt("Z", "context", "set security context of files and directories", "CONTEXT"); + 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"); @@ -155,6 +167,14 @@ fn opts() -> getopts::Options { 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") @@ -195,6 +215,14 @@ fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> { } } +/// 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 @@ -224,6 +252,8 @@ fn behaviour(matches: &getopts::Matches) -> Result { }) } +/// Print utility help to stdout. +/// fn help(usage: &str) { println!("{0} {1}\n\n\ Usage: {0} SOURCE DEST\n \ @@ -231,16 +261,33 @@ fn help(usage: &str) { {2}", NAME, VERSION, usage); } -fn exec(files: &[PathBuf], b: Behaviour) -> i32 { - if 2 == files.len() { - move_files_into_dir(&files[0..1], &files[1], &b) - } else { - println!("Not implemented."); +/// 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) } } -fn move_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behaviour) -> i32 { +/// 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; @@ -259,26 +306,36 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behaviour) - } }; - if let Err(e) = rename(sourcepath, &targetpath, b) { - show_error!("mv: cannot move ‘{}’ to ‘{}’: {}", - sourcepath.display(), targetpath.display(), e); + if copy(sourcepath, &targetpath, b).is_err() { all_successful = false; } }; if all_successful { 0 } else { 1 } } -fn rename(from: &PathBuf, to: &PathBuf, b: &Behaviour) -> std::io::Result<()> { - let backup_path: Option = None; +/// Copy one file to a new location. +/// +/// # 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); - try!(fs::rename(from, to)); + if let Err(err) = io_result { + show_error!("install: cannot install ‘{}’ to ‘{}’: {}", + from.display(), to.display(), err); + return Err(()) + } if b.verbose { print!("‘{}’ -> ‘{}’", from.display(), to.display()); - match backup_path { - Some(path) => println!(" (backup: ‘{}’)", path.display()), - None => println!("") - } } + Ok(()) } diff --git a/tests/test_install.rs b/tests/test_install.rs index 817f6d397..0058f763a 100644 --- a/tests/test_install.rs +++ b/tests/test_install.rs @@ -24,17 +24,21 @@ fn test_install_help() { fn test_install_basic() { let (at, mut ucmd) = testing(UTIL_NAME); let dir = "test_install_target_dir_dir_a"; - let file = "test_install_target_dir_file_a"; + let file1 = "test_install_target_dir_file_a1"; + let file2 = "test_install_target_dir_file_a2"; - at.touch(file); + at.touch(file1); + at.touch(file2); at.mkdir(dir); - let result = ucmd.arg(file).arg(dir).run(); + let result = ucmd.arg(file1).arg(file2).arg(dir).run(); assert_empty_stderr!(result); assert!(result.success); - assert!(!at.file_exists(file)); - assert!(at.file_exists(&format!("{}/{}", dir, file))); + 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] From b15fff6269cd02e5e190c8336c2abdc69cbb0836 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Wed, 13 Jul 2016 12:53:22 +0200 Subject: [PATCH 6/9] Implement creation of component directories (-d option) Tested in two integration test: - One creating three component directories - One trying to create an already existing directory --- src/install/install.rs | 50 ++++++++++++++++++++++++++++++++++++++---- tests/test_install.rs | 36 ++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/install/install.rs b/src/install/install.rs index efb4f28bd..e5ea969fa 100644 --- a/src/install/install.rs +++ b/src/install/install.rs @@ -31,8 +31,13 @@ pub struct Behaviour { #[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 } @@ -85,6 +90,9 @@ pub fn uumain(args: Vec) -> i32 { help(&usage); 0 }, + MainFunction::Directory => { + directory(&paths[..], behaviour) + }, MainFunction::Standard => { standard(&paths[..], behaviour) } @@ -109,8 +117,7 @@ fn opts() -> getopts::Options { 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"); - // TODO implement flag - opts.optflag("d", "directory", "(unimplemented) treat all arguments as directory names;\n \ + opts.optflag("d", "directory", "treat all arguments as directory names;\n \ create all components of the specified directories"); @@ -182,8 +189,6 @@ fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> { Err("-b") } else if matches.opt_present("compare") { Err("--compare, -C") - } else if matches.opt_present("directory") { - Err("--directory, -d") } else if matches.opt_present("D") { Err("-D") } else if matches.opt_present("group") { @@ -228,6 +233,8 @@ fn behaviour(matches: &getopts::Matches) -> Result { MainFunction::Version } else if matches.opt_present("help") { MainFunction::Help + } else if matches.opt_present("directory") { + MainFunction::Directory } else { MainFunction::Standard }; @@ -261,6 +268,41 @@ fn help(usage: &str) { {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 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. diff --git a/tests/test_install.rs b/tests/test_install.rs index 0058f763a..f47b6a37d 100644 --- a/tests/test_install.rs +++ b/tests/test_install.rs @@ -14,8 +14,9 @@ fn test_install_help() { let (at, mut ucmd) = testing(UTIL_NAME); let result = ucmd.arg("--help").run(); - assert_empty_stderr!(result); + assert!(result.success); + assert_empty_stderr!(result); assert!(result.stdout.contains("Usage:")); } @@ -32,8 +33,8 @@ fn test_install_basic() { at.mkdir(dir); let result = ucmd.arg(file1).arg(file2).arg(dir).run(); - assert_empty_stderr!(result); assert!(result.success); + assert_empty_stderr!(result); assert!(at.file_exists(file1)); assert!(at.file_exists(file2)); @@ -57,3 +58,34 @@ fn test_install_unimplemented_arg() { 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")); +} From fa2145bb84d9b04f3bca6648954fc7249129d1c8 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Wed, 13 Jul 2016 15:29:24 +0200 Subject: [PATCH 7/9] Allow specification of mode strings for install We now accept symbolic and numeric mode strings using the --mode or -m option for install. This is used either when moving files into a directory, or when creating component directories with the -d option. This feature was designed to mirror the GNU implementation, including the possibly quirky behaviour of `install --mode=u+wx file dir` resulting in dir/file having exactly permissions 0300. Extensive integration tests are included. This chnage required a higher libc dependency. --- Cargo.lock | 2 +- src/install/Cargo.toml | 2 +- src/install/install.rs | 60 +++++++++++++-- src/install/mode.rs | 166 +++++++++++++++++++++++++++++++++++++++++ tests/test_install.rs | 79 ++++++++++++++++++++ 5 files changed, 299 insertions(+), 10 deletions(-) create mode 100644 src/install/mode.rs diff --git a/Cargo.lock b/Cargo.lock index 4c0640ad4..3b17b9250 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,7 +426,7 @@ name = "install" version = "0.0.1" dependencies = [ "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.1.12 (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", ] diff --git a/src/install/Cargo.toml b/src/install/Cargo.toml index 87fe3d1f4..d2059c88e 100644 --- a/src/install/Cargo.toml +++ b/src/install/Cargo.toml @@ -9,7 +9,7 @@ path = "install.rs" [dependencies] getopts = "*" -libc = "*" +libc = ">= 0.2" uucore = { path="../uucore" } [dev-dependencies] diff --git a/src/install/install.rs b/src/install/install.rs index e5ea969fa..4db8f63db 100644 --- a/src/install/install.rs +++ b/src/install/install.rs @@ -12,21 +12,26 @@ extern crate getopts; extern crate libc; +mod mode; + #[macro_use] extern crate uucore; use std::fs; -use std::io::{Write}; +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, + verbose: bool } #[derive(Clone, Eq, PartialEq)] @@ -41,6 +46,16 @@ pub enum MainFunction { 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. @@ -129,8 +144,7 @@ fn opts() -> getopts::Options { opts.optflagopt("g", "group", "(unimplemented) set group ownership, instead of process'\n \ current group", "GROUP"); - // TODO implement flag - opts.optflagopt("m", "mode", "(unimplemented) set permission mode (as in chmod), instead\n \ + opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead\n \ of rwxr-xr-x", "MODE"); // TODO implement flag @@ -193,8 +207,6 @@ fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> { Err("-D") } else if matches.opt_present("group") { Err("--group, -g") - } else if matches.opt_present("mode") { - Err("--mode, -m") } else if matches.opt_present("owner") { Err("--owner, -o") } else if matches.opt_present("preserve-timestamps") { @@ -239,6 +251,29 @@ fn behaviour(matches: &getopts::Matches) -> Result { 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, @@ -254,6 +289,7 @@ fn behaviour(matches: &getopts::Matches) -> Result { Ok(Behaviour { main_function: main_function, + specified_mode: specified_mode, suffix: backup_suffix, verbose: matches.opt_present("v"), }) @@ -295,6 +331,10 @@ fn directory(paths: &[PathBuf], b: Behaviour) -> i32 { all_successful = false; } + if mode::chmod(&path, b.mode()).is_err() { + all_successful = false; + } + if b.verbose { show_info!("created directory '{}'", path.display()); } @@ -355,7 +395,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &PathBuf, b: &Behaviour) - if all_successful { 0 } else { 1 } } -/// Copy one file to a new location. +/// Copy one file to a new location, changing metadata. /// /// # Parameters /// @@ -372,7 +412,11 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behaviour) -> Result<(), ()> { if let Err(err) = io_result { show_error!("install: cannot install ‘{}’ to ‘{}’: {}", from.display(), to.display(), err); - return Err(()) + return Err(()); + } + + if mode::chmod(&to, b.mode()).is_err() { + return Err(()); } if b.verbose { 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 index f47b6a37d..b223d6339 100644 --- a/tests/test_install.rs +++ b/tests/test_install.rs @@ -6,6 +6,7 @@ extern crate filetime; use self::filetime::*; use common::util::*; +use std::os::unix::fs::PermissionsExt; static UTIL_NAME: &'static str = "install"; @@ -89,3 +90,81 @@ fn test_install_component_directories_failing() { 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)); +} From 28d1de1de05b30c8ed7e31ece85c86ea60774460 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Wed, 13 Jul 2016 15:51:41 +0200 Subject: [PATCH 8/9] Fix type synonym relaiance which breaks some rusts --- src/install/install.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/install/install.rs b/src/install/install.rs index 4db8f63db..7aa97ae3d 100644 --- a/src/install/install.rs +++ b/src/install/install.rs @@ -29,7 +29,7 @@ const DEFAULT_MODE: libc::mode_t = 755; pub struct Behaviour { main_function: MainFunction, - specified_mode: Option, + specified_mode: Option, suffix: String, verbose: bool } From 5de83357496e445bbc12a151f8a3a57311679063 Mon Sep 17 00:00:00 2001 From: Ben Eills Date: Wed, 13 Jul 2016 20:47:04 +0200 Subject: [PATCH 9/9] Remove tab from Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 77fa9222a..556b56e0a 100644 --- a/Makefile +++ b/Makefile @@ -107,7 +107,7 @@ UNIX_PROGS := \ hostid \ hostname \ id \ - install \ + install \ kill \ logname \ mkfifo \