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:
parent
b15fff6269
commit
fa2145bb84
5 changed files with 299 additions and 10 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -426,7 +426,7 @@ name = "install"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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",
|
"uucore 0.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ path = "install.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
getopts = "*"
|
getopts = "*"
|
||||||
libc = "*"
|
libc = ">= 0.2"
|
||||||
uucore = { path="../uucore" }
|
uucore = { path="../uucore" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -12,21 +12,26 @@
|
||||||
extern crate getopts;
|
extern crate getopts;
|
||||||
extern crate libc;
|
extern crate libc;
|
||||||
|
|
||||||
|
mod mode;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{Write};
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
|
|
||||||
static NAME: &'static str = "install";
|
static NAME: &'static str = "install";
|
||||||
static VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
static VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
const DEFAULT_MODE: libc::mode_t = 755;
|
||||||
|
|
||||||
pub struct Behaviour {
|
pub struct Behaviour {
|
||||||
main_function: MainFunction,
|
main_function: MainFunction,
|
||||||
|
specified_mode: Option<u16>,
|
||||||
suffix: String,
|
suffix: String,
|
||||||
verbose: bool,
|
verbose: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
@ -41,6 +46,16 @@ pub enum MainFunction {
|
||||||
Standard
|
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.
|
/// Main install utility function, called from main.rs.
|
||||||
///
|
///
|
||||||
/// Returns a program return code.
|
/// 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 \
|
opts.optflagopt("g", "group", "(unimplemented) set group ownership, instead of process'\n \
|
||||||
current group", "GROUP");
|
current group", "GROUP");
|
||||||
|
|
||||||
// TODO implement flag
|
opts.optflagopt("m", "mode", "set permission mode (as in chmod), instead\n \
|
||||||
opts.optflagopt("m", "mode", "(unimplemented) set permission mode (as in chmod), instead\n \
|
|
||||||
of rwxr-xr-x", "MODE");
|
of rwxr-xr-x", "MODE");
|
||||||
|
|
||||||
// TODO implement flag
|
// TODO implement flag
|
||||||
|
@ -193,8 +207,6 @@ fn check_unimplemented(matches: &getopts::Matches) -> Result<(), &str> {
|
||||||
Err("-D")
|
Err("-D")
|
||||||
} else if matches.opt_present("group") {
|
} else if matches.opt_present("group") {
|
||||||
Err("--group, -g")
|
Err("--group, -g")
|
||||||
} else if matches.opt_present("mode") {
|
|
||||||
Err("--mode, -m")
|
|
||||||
} else if matches.opt_present("owner") {
|
} else if matches.opt_present("owner") {
|
||||||
Err("--owner, -o")
|
Err("--owner, -o")
|
||||||
} else if matches.opt_present("preserve-timestamps") {
|
} else if matches.opt_present("preserve-timestamps") {
|
||||||
|
@ -239,6 +251,29 @@ fn behaviour(matches: &getopts::Matches) -> Result<Behaviour, i32> {
|
||||||
MainFunction::Standard
|
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") {
|
let backup_suffix = if matches.opt_present("suffix") {
|
||||||
match matches.opt_str("suffix") {
|
match matches.opt_str("suffix") {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
|
@ -254,6 +289,7 @@ fn behaviour(matches: &getopts::Matches) -> Result<Behaviour, i32> {
|
||||||
|
|
||||||
Ok(Behaviour {
|
Ok(Behaviour {
|
||||||
main_function: main_function,
|
main_function: main_function,
|
||||||
|
specified_mode: specified_mode,
|
||||||
suffix: backup_suffix,
|
suffix: backup_suffix,
|
||||||
verbose: matches.opt_present("v"),
|
verbose: matches.opt_present("v"),
|
||||||
})
|
})
|
||||||
|
@ -295,6 +331,10 @@ fn directory(paths: &[PathBuf], b: Behaviour) -> i32 {
|
||||||
all_successful = false;
|
all_successful = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mode::chmod(&path, b.mode()).is_err() {
|
||||||
|
all_successful = false;
|
||||||
|
}
|
||||||
|
|
||||||
if b.verbose {
|
if b.verbose {
|
||||||
show_info!("created directory '{}'", path.display());
|
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 }
|
if all_successful { 0 } else { 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy one file to a new location.
|
/// Copy one file to a new location, changing metadata.
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
|
@ -372,7 +412,11 @@ fn copy(from: &PathBuf, to: &PathBuf, b: &Behaviour) -> Result<(), ()> {
|
||||||
if let Err(err) = io_result {
|
if let Err(err) = io_result {
|
||||||
show_error!("install: cannot install ‘{}’ to ‘{}’: {}",
|
show_error!("install: cannot install ‘{}’ to ‘{}’: {}",
|
||||||
from.display(), to.display(), err);
|
from.display(), to.display(), err);
|
||||||
return Err(())
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode::chmod(&to, b.mode()).is_err() {
|
||||||
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.verbose {
|
if b.verbose {
|
||||||
|
|
166
src/install/mode.rs
Normal file
166
src/install/mode.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ extern crate filetime;
|
||||||
|
|
||||||
use self::filetime::*;
|
use self::filetime::*;
|
||||||
use common::util::*;
|
use common::util::*;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
static UTIL_NAME: &'static str = "install";
|
static UTIL_NAME: &'static str = "install";
|
||||||
|
|
||||||
|
@ -89,3 +90,81 @@ fn test_install_component_directories_failing() {
|
||||||
assert!(!result.success);
|
assert!(!result.success);
|
||||||
assert!(result.stderr.contains("File exists"));
|
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));
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue