use clap::{crate_version, Arg, ArgAction, Command};
use std::ffi::OsString;
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path;
use uucore::display::Quotable;
use uucore::error::{set_exit_code, ExitCode, UResult, USimpleError, UUsageError};
use uucore::fs::display_permissions_unix;
use uucore::libc::mode_t;
#[cfg(not(windows))]
use uucore::mode;
use uucore::perms::{configure_symlink_and_recursion, TraverseSymlinks};
use uucore::{format_usage, help_about, help_section, help_usage, show, show_error};
const ABOUT: &str = help_about!("chmod.md");
const USAGE: &str = help_usage!("chmod.md");
const LONG_USAGE: &str = help_section!("after help", "chmod.md");
mod options {
pub const HELP: &str = "help";
pub const CHANGES: &str = "changes";
pub const QUIET: &str = "quiet"; pub const VERBOSE: &str = "verbose";
pub const NO_PRESERVE_ROOT: &str = "no-preserve-root";
pub const PRESERVE_ROOT: &str = "preserve-root";
pub const REFERENCE: &str = "RFILE";
pub const RECURSIVE: &str = "recursive";
pub const MODE: &str = "MODE";
pub const FILE: &str = "FILE";
}
fn extract_negative_modes(mut args: impl uucore::Args) -> (Option<String>, Vec<OsString>) {
let (parsed_cmode_vec, pre_double_hyphen_args): (Vec<OsString>, Vec<OsString>) =
args.by_ref().take_while(|a| a != "--").partition(|arg| {
let arg = if let Some(arg) = arg.to_str() {
arg.to_string()
} else {
return false;
};
arg.len() >= 2
&& arg.starts_with('-')
&& matches!(
arg.chars().nth(1).unwrap(),
'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7'
)
});
let mut clean_args = Vec::new();
if !parsed_cmode_vec.is_empty() {
clean_args.push("w".into());
}
clean_args.extend(pre_double_hyphen_args);
if let Some(arg) = args.next() {
clean_args.push("--".into());
clean_args.push(arg);
}
clean_args.extend(args);
let parsed_cmode = Some(
parsed_cmode_vec
.iter()
.map(|s| s.to_str().unwrap())
.collect::<Vec<&str>>()
.join(","),
)
.filter(|s| !s.is_empty());
(parsed_cmode, clean_args)
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let (parsed_cmode, args) = extract_negative_modes(args.skip(1)); let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let changes = matches.get_flag(options::CHANGES);
let quiet = matches.get_flag(options::QUIET);
let verbose = matches.get_flag(options::VERBOSE);
let preserve_root = matches.get_flag(options::PRESERVE_ROOT);
let fmode = match matches.get_one::<String>(options::REFERENCE) {
Some(fref) => match fs::metadata(fref) {
Ok(meta) => Some(meta.mode() & 0o7777),
Err(err) => {
return Err(USimpleError::new(
1,
format!("cannot stat attributes of {}: {}", fref.quote(), err),
))
}
},
None => None,
};
let modes = matches.get_one::<String>(options::MODE);
let cmode = if let Some(parsed_cmode) = parsed_cmode {
parsed_cmode
} else {
modes.unwrap().to_string() };
let mut files: Vec<String> = matches
.get_many::<String>(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
let cmode = if fmode.is_some() {
files.push(cmode);
None
} else {
Some(cmode)
};
if files.is_empty() {
return Err(UUsageError::new(1, "missing operand".to_string()));
}
let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?;
let chmoder = Chmoder {
changes,
quiet,
verbose,
preserve_root,
recursive,
fmode,
cmode,
traverse_symlinks,
dereference,
};
chmoder.chmod(&files)
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.args_override_self(true)
.infer_long_args(true)
.no_binary_name(true)
.disable_help_flag(true)
.arg(
Arg::new(options::HELP)
.long(options::HELP)
.help("Print help information.")
.action(ArgAction::Help),
)
.arg(
Arg::new(options::CHANGES)
.long(options::CHANGES)
.short('c')
.help("like verbose but report only when a change is made")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::QUIET)
.long(options::QUIET)
.visible_alias("silent")
.short('f')
.help("suppress most error messages")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::VERBOSE)
.long(options::VERBOSE)
.short('v')
.help("output a diagnostic for every file processed")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::NO_PRESERVE_ROOT)
.long(options::NO_PRESERVE_ROOT)
.help("do not treat '/' specially (the default)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::PRESERVE_ROOT)
.long(options::PRESERVE_ROOT)
.help("fail to operate recursively on '/'")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::RECURSIVE)
.long(options::RECURSIVE)
.short('R')
.help("change files and directories recursively")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::REFERENCE)
.long("reference")
.value_hint(clap::ValueHint::FilePath)
.help("use RFILE's mode instead of MODE values"),
)
.arg(
Arg::new(options::MODE).required_unless_present(options::REFERENCE),
)
.arg(
Arg::new(options::FILE)
.required_unless_present(options::MODE)
.action(ArgAction::Append)
.value_hint(clap::ValueHint::AnyPath),
)
.args(uucore::perms::common_args())
}
struct Chmoder {
changes: bool,
quiet: bool,
verbose: bool,
preserve_root: bool,
recursive: bool,
fmode: Option<u32>,
cmode: Option<String>,
traverse_symlinks: TraverseSymlinks,
dereference: bool,
}
impl Chmoder {
fn chmod(&self, files: &[String]) -> UResult<()> {
let mut r = Ok(());
for filename in files {
let filename = &filename[..];
let file = Path::new(filename);
if !file.exists() {
if file.is_symlink() {
if !self.dereference && !self.recursive {
continue;
}
if !self.quiet {
show!(USimpleError::new(
1,
format!("cannot operate on dangling symlink {}", filename.quote()),
));
set_exit_code(1);
}
if self.verbose {
println!(
"failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)",
filename.quote()
);
}
} else if !self.quiet {
show!(USimpleError::new(
1,
format!(
"cannot access {}: No such file or directory",
filename.quote()
)
));
}
set_exit_code(1);
continue;
} else if !self.dereference && file.is_symlink() {
continue;
}
if self.recursive && self.preserve_root && filename == "/" {
return Err(USimpleError::new(
1,
format!(
"it is dangerous to operate recursively on {}\nchmod: use --no-preserve-root to override this failsafe",
filename.quote()
)
));
}
if self.recursive {
r = self.walk_dir(file);
} else {
r = self.chmod_file(file).and(r);
}
}
r
}
fn walk_dir(&self, file_path: &Path) -> UResult<()> {
let mut r = self.chmod_file(file_path);
let should_follow_symlink = match self.traverse_symlinks {
TraverseSymlinks::All => true,
TraverseSymlinks::First => {
file_path == file_path.canonicalize().unwrap_or(file_path.to_path_buf())
}
TraverseSymlinks::None => false,
};
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
for dir_entry in file_path.read_dir()? {
let path = dir_entry?.path();
if !path.is_symlink() {
r = self.walk_dir(path.as_path());
} else if should_follow_symlink {
r = self.chmod_file(path.as_path()).and(r);
}
}
}
r
}
#[cfg(windows)]
fn chmod_file(&self, file: &Path) -> UResult<()> {
Ok(())
}
#[cfg(unix)]
fn chmod_file(&self, file: &Path) -> UResult<()> {
use uucore::{mode::get_umask, perms::get_metadata};
let metadata = get_metadata(file, self.dereference);
let fperm = match metadata {
Ok(meta) => meta.mode() & 0o7777,
Err(err) => {
if file.is_symlink() && !self.dereference {
if self.verbose {
println!(
"neither symbolic link {} nor referent has been changed",
file.quote()
);
}
return Ok(()); } else if err.kind() == std::io::ErrorKind::PermissionDenied {
return Err(USimpleError::new(
1,
format!("{}: Permission denied", file.quote()),
));
} else {
return Err(USimpleError::new(1, format!("{}: {}", file.quote(), err)));
}
}
};
match self.fmode {
Some(mode) => self.change_file(fperm, mode, file)?,
None => {
let cmode_unwrapped = self.cmode.clone().unwrap();
let mut new_mode = fperm;
let mut naively_expected_new_mode = new_mode;
for mode in cmode_unwrapped.split(',') {
let result = if mode.chars().any(|c| c.is_ascii_digit()) {
mode::parse_numeric(new_mode, mode, file.is_dir()).map(|v| (v, v))
} else {
mode::parse_symbolic(new_mode, mode, get_umask(), file.is_dir()).map(|m| {
let naive_mode = mode::parse_symbolic(
naively_expected_new_mode,
mode,
0,
file.is_dir(),
)
.unwrap(); (m, naive_mode)
})
};
match result {
Ok((mode, naive_mode)) => {
new_mode = mode;
naively_expected_new_mode = naive_mode;
}
Err(f) => {
return if self.quiet {
Err(ExitCode::new(1))
} else {
Err(USimpleError::new(1, f))
};
}
}
}
self.change_file(fperm, new_mode, file)?;
if (new_mode & !naively_expected_new_mode) != 0 {
return Err(USimpleError::new(
1,
format!(
"{}: new permissions are {}, not {}",
file.maybe_quote(),
display_permissions_unix(new_mode as mode_t, false),
display_permissions_unix(naively_expected_new_mode as mode_t, false)
),
));
}
}
}
Ok(())
}
#[cfg(unix)]
fn change_file(&self, fperm: u32, mode: u32, file: &Path) -> Result<(), i32> {
if fperm == mode {
if self.verbose && !self.changes {
println!(
"mode of {} retained as {:04o} ({})",
file.quote(),
fperm,
display_permissions_unix(fperm as mode_t, false),
);
}
Ok(())
} else if let Err(err) = fs::set_permissions(file, fs::Permissions::from_mode(mode)) {
if !self.quiet {
show_error!("{}", err);
}
if self.verbose {
println!(
"failed to change mode of file {} from {:04o} ({}) to {:04o} ({})",
file.quote(),
fperm,
display_permissions_unix(fperm as mode_t, false),
mode,
display_permissions_unix(mode as mode_t, false)
);
}
Err(1)
} else {
if self.verbose || self.changes {
println!(
"mode of {} changed from {:04o} ({}) to {:04o} ({})",
file.quote(),
fperm,
display_permissions_unix(fperm as mode_t, false),
mode,
display_permissions_unix(mode as mode_t, false)
);
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_negative_modes() {
let (c, a) = extract_negative_modes(["-w", "-r", "file"].iter().map(OsString::from));
assert_eq!(c, Some("-w,-r".to_string()));
assert_eq!(a, ["w", "file"]);
let (c, a) = extract_negative_modes(["-w", "file", "-r"].iter().map(OsString::from));
assert_eq!(c, Some("-w,-r".to_string()));
assert_eq!(a, ["w", "file"]);
let (c, a) = extract_negative_modes(["-w", "--", "-r", "f"].iter().map(OsString::from));
assert_eq!(c, Some("-w".to_string()));
assert_eq!(a, ["w", "--", "-r", "f"]);
let (c, a) = extract_negative_modes(["--", "-r", "file"].iter().map(OsString::from));
assert_eq!(c, None);
assert_eq!(a, ["--", "-r", "file"]);
}
}