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)); +}