diff --git a/Cargo.lock b/Cargo.lock index cd8f96a..8807634 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -25,6 +31,12 @@ version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.1.1" @@ -76,12 +88,61 @@ dependencies = [ "roff", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + [[package]] name = "data-encoding" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "errno" version = "0.2.8" @@ -172,6 +233,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -188,6 +258,28 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "roff" version = "0.2.1" @@ -208,6 +300,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "shitbox" version = "0.1.0" @@ -221,8 +328,10 @@ dependencies = [ "hostname", "libc", "num_cpus", + "rayon", "termcolor", "textwrap", + "walkdir", ] [[package]] @@ -255,6 +364,17 @@ dependencies = [ "smawk", ] +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index a228fea..5ef0ee9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,10 @@ data-encoding = "2.3" hostname = { version = "0.3", features = ["set"] } libc = "0.2" num_cpus = "1.15" +rayon = "1.6.1" termcolor = "1.1" textwrap = { version = "0.16", default-features = false, features = ["smawk"] } +walkdir = "2.3.2" [profile.release] codegen-units = 1 diff --git a/src/cmd/chown/mod.rs b/src/cmd/chown/mod.rs new file mode 100644 index 0000000..c738fd9 --- /dev/null +++ b/src/cmd/chown/mod.rs @@ -0,0 +1,288 @@ +use super::Cmd; +use crate::{fs::FileType, pw}; +use clap::{Arg, ArgAction, ArgGroup, Command, ValueHint}; +use std::{error::Error, fs::{File, self}, io, path::PathBuf, os::{unix::prelude::MetadataExt, fd::AsRawFd}}; +use walkdir::{DirEntry, WalkDir}; + +#[derive(Debug, Default)] +pub struct Chown; + +impl Cmd for Chown { + fn cli(&self) -> clap::Command { + Command::new("chown") + .about("change file owner and group") + .author("Nathan Fisher") + .version(env!("CARGO_PKG_VERSION")) + .args([ + Arg::new("user") + .value_name("OWNER[:GROUP]") + .value_hint(ValueHint::Username) + .num_args(1) + .required(true), + Arg::new("changes") + .help("report only when a change is made") + .short('c') + .long("changes") + .conflicts_with("verbose") + .action(ArgAction::SetTrue), + Arg::new("verbose") + .help("output a diagnostic for every file processed") + .short('v') + .long("verbose") + .action(ArgAction::SetTrue), + Arg::new("recursive") + .help("operate on files and directories recursively") + .short('R') + .long("recursive") + .requires("links") + .action(ArgAction::SetTrue), + Arg::new("cli-traverse") + .help( + "if a command line argument is a symbolic link to a directory, traverse it", + ) + .short('H') + .action(ArgAction::SetTrue), + Arg::new("full-traverse") + .help("traverse every symbolic link encountered in a directory") + .short('L') + .action(ArgAction::SetTrue), + Arg::new("no-traverse") + .help("do not traverse any symbolic links (default)") + .short('P') + .action(ArgAction::SetTrue), + Arg::new("same-filesystem") + .help("do not cross filesystem boundaries (requires recursive)") + .short('s') + .requires("recursive") + .long("same-filesystem") + .action(ArgAction::SetTrue), + Arg::new("file") + .value_name("FILE") + .value_hint(ValueHint::AnyPath) + .num_args(1..) + .required(true), + ]) + .group( + ArgGroup::new("links") + .args(["cli-traverse", "full-traverse", "no-traverse"]) + .requires("recursive") + .multiple(false), + ) + } + + fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + let Some(matches) = matches else { + return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input"))); + }; + let recurse = if matches.get_flag("recursive") { + if matches.get_flag("full-traverse") { + Some(Recurse { + traversal: Traversal::FullLinks, + same_filesystem: matches.get_flag("same-filesystem") + }) + } else if matches.get_flag("cli-traverse") { + Some(Recurse { + traversal: Traversal::CliLinks, + same_filesystem: matches.get_flag("same-filesystem") + }) + } else { + Some(Recurse { + traversal: Traversal::NoLinks, + same_filesystem: matches.get_flag("same-filesystem") + }) + } + } else { + None + }; + let feedback = if matches.get_flag("verbose") { + Some(Feedback::Full) + } else if matches.get_flag("changes") { + Some(Feedback::Changes) + } else { + None + }; + let (user, group) = if let Some(who) = matches.get_one::("user") { + if let Some((u, g)) = who.split_once(':') { + let uid = pw::get_uid_for_name(u).ok_or(io::Error::new(io::ErrorKind::Other, "cannot get uid"))?; + let gid = pw::get_gid_for_groupname(g).ok_or(io::Error::new(io::ErrorKind::Other, "cannot get gid"))?; + (User { name: u.to_string(), uid }, Some(Group { name: g.to_string(), gid })) + } else { + let uid = pw::get_uid_for_name(who).ok_or(io::Error::new(io::ErrorKind::Other, "cannot get uid"))?; + (User { name: who.to_string(), uid }, None) + } + } else { + return Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "no user specified", + ))); + }; + if let Some(files) = matches.get_many::("file") { + for f in files { + let action = Action { + path: PathBuf::from(f), + user: user.clone(), + group: group.clone(), + recurse, + feedback, + }; + action.apply()?; + } + } + Ok(()) + } + + fn path(&self) -> Option { + Some(crate::Path::Bin) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum Traversal { + CliLinks, + FullLinks, + NoLinks, +} + +impl Traversal { + fn increment(&self) -> Self { + match self { + Self::CliLinks => Self::NoLinks, + _ => *self, + } + } +} + +#[derive(Clone, Copy, Debug)] +struct Recurse { + traversal: Traversal, + same_filesystem: bool, +} + +impl Recurse { + fn increment(&self) -> Self { + Self { + traversal: self.traversal.increment(), + same_filesystem: self.same_filesystem, + } + } +} + +#[derive(Clone, Copy, Debug)] +enum Feedback { + Full, + Changes, +} + +#[derive(Clone, Debug)] +struct User { + name: String, + uid: u32, +} + +#[derive(Clone, Debug)] +struct Group { + name: String, + gid: u32, +} + +#[derive(Debug)] +struct Action { + path: PathBuf, + user: User, + group: Option, + recurse: Option, + feedback: Option, +} + +impl Action { + fn apply(&self) -> Result<(), Box> { + let fd = File::open(&self.path)?; + let meta = fd.metadata()?; + let uid = meta.uid(); + let gid = meta.gid(); + unsafe { + if libc::fchown(fd.as_raw_fd(), self.user.uid, self.group.clone().map(|x| x.gid).unwrap_or(gid)) != 0 { + return Err(io::Error::last_os_error().into()); + } + } + let ft = FileType::from(meta); + match ft { + FileType::File => {}, + FileType::Symlink => { + let tgt = fs::read_link(&self.path)?; + if tgt.is_dir() { + if let Some(r) = self.recurse { + if r.traversal != Traversal::NoLinks { + self.recurse()?; + } + } + } + }, + FileType::Dir => { + if let Some(r) = self.recurse { + if r.traversal != Traversal::NoLinks { + self.recurse()?; + } + } + }, + } + if let Some(feedback) = self.feedback { + match feedback { + Feedback::Full => { + if self.user.uid != uid || self.group.clone().map(|x| x.gid) != Some(gid) { + if let Some(g) = &self.group { + println!("{} changed to {}:{}", &self.path.display(), &self.user.name, &g.name); + } else { + println!("{} changed to {}", &self.path.display(), &self.user.name); + } + } else { + if let Some(g) = &self.group { + println!("{} retained as {}:{}", &self.path.display(), &self.user.name, &g.name); + } else { + println!("{} retained as {}", &self.path.display(), &self.user.name); + } + } + } + Feedback::Changes => { + if self.user.uid != uid || self.group.clone().map(|x| x.gid) != Some(gid) { + if let Some(g) = &self.group { + println!("{} changed to {}:{}", self.path.display(), &self.user.name, &g.name); + } else { + println!("{}, changed to {}", self.path.display(), &self.user.name); + } + } + }, + } + } + Ok(()) + } + + fn into_child(&self, entry: DirEntry) -> Result> { + let path = entry.path().to_path_buf(); + let recurse = if let Some(r) = self.recurse { Some(r.increment()) } else { None }; + Ok(Self { + path, + user: self.user.clone(), + group: self.group.clone(), + recurse, + feedback: self.feedback, + }) + } + + fn recurse(&self) -> Result<(), Box> { + let walker = WalkDir::new(&self.path) + .same_file_system(self.recurse.map_or(false, |x| !x.same_filesystem)) + .follow_links(self.recurse.map_or(false, |x| { + match x.traversal { + Traversal::NoLinks | Traversal::CliLinks => false, + _ => true, + } + })); + for entry in walker { + let entry = entry?; + let action = self.into_child(entry)?; + action.apply()?; + } + Ok(()) + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 55da2c9..9de2b08 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -7,6 +7,7 @@ mod basename; mod bootstrap; mod cat; mod chmod; +mod chown; mod clear; mod cp; mod cut; @@ -44,10 +45,11 @@ mod yes; #[allow(clippy::module_name_repetitions)] pub use { self::hostname::Hostname, self::shitbox::Shitbox, base32::Base32, base64::Base64, - basename::Basename, bootstrap::Bootstrap, clear::Clear, cut::Cut, dirname::Dirname, echo::Echo, - factor::Factor, fold::Fold, groups::Groups, head::Head, link::Link, mountpoint::Mountpoint, - nologin::Nologin, nproc::Nproc, r#false::False, r#true::True, rev::Rev, rm::Rm, rmdir::Rmdir, - sleep::Sleep, sync::Sync as SyncCmd, unlink::Unlink, which::Which, whoami::Whoami, yes::Yes, + basename::Basename, bootstrap::Bootstrap, chown::Chown, clear::Clear, cut::Cut, + dirname::Dirname, echo::Echo, factor::Factor, fold::Fold, groups::Groups, head::Head, + link::Link, mountpoint::Mountpoint, nologin::Nologin, nproc::Nproc, r#false::False, + r#true::True, rev::Rev, rm::Rm, rmdir::Rmdir, sleep::Sleep, sync::Sync as SyncCmd, + unlink::Unlink, which::Which, whoami::Whoami, yes::Yes, }; /// Defines a command or applet, it's cli interface, and it's installation directory @@ -73,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())), + "chown" => Some(Box::new(chown::Chown::default())), "clear" => Some(Box::new(Clear::default())), "cut" => Some(Box::new(Cut::default())), "dirname" => Some(Box::new(Dirname::default())), @@ -101,11 +104,12 @@ pub fn get(name: &str) -> Option> { } } -pub static COMMANDS: [&str; 29] = [ +pub static COMMANDS: [&str; 30] = [ "base32", "base64", "basename", "bootstrap", + "chown", "clear", "cut", "dirname", diff --git a/src/cmd/rm/mod.rs b/src/cmd/rm/mod.rs index c90c059..3bf6b7d 100644 --- a/src/cmd/rm/mod.rs +++ b/src/cmd/rm/mod.rs @@ -1,9 +1,10 @@ use super::Cmd; +use crate::fs::FileType; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint}; use std::{ error::Error, fmt, - fs::{self, File, Metadata}, + fs::{self, File}, io, path::PathBuf, str::FromStr, @@ -142,39 +143,6 @@ impl FromStr for When { } } -enum Filetype { - File, - Dir, - Symlink, -} - -impl From for Filetype { - fn from(meta: Metadata) -> Self { - let ft = meta.file_type(); - if ft.is_dir() { - Self::Dir - } else if ft.is_symlink() { - Self::Symlink - } else { - Self::File - } - } -} - -impl fmt::Display for Filetype { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::File => "file", - Self::Symlink => "symlink", - Self::Dir => "directory", - } - ) - } -} - struct Action { path: String, prompt: When, @@ -200,7 +168,7 @@ impl Action { } else { match File::open(&self.path) .and_then(|fd| fd.metadata()) - .and_then(|meta| Ok(Filetype::from(meta))) + .and_then(|meta| Ok(FileType::from(meta))) { Ok(ft) => ft, Err(e) => { @@ -213,13 +181,13 @@ impl Action { } }; match ft { - Filetype::File | Filetype::Symlink => { + FileType::File | FileType::Symlink => { fs::remove_file(&self.path)?; if self.verbose { println!("removed '{}'", &self.path); } } - Filetype::Dir => { + FileType::Dir => { if self.recursive { match fs::read_dir(&self.path) { Ok(items) => { @@ -253,18 +221,18 @@ impl Action { Ok(()) } - fn prompt(&self) -> Result, Box> { + fn prompt(&self) -> Result, Box> { let path = PathBuf::from(&self.path); - let ft: Filetype = if path.is_dir() { + let ft: FileType = if path.is_dir() { if self.recursive { - return Ok(Some(Filetype::Dir)); + return Ok(Some(FileType::Dir)); } else { return Ok(None); } } else { File::open(path) .and_then(|fd| fd.metadata()) - .and_then(|meta| Ok(Filetype::from(meta)))? + .and_then(|meta| Ok(FileType::from(meta)))? }; eprint!("rm: remove {ft} '{}'? ", &self.path); let mut reply = String::new(); diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 0000000..7d140ad --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,60 @@ +use std::{ + fmt, + fs::{self, DirEntry, Metadata}, + io, +}; + +#[derive(Clone, Copy, Debug)] +pub enum FileType { + File, + Dir, + Symlink, +} + +impl From for FileType { + fn from(meta: Metadata) -> Self { + let ft = meta.file_type(); + if ft.is_dir() { + Self::Dir + } else if ft.is_symlink() { + Self::Symlink + } else { + Self::File + } + } +} + +impl From for FileType { + fn from(value: fs::FileType) -> Self { + if value.is_dir() { + Self::Dir + } else if value.is_file() { + Self::File + } else { + Self::Symlink + } + } +} + +impl TryFrom for FileType { + type Error = io::Error; + + fn try_from(value: DirEntry) -> Result { + let ft = value.file_type()?; + Ok(ft.into()) + } +} + +impl fmt::Display for FileType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::File => "file", + Self::Symlink => "symlink", + Self::Dir => "directory", + } + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index a21defb..589a8b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::{env, path::PathBuf, process, string::ToString}; mod cmd; pub use cmd::Cmd; +pub mod fs; pub mod math; pub mod mode; pub mod pw; diff --git a/src/pw/mod.rs b/src/pw/mod.rs index 30b8ade..9b5c203 100644 --- a/src/pw/mod.rs +++ b/src/pw/mod.rs @@ -31,6 +31,15 @@ pub fn get_eusername<'a>() -> Result<&'a str, std::str::Utf8Error> { user.to_str() } +pub fn get_username_for_uid<'a>(uid: u32) -> Result<&'a str, std::str::Utf8Error> { + let user = unsafe { + let pw = libc::getpwuid(uid); + let name = (*pw).pw_name; + CStr::from_ptr(name) + }; + user.to_str() +} + /// Gets the uid associated with the given name #[must_use] pub fn get_uid_for_name(name: &str) -> Option { @@ -72,6 +81,20 @@ pub fn get_grpname<'a>() -> Result<&'a str, std::str::Utf8Error> { group.to_str() } +pub fn get_gid_for_groupname(groupname: &str) -> Option { + let Ok(grp) = CString::new(groupname.as_bytes()) else { return None }; + unsafe { Some((*libc::getgrnam(grp.as_ptr())).gr_gid as u32) } +} + +pub fn get_groupname_for_gid<'a>(gid: u32) -> Result<&'a str, std::str::Utf8Error> { + let group = unsafe { + let gr = libc::getgrgid(gid); + let name = (*gr).gr_name; + CStr::from_ptr(name) + }; + group.to_str() +} + /// Gets the effective group for the current process /// # Errors /// The name must be valid utf8