1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

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.
This commit is contained in:
Ben Eills 2016-07-13 15:29:24 +02:00
parent b15fff6269
commit fa2145bb84
5 changed files with 299 additions and 10 deletions

2
Cargo.lock generated
View file

@ -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",
]

View file

@ -9,7 +9,7 @@ path = "install.rs"
[dependencies]
getopts = "*"
libc = "*"
libc = ">= 0.2"
uucore = { path="../uucore" }
[dev-dependencies]

View file

@ -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<u16>,
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<Behaviour, i32> {
MainFunction::Standard
};
let considering_dir: bool = MainFunction::Directory == main_function;
let specified_mode: Option<libc::mode_t> = 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<Behaviour, i32> {
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 {

166
src/install/mode.rs Normal file
View file

@ -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<libc::mode_t, String> {
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<libc::mode_t, String> {
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<libc::mode_t, String> {
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<char>) -> 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)
}
}

View file

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