Add `chmod` applet (untested)

This commit is contained in:
Nathan Fisher 2023-01-20 12:08:04 -05:00
parent e648a8a83a
commit 0453acd0de
5 changed files with 222 additions and 28 deletions

View File

@ -21,6 +21,8 @@ code between applets, making for an overall smaller binary.
- base64
- basename
- bootstrap
- chmod
- chown
- clear
- cut
- dirname

View File

@ -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<dyn std::error::Error>> {
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::<String>("mode")
.ok_or(io::Error::new(io::ErrorKind::Other, "no mode given"))?;
if let Some(files) = matches.get_many::<String>("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<crate::Path> {
Some(crate::Path::Bin)
}
}
struct Action<'a> {
path: PathBuf,
feedback: Option<Feedback>,
quiet: bool,
mode: &'a str,
}
impl Action<'_> {
fn apply(&self) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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(())
}
}

View File

@ -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<Self> {
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,

View File

@ -75,6 +75,7 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
"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<Box<dyn Cmd>> {
}
}
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<Self> {
if matches.get_flag("verbose") {
Some(Feedback::Full)
} else if matches.get_flag("changes") {
Some(Feedback::Changes)
} else {
None
}
}
}

View File

@ -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<String, fmt::Error>;
/// Returns a string representing permissions in symbolic format
/// minus file type
fn mode_string(&self) -> Result<String, fmt::Error>;
}
impl Mode for u32 {
fn mode_string(&self) -> Result<String, fmt::Error> {
fn mode_string_full(&self) -> Result<String, fmt::Error> {
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<String, fmt::Error> {
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<u32> 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")
}
}