diff --git a/README.md b/README.md index 4dff552..0b2acd5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ code between applets, making for an overall smaller binary. - base64 - basename - bootstrap +- chmod +- chown - clear - cut - dirname diff --git a/src/cmd/chmod/mod.rs b/src/cmd/chmod/mod.rs index 8b13789..a476970 100644 --- a/src/cmd/chmod/mod.rs +++ b/src/cmd/chmod/mod.rs @@ -1 +1,168 @@ +use super::{Cmd, Feedback}; +use crate::mode::{Mode, Parser}; +use clap::{Arg, ArgAction, Command}; +use std::{ + error::Error, + fs::{self, File, Permissions}, + io, + os::unix::prelude::{MetadataExt, PermissionsExt}, + path::PathBuf, +}; +use walkdir::{DirEntry, WalkDir}; +#[derive(Debug, Default)] +pub struct Chmod; + +impl Cmd for Chmod { + fn cli(&self) -> clap::Command { + Command::new("chmod") + .about("change file mode bits") + .author("Nathan Fisher") + .version(env!("CARGO_PKG_VERSION")) + .args([ + Arg::new("verbose") + .short('v') + .long("verbose") + .help("output a diagnostic for every file processed") + .action(ArgAction::SetTrue), + Arg::new("changes") + .short('c') + .long("changes") + .help("like verbose but report only when a change is made") + .conflicts_with("verbose") + .action(ArgAction::SetTrue), + Arg::new("quiet") + .short('f') + .long("silent") + .visible_alias("quiet") + .help("suppress most error messages") + .action(ArgAction::SetTrue), + Arg::new("recursive") + .short('R') + .long("recursive") + .visible_short_alias('r') + .help("change files and directories recursively") + .action(ArgAction::SetTrue), + Arg::new("mode") + .value_name("MODE") + .value_delimiter(',') + .num_args(1) + .required(true), + Arg::new("file") + .value_name("FILE") + .num_args(1..) + .required(true), + ]) + } + + fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + let Some(matches) = matches else { + return Err(io::Error::new(io::ErrorKind::Other, "no input").into()); + }; + let feedback = Feedback::from_matches(matches); + let mode = matches + .get_one::("mode") + .ok_or(io::Error::new(io::ErrorKind::Other, "no mode given"))?; + if let Some(files) = matches.get_many::("file") { + for f in files { + let mut path = PathBuf::from(f); + if path.is_symlink() { + path = fs::read_link(&path)?; + } + let action = Action { + path, + feedback, + quiet: matches.get_flag("quiet"), + mode, + }; + action.apply()?; + if matches.get_flag("recursive") { + if action.path.is_dir() { + action.recurse()?; + } + } + } + } + Ok(()) + } + + fn path(&self) -> Option { + Some(crate::Path::Bin) + } +} + +struct Action<'a> { + path: PathBuf, + feedback: Option, + quiet: bool, + mode: &'a str, +} + +impl Action<'_> { + fn apply(&self) -> Result<(), Box> { + let oldmode = { + let fd = File::open(&self.path)?; + let meta = fd.metadata()?; + meta.mode() + }; + let mut parser = Parser::new(oldmode); + let mode = parser.parse(self.mode)?; + let permissions = Permissions::from_mode(mode); + fs::set_permissions(&self.path, permissions)?; + if let Some(f) = self.feedback { + match f { + Feedback::Full => { + if oldmode == mode { + self.display_retained(oldmode)?; + } else { + self.display_changes(oldmode, mode)?; + } + } + Feedback::Changes => { + if oldmode != mode { + self.display_changes(oldmode, mode)?; + } + } + } + } + Ok(()) + } + + fn display_changes(&self, oldmode: u32, mode: u32) -> Result<(), Box> { + let oldstring = oldmode.mode_string()?; + let newstring = mode.mode_string()?; + println!( + "mode of '{}' changed from {oldmode:o} ({oldstring}) to {mode:o} ({newstring})", + self.path.display() + ); + Ok(()) + } + + fn display_retained(&self, mode: u32) -> Result<(), Box> { + let modestring = mode.mode_string()?; + println!( + "mode of '{}' retained as {mode:o} ({modestring})", + self.path.display() + ); + Ok(()) + } + + fn into_child(&self, entry: DirEntry) -> Self { + Self { + path: entry.path().to_path_buf(), + feedback: self.feedback, + quiet: self.quiet, + mode: self.mode, + } + } + + fn recurse(&self) -> Result<(), Box> { + let walker = WalkDir::new(&self.path).max_open(1); + for entry in walker { + let entry = entry?; + let action = self.into_child(entry); + action.apply()?; + } + Ok(()) + } +} diff --git a/src/cmd/chown/mod.rs b/src/cmd/chown/mod.rs index 798ef89..46815c8 100644 --- a/src/cmd/chown/mod.rs +++ b/src/cmd/chown/mod.rs @@ -1,4 +1,4 @@ -use super::Cmd; +use super::{Cmd, Feedback}; use crate::pw; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint}; use std::{ @@ -165,24 +165,6 @@ impl Recurse { } } -#[derive(Clone, Copy, Debug)] -enum Feedback { - Full, - Changes, -} - -impl Feedback { - fn from_matches(matches: &ArgMatches) -> Option { - if matches.get_flag("verbose") { - Some(Feedback::Full) - } else if matches.get_flag("changes") { - Some(Feedback::Changes) - } else { - None - } - } -} - #[derive(Clone, Debug)] struct User<'a> { name: &'a str, diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 9de2b08..6d718d6 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -75,6 +75,7 @@ pub fn get(name: &str) -> Option> { "base32" => Some(Box::new(Base32::default())), "basename" => Some(Box::new(Basename::default())), "bootstrap" => Some(Box::new(Bootstrap::default())), + "chmod" => Some(Box::new(chmod::Chmod::default())), "chown" => Some(Box::new(chown::Chown::default())), "clear" => Some(Box::new(Clear::default())), "cut" => Some(Box::new(Cut::default())), @@ -104,11 +105,12 @@ pub fn get(name: &str) -> Option> { } } -pub static COMMANDS: [&str; 30] = [ +pub static COMMANDS: [&str; 31] = [ "base32", "base64", "basename", "bootstrap", + "chmod", "chown", "clear", "cut", @@ -136,3 +138,21 @@ pub static COMMANDS: [&str; 30] = [ "whoami", "yes", ]; + +#[derive(Clone, Copy, Debug)] +enum Feedback { + Full, + Changes, +} + +impl Feedback { + fn from_matches(matches: &ArgMatches) -> Option { + if matches.get_flag("verbose") { + Some(Feedback::Full) + } else if matches.get_flag("changes") { + Some(Feedback::Changes) + } else { + None + } + } +} diff --git a/src/mode/mod.rs b/src/mode/mod.rs index ca7840e..b763ed4 100644 --- a/src/mode/mod.rs +++ b/src/mode/mod.rs @@ -54,11 +54,16 @@ impl Bit { /// Functions for extracting information about Unix modes pub trait Mode { /// Returns a string representing permissions in symbolic format + /// including file type + fn mode_string_full(&self) -> Result; + + /// Returns a string representing permissions in symbolic format + /// minus file type fn mode_string(&self) -> Result; } impl Mode for u32 { - fn mode_string(&self) -> Result { + fn mode_string_full(&self) -> Result { let b = if self & 0o40000 != 0 && self & 0o20000 != 0 { 'b' } else if self & 0o40000 != 0 { @@ -85,6 +90,24 @@ impl Mode for u32 { .try_for_each(|b| write!(s, "{}", b.as_char(*self)))?; Ok(s) } + + fn mode_string(&self) -> Result { + let mut s = String::new(); + [ + Bit::URead, + Bit::UWrite, + Bit::UExec, + Bit::GRead, + Bit::GWrite, + Bit::GExec, + Bit::ORead, + Bit::OWrite, + Bit::OExec, + ] + .iter() + .try_for_each(|b| write!(s, "{}", b.as_char(*self)))?; + Ok(s) + } } impl BitAnd for Bit { @@ -147,49 +170,49 @@ mod test { #[test] fn display_bits_dir() { let m: u32 = 0o40755; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "drwxr-xr-x") } #[test] fn display_bits_char() { let m: u32 = 0o20666; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "crw-rw-rw-") } #[test] fn display_bits_block() { let m: u32 = 0o60660; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "brw-rw----") } #[test] fn display_bits_file() { let m: u32 = 0o100644; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "-rw-r--r--") } #[test] fn display_bits_suid() { let m: u32 = 0o104755; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "-rwsr-xr-x") } #[test] fn display_bits_sgid() { let m: u32 = 0o102755; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "-rwxr-sr-x") } #[test] fn display_bits_sticky() { let m: u32 = 0o41777; - let s = m.mode_string().unwrap(); + let s = m.mode_string_full().unwrap(); assert_eq!(s.as_str(), "drwxrwxrwt") } }