diff --git a/src/chmod/mod.rs b/src/chmod/mod.rs new file mode 100644 index 0000000..1af9b14 --- /dev/null +++ b/src/chmod/mod.rs @@ -0,0 +1 @@ +//! Functions for parsing and managing permissions diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 9d7c909..bfea31a 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,44 +1,45 @@ use clap::ArgMatches; use std::{error::Error, fmt}; -pub mod base32; -pub mod base64; -pub mod basename; -pub mod bootstrap; +mod base32; +mod base64; +mod basename; +mod bootstrap; mod cat; mod chmod; -pub mod clear; +mod clear; mod cp; -pub mod cut; +mod cut; mod date; mod dd; -pub mod dirname; -pub mod echo; -pub mod factor; -pub mod r#false; -pub mod fold; +mod dirname; +mod echo; +mod factor; +mod r#false; +mod fold; mod getty; -pub mod groups; -pub mod head; -pub mod hostname; -pub mod link; +mod groups; +mod head; +mod hostname; +mod link; mod ln; mod ls; -pub mod mountpoint; +mod mountpoint; mod mv; -pub mod nologin; -pub mod nproc; +mod nologin; +mod nproc; mod pwd; -pub mod rev; +mod rev; mod rm; mod rmdir; -pub mod shitbox; -pub mod sleep; -pub mod sync; -pub mod r#true; -pub mod which; -pub mod whoami; -pub mod yes; +mod shitbox; +mod sleep; +mod sync; +mod r#true; +mod unlink; +mod which; +mod whoami; +mod yes; #[allow(clippy::module_name_repetitions)] pub use { @@ -46,15 +47,24 @@ pub use { 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, shitbox::Shitbox, sleep::Sleep, - sync::Sync as SyncCmd, which::Which, whoami::Whoami, yes::Yes, + 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 +/// relative to the binary pub trait Cmd: fmt::Debug + Sync { + /// Defines the cli of the applet fn cli(&self) -> clap::Command; + /// Runs the applet + /// # Errors + /// Bubbles up any errors to the caller fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box>; + /// Returns the path relative to the binary where the link to this applet + /// will be installed fn path(&self) -> Option; } +/// Parses a string into a command to run #[must_use] #[allow(clippy::box_default)] pub fn get(name: &str) -> Option> { @@ -81,6 +91,7 @@ pub fn get(name: &str) -> Option> { "sleep" => Some(Box::new(Sleep::default())), "sync" => Some(Box::new(SyncCmd::default())), "true" => Some(Box::new(True::default())), + "unlink" => Some(Box::new(Unlink::default())), "which" => Some(Box::new(Which::default())), "whoami" => Some(Box::new(Whoami::default())), "yes" => Some(Box::new(Yes::default())), @@ -88,7 +99,7 @@ pub fn get(name: &str) -> Option> { } } -pub static COMMANDS: [&str; 26] = [ +pub static COMMANDS: [&str; 27] = [ "base32", "base64", "basename", @@ -112,6 +123,7 @@ pub static COMMANDS: [&str; 26] = [ "shitbox", "sync", "true", + "unlink", "which", "whoami", "yes", diff --git a/src/cmd/unlink/mod.rs b/src/cmd/unlink/mod.rs new file mode 100644 index 0000000..09fb6b7 --- /dev/null +++ b/src/cmd/unlink/mod.rs @@ -0,0 +1,50 @@ +use clap::{Arg, ArgAction, Command}; +use std::{io, ffi::CString, process}; +use super::Cmd; + +#[derive(Debug, Default)] +pub struct Unlink; + +impl Cmd for Unlink { + fn cli(&self) -> clap::Command { + Command::new("unlink") + .about("call the unlink function to remove the specified file") + .author("Nathan Fisher") + .version(env!("CARGO_PKG_VERSION")) + .args([ + Arg::new("verbose") + .help("display user feedback upon success") + .short('v') + .long("verbose") + .action(ArgAction::SetTrue), + Arg::new("file") + .value_name("FILE") + .required(true) + .num_args(1..), + ]) + } + + 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"))); + }; + if let Some(files) = matches.get_many::("file") { + for f in files { + let fname = CString::new(f.as_str())?; + let ret = unsafe { libc::unlink(fname.as_ptr()) }; + if matches.get_flag("verbose") && ret == 0 { + println!("unlink: {f}"); + } else if ret != 0 { + let err = io::Error::last_os_error(); + eprintln!("unlink: cannot unlink {f}: {err}"); + process::exit(ret); + } + } + } + Ok(()) + } + + fn path(&self) -> Option { + Some(crate::Path::UsrBin) + } +} diff --git a/src/lib.rs b/src/lib.rs index b1ef164..3acdb72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,22 @@ #![warn(clippy::all, clippy::pedantic)] use std::{env, path::PathBuf, process, string::ToString}; -pub mod cmd; +pub mod chmod; +mod cmd; pub use cmd::Cmd; pub mod math; -/// User and group related functionality pub mod pw; +/// Defines the location relative to the binary where a command will be installed #[derive(Debug, Clone, Copy)] pub enum Path { + /// /bin Bin, + /// /sbin Sbin, + /// /usr/bin UsrBin, + /// /usr/sbin UsrSbin, } @@ -27,6 +32,7 @@ impl Path { } } +/// Returns the basename of the command as it was called #[must_use] pub fn progname() -> Option { env::args() @@ -39,6 +45,9 @@ pub fn progname() -> Option { .flatten() } +/// Returns the path to the binary, if called as "shitbox". Used in the bootstrap +/// applet to find the binary location in order to install it into it's permanent +/// location #[must_use] pub fn progpath() -> Option { match progname() { @@ -47,6 +56,7 @@ pub fn progpath() -> Option { } } +/// The entry point of the program pub fn run() { if let Some(progname) = progname() { if let Some(command) = cmd::get(&progname) { diff --git a/src/math.rs b/src/math/mod.rs similarity index 89% rename from src/math.rs rename to src/math/mod.rs index 0f3677c..852833f 100644 --- a/src/math.rs +++ b/src/math/mod.rs @@ -1,3 +1,4 @@ +//! Math related functions not included in std #[must_use] pub fn is_prime(num: u64) -> bool { match num { diff --git a/src/pw/mod.rs b/src/pw/mod.rs index 801653b..30b8ade 100644 --- a/src/pw/mod.rs +++ b/src/pw/mod.rs @@ -1,9 +1,13 @@ +//! Wraps certain libc functions around groups and users use std::{ error::Error, ffi::{c_int, CStr, CString}, io, num, }; +/// Gets the current username of this process +/// # Errors +/// The user name must be valid utf8 pub fn get_username<'a>() -> Result<&'a str, std::str::Utf8Error> { let user = unsafe { let uid = libc::getuid(); @@ -14,6 +18,9 @@ pub fn get_username<'a>() -> Result<&'a str, std::str::Utf8Error> { user.to_str() } +/// Gets the current effective user name of this process +/// # Errors +/// The user name must be valid utf8 pub fn get_eusername<'a>() -> Result<&'a str, std::str::Utf8Error> { let user = unsafe { let uid = libc::geteuid(); @@ -24,6 +31,7 @@ pub fn get_eusername<'a>() -> Result<&'a str, std::str::Utf8Error> { user.to_str() } +/// Gets the uid associated with the given name #[must_use] pub fn get_uid_for_name(name: &str) -> Option { let Ok(user) = CString::new(name.as_bytes()) else { return None }; @@ -37,6 +45,7 @@ pub fn get_uid_for_name(name: &str) -> Option { } } +/// Gets the gid for the main group associated with the given user #[must_use] pub fn get_pgid_for_name(name: &str) -> Option { let Ok(user) = CString::new(name.as_bytes()) else { return None }; @@ -50,6 +59,9 @@ pub fn get_pgid_for_name(name: &str) -> Option { } } +/// Gets the main group name for the current process +/// # Errors +/// The name must be valid utf8 pub fn get_grpname<'a>() -> Result<&'a str, std::str::Utf8Error> { let group = unsafe { let gid = libc::getgid(); @@ -60,6 +72,9 @@ pub fn get_grpname<'a>() -> Result<&'a str, std::str::Utf8Error> { group.to_str() } +/// Gets the effective group for the current process +/// # Errors +/// The name must be valid utf8 pub fn get_egrpname<'a>() -> Result<&'a str, std::str::Utf8Error> { let group = unsafe { let gid = libc::getegid();