use std::fs;
use std::io;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
#[derive(Debug, Clone, Default)]
pub struct ChmodConfig {
pub changes: bool,
pub quiet: bool,
pub verbose: bool,
pub preserve_root: bool,
pub recursive: bool,
}
const S_ISUID: u32 = 0o4000;
const S_ISGID: u32 = 0o2000;
const S_ISVTX: u32 = 0o1000;
const S_IRUSR: u32 = 0o0400;
const S_IWUSR: u32 = 0o0200;
const S_IXUSR: u32 = 0o0100;
const S_IRGRP: u32 = 0o0040;
const S_IWGRP: u32 = 0o0020;
const S_IXGRP: u32 = 0o0010;
const S_IROTH: u32 = 0o0004;
const S_IWOTH: u32 = 0o0002;
const S_IXOTH: u32 = 0o0001;
const USER_BITS: u32 = S_IRUSR | S_IWUSR | S_IXUSR;
const GROUP_BITS: u32 = S_IRGRP | S_IWGRP | S_IXGRP;
const OTHER_BITS: u32 = S_IROTH | S_IWOTH | S_IXOTH;
const ALL_BITS: u32 = USER_BITS | GROUP_BITS | OTHER_BITS | S_ISUID | S_ISGID | S_ISVTX;
pub fn parse_mode(mode_str: &str, current_mode: u32) -> Result<u32, String> {
if !mode_str.is_empty() && mode_str.chars().all(|c| c.is_ascii_digit() && c < '8') {
if let Ok(octal) = u32::from_str_radix(mode_str, 8) {
return Ok(octal & 0o7777);
}
}
parse_symbolic_mode(mode_str, current_mode)
}
pub fn parse_mode_no_umask(mode_str: &str, current_mode: u32) -> Result<u32, String> {
if !mode_str.is_empty() && mode_str.chars().all(|c| c.is_ascii_digit() && c < '8') {
if let Ok(octal) = u32::from_str_radix(mode_str, 8) {
return Ok(octal & 0o7777);
}
}
parse_symbolic_mode_with_umask(mode_str, current_mode, 0)
}
pub fn parse_mode_check_umask(mode_str: &str, current_mode: u32) -> Result<(u32, bool), String> {
if !mode_str.is_empty() && mode_str.chars().all(|c| c.is_ascii_digit() && c < '8') {
if let Ok(octal) = u32::from_str_radix(mode_str, 8) {
return Ok((octal & 0o7777, false));
}
}
let umask = get_umask();
let with_umask = parse_symbolic_mode_with_umask(mode_str, current_mode, umask)?;
let without_umask = parse_symbolic_mode_with_umask(mode_str, current_mode, 0)?;
Ok((with_umask, with_umask != without_umask))
}
fn parse_symbolic_mode(mode_str: &str, current_mode: u32) -> Result<u32, String> {
parse_symbolic_mode_with_umask(mode_str, current_mode, get_umask())
}
fn parse_symbolic_mode_with_umask(
mode_str: &str,
current_mode: u32,
umask: u32,
) -> Result<u32, String> {
let mut mode = current_mode & 0o7777;
let file_type_bits = current_mode & 0o170000;
for clause in mode_str.split(',') {
if clause.is_empty() {
return Err(format!("invalid mode: '{}'", mode_str));
}
mode = apply_symbolic_clause(clause, mode | file_type_bits, umask)? & 0o7777;
}
Ok(mode)
}
pub fn get_umask() -> u32 {
let old = unsafe { libc::umask(0) };
unsafe {
libc::umask(old);
}
old as u32
}
fn apply_symbolic_clause(clause: &str, current_mode: u32, umask: u32) -> Result<u32, String> {
let bytes = clause.as_bytes();
let len = bytes.len();
let mut pos = 0;
let mut who_mask: u32 = 0;
let mut who_specified = false;
while pos < len {
match bytes[pos] {
b'u' => {
who_mask |= USER_BITS | S_ISUID;
who_specified = true;
}
b'g' => {
who_mask |= GROUP_BITS | S_ISGID;
who_specified = true;
}
b'o' => {
who_mask |= OTHER_BITS | S_ISVTX;
who_specified = true;
}
b'a' => {
who_mask |= ALL_BITS;
who_specified = true;
}
_ => break,
}
pos += 1;
}
if !who_specified {
who_mask = ALL_BITS;
}
if pos >= len {
return Err(format!("invalid mode: '{}'", clause));
}
let mut mode = current_mode;
while pos < len {
let op = match bytes[pos] {
b'+' => '+',
b'-' => '-',
b'=' => '=',
_ => return Err(format!("invalid mode: '{}'", clause)),
};
pos += 1;
let mut perm_bits: u32 = 0;
let mut has_x_cap = false;
let mut has_perm_chars = false;
let mut has_copy_from = false;
while pos < len && bytes[pos] != b'+' && bytes[pos] != b'-' && bytes[pos] != b'=' {
match bytes[pos] {
b'r' => {
if has_copy_from {
return Err(format!("invalid mode: '{}'", clause));
}
has_perm_chars = true;
perm_bits |= S_IRUSR | S_IRGRP | S_IROTH;
}
b'w' => {
if has_copy_from {
return Err(format!("invalid mode: '{}'", clause));
}
has_perm_chars = true;
perm_bits |= S_IWUSR | S_IWGRP | S_IWOTH;
}
b'x' => {
if has_copy_from {
return Err(format!("invalid mode: '{}'", clause));
}
has_perm_chars = true;
perm_bits |= S_IXUSR | S_IXGRP | S_IXOTH;
}
b'X' => {
if has_copy_from {
return Err(format!("invalid mode: '{}'", clause));
}
has_perm_chars = true;
has_x_cap = true;
}
b's' => {
if has_copy_from {
return Err(format!("invalid mode: '{}'", clause));
}
has_perm_chars = true;
perm_bits |= S_ISUID | S_ISGID;
}
b't' => {
if has_copy_from {
return Err(format!("invalid mode: '{}'", clause));
}
has_perm_chars = true;
perm_bits |= S_ISVTX;
}
b'u' => {
if has_perm_chars {
return Err(format!("invalid mode: '{}'", clause));
}
has_copy_from = true;
let u = current_mode & USER_BITS;
perm_bits |= u | (u >> 3) | (u >> 6);
}
b'g' => {
if has_perm_chars {
return Err(format!("invalid mode: '{}'", clause));
}
has_copy_from = true;
let g = current_mode & GROUP_BITS;
perm_bits |= (g << 3) | g | (g >> 3);
}
b'o' => {
if has_perm_chars {
return Err(format!("invalid mode: '{}'", clause));
}
has_copy_from = true;
let o = current_mode & OTHER_BITS;
perm_bits |= (o << 6) | (o << 3) | o;
}
b',' => break,
_ => return Err(format!("invalid mode: '{}'", clause)),
}
pos += 1;
}
if has_x_cap {
let is_executable = (current_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
let is_dir = (current_mode & 0o170000) == 0o040000;
if is_executable || is_dir {
perm_bits |= S_IXUSR | S_IXGRP | S_IXOTH;
}
}
let effective = perm_bits & who_mask;
let effective = if !who_specified {
let umask_filter = !(umask) & (USER_BITS | GROUP_BITS | OTHER_BITS);
let special = effective & (S_ISUID | S_ISGID | S_ISVTX);
(effective & umask_filter) | special
} else {
effective
};
match op {
'+' => {
mode |= effective;
}
'-' => {
mode &= !effective;
}
'=' => {
let clear_mask = who_mask & (USER_BITS | GROUP_BITS | OTHER_BITS);
let clear_special = who_mask & (S_ISUID | S_ISGID | S_ISVTX);
mode &= !(clear_mask | clear_special);
let effective_eq = if !who_specified {
let umask_filter = !(umask) & (USER_BITS | GROUP_BITS | OTHER_BITS);
let special = (perm_bits & who_mask) & (S_ISUID | S_ISGID | S_ISVTX);
((perm_bits & who_mask) & umask_filter) | special
} else {
perm_bits & who_mask
};
mode |= effective_eq;
}
_ => unreachable!(),
}
}
Ok(mode)
}
fn format_mode(mode: u32) -> String {
format!("{:04o}", mode & 0o7777)
}
fn format_symbolic(mode: u32) -> String {
let m = mode & 0o7777;
let mut s = [b'-'; 9];
if m & S_IRUSR != 0 {
s[0] = b'r';
}
if m & S_IWUSR != 0 {
s[1] = b'w';
}
if m & S_IXUSR != 0 {
s[2] = if m & S_ISUID != 0 { b's' } else { b'x' };
} else if m & S_ISUID != 0 {
s[2] = b'S';
}
if m & S_IRGRP != 0 {
s[3] = b'r';
}
if m & S_IWGRP != 0 {
s[4] = b'w';
}
if m & S_IXGRP != 0 {
s[5] = if m & S_ISGID != 0 { b's' } else { b'x' };
} else if m & S_ISGID != 0 {
s[5] = b'S';
}
if m & S_IROTH != 0 {
s[6] = b'r';
}
if m & S_IWOTH != 0 {
s[7] = b'w';
}
if m & S_IXOTH != 0 {
s[8] = if m & S_ISVTX != 0 { b't' } else { b'x' };
} else if m & S_ISVTX != 0 {
s[8] = b'T';
}
String::from_utf8(s.to_vec()).unwrap()
}
pub fn format_symbolic_for_warning(mode: u32) -> String {
format_symbolic(mode)
}
pub fn chmod_file(path: &Path, mode: u32, config: &ChmodConfig) -> Result<bool, io::Error> {
let metadata = fs::symlink_metadata(path)?;
if metadata.file_type().is_symlink() {
return Ok(false);
}
let old_mode = metadata.mode() & 0o7777;
let changed = old_mode != mode;
if changed {
let perms = fs::Permissions::from_mode(mode);
fs::set_permissions(path, perms)?;
}
let path_display = path.display();
if config.verbose {
if changed {
println!(
"mode of '{}' changed from {} ({}) to {} ({})",
path_display,
format_mode(old_mode),
format_symbolic(old_mode),
format_mode(mode),
format_symbolic(mode)
);
} else {
println!(
"mode of '{}' retained as {} ({})",
path_display,
format_mode(old_mode),
format_symbolic(old_mode)
);
}
} else if config.changes && changed {
println!(
"mode of '{}' changed from {} ({}) to {} ({})",
path_display,
format_mode(old_mode),
format_symbolic(old_mode),
format_mode(mode),
format_symbolic(mode)
);
}
Ok(changed)
}
pub fn chmod_recursive(
path: &Path,
mode_str: &str,
config: &ChmodConfig,
) -> Result<bool, io::Error> {
if config.preserve_root && path == Path::new("/") {
return Err(io::Error::other(
"it is dangerous to operate recursively on '/'",
));
}
let mut had_error = false;
match process_entry(path, mode_str, config) {
Ok(()) => {}
Err(e) => {
if !config.quiet {
eprintln!("chmod: cannot access '{}': {}", path.display(), e);
}
had_error = true;
}
}
if path.is_dir() {
walk_dir(path, mode_str, config, &mut had_error);
}
if had_error {
Err(io::Error::other("some operations failed"))
} else {
Ok(true)
}
}
fn process_entry(path: &Path, mode_str: &str, config: &ChmodConfig) -> Result<(), io::Error> {
let metadata = fs::symlink_metadata(path)?;
if metadata.file_type().is_symlink() {
return Ok(());
}
let current_mode = metadata.mode();
let mut new_mode = parse_mode(mode_str, current_mode).map_err(|e| io::Error::other(e))?;
if metadata.is_dir()
&& !mode_str.is_empty()
&& mode_str.bytes().all(|b| b.is_ascii_digit() && b < b'8')
&& mode_str.len() <= 4
{
let existing_special = current_mode & 0o7000;
new_mode |= existing_special;
}
chmod_file(path, new_mode, config)?;
Ok(())
}
fn walk_dir(dir: &Path, mode_str: &str, config: &ChmodConfig, had_error: &mut bool) {
if !config.verbose && !config.changes {
let error_flag = std::sync::atomic::AtomicBool::new(false);
walk_dir_parallel(dir, mode_str, config, &error_flag);
if error_flag.load(std::sync::atomic::Ordering::Relaxed) {
*had_error = true;
}
return;
}
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
if !config.quiet {
eprintln!("chmod: cannot open directory '{}': {}", dir.display(), e);
}
*had_error = true;
return;
}
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
if !config.quiet {
eprintln!("chmod: error reading directory entry: {}", e);
}
*had_error = true;
continue;
}
};
let entry_path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(e) => {
if !config.quiet {
eprintln!(
"chmod: cannot read file type of '{}': {}",
entry_path.display(),
e
);
}
*had_error = true;
continue;
}
};
if file_type.is_symlink() {
continue;
}
match process_entry(&entry_path, mode_str, config) {
Ok(()) => {}
Err(e) => {
if !config.quiet {
eprintln!(
"chmod: changing permissions of '{}': {}",
entry_path.display(),
e
);
}
*had_error = true;
}
}
if file_type.is_dir() {
walk_dir(&entry_path, mode_str, config, had_error);
}
}
}
fn walk_dir_parallel(
dir: &Path,
mode_str: &str,
config: &ChmodConfig,
had_error: &std::sync::atomic::AtomicBool,
) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
if !config.quiet {
eprintln!("chmod: cannot open directory '{}': {}", dir.display(), e);
}
had_error.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
};
let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
use rayon::prelude::*;
entries.par_iter().for_each(|entry| {
let entry_path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => {
had_error.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
};
if file_type.is_symlink() {
return;
}
if process_entry(&entry_path, mode_str, config).is_err() {
had_error.store(true, std::sync::atomic::Ordering::Relaxed);
}
if file_type.is_dir() {
walk_dir_parallel(&entry_path, mode_str, config, had_error);
}
});
}