From cf99f2005a8e77be8a1428a43b2b9471915e3134 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Sun, 25 Dec 2022 22:27:17 -0500 Subject: [PATCH] Added README.md, CONTRIBUTING.md and LICENSE. Added /sbin/nologin applet. --- CONTRIBUTING.md | 71 +++++++++++++++++++++++++++++ LICENSE | 28 ++++++++++++ README.md | 48 ++++++++++++++++++++ src/cmd/bootstrap/mod.rs | 98 ++++++++++++++++++++++++++++++++++++++-- src/cmd/mod.rs | 15 +++--- src/cmd/nologin/mod.rs | 35 ++++++++++++++ src/cmd/shitbox/mod.rs | 5 +- src/lib.rs | 11 ++++- 8 files changed, 297 insertions(+), 14 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/cmd/nologin/mod.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..878de7b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +Contents +======== +* [The **Cmd** trait](#the-cmd-trait) +* [A simple example applet](#a-simple-example-applet) +* [Incorporating a new applet](#incorporating-a-new-applet) + +## The Cmd trait +The `Cmd` trait is defined in `src/cmd/mod.rs`. All applets should live in their +own submodule under `crate::cmd` and include a struct with the name of the command, +in snake case. This struct must implement `Cmd`. It is recommended that this struct +have at least the fields `name: &'static str` and `path: crate::Path`. The +trait methods `name` and `path` can then just return the corresponding fields. + +The applet module should further contain a constant which is basically the +default instance for this struct. + +## A Simple Example Applet +```Rust +// src/cmd/myapplet/mod.rs +use clap::Command; +use super::Cmd; + +pub struct MyApplet { + name: &'static str, + path: crate::Path, +} + +pub const MYAPPLET: MyApplet = MyApplet { + name: "myapplet", + path: crate::Path::UsrBin, +}; + +impl Cmd for MyApplet { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .version(env!("CARGO_PKG_VERSION")) + .author("Zaphod Beeblebrox") + .about("Does sketchy things") + } + + fn run(&self, _matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + println!("If there's anything more important than my ego around, I want it caught and shot now."); + Ok(()) + } + + fn path(&self) -> Option { + self.path + } +} +``` +## Incorporating a new applet +Each command module should be public in `src/cmd/mod.rs` and both the struct which +implements `Cmd` and the constant which is an instance of that struct should be +exported as public in that file. + +There are several other files which must also be edited to fully integrate a new +command. Expect improvements to this process as the Api evolves. + +- src/lib.rs: The function `run` has a match statement which checks the name with +which the program was invoked. A new match arm must be added here. +- src/cmd/bootstrap/mod.rs: The `run` method has a `Vec` of trait objects representing +all of the available applets. The constant which is an instance of the struct implementing +`Cmd` should be added to this `Vec`. +-src/cmd/shitbox/mod.rs: The `cli` method will need `MYAPPLET.cli()` added in to +the `clap` subcommands. The `run` method has a match statement which checks the +subcommand that has been asked to run. A new match arm must also be added here. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e3f79d --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ + ----------------------------------------------------------------------------- + "THE BEER-WARE LICENSE" (Revision 42): + wrote this program. As long as you retain + this notice you can do whatever you want with this stuff. If we meet some day, + and you think this stuff is worth it, you can buy me a beer in return. + ----------------------------------------------------------------------------- + + 88888b d888b 888b 88 8P 888888 88888b 888 888b 88 88 d888b 88 + 88 88 88 88 88`8b 88 88 88 88 88 88 88`8b 88 88 88 ` 88 + 88 88 88 88 88 88 88 88 88888P 88 88 88 88 88 88 88 88 + 88 88 88 88 88 `8b88 88 88 d8888888b 88 `8b88 88 88 , "" + 88888P `888P 88 `888 88 88 88 `8b 88 `888 88 `888P 88 + + nnnmmm + \||\ ;;;;%%%@@@@@@ \ //, + V|/ %;;%%%%%@@@@@@@@@@ ===Y// + 68=== ;;;;%%%%%%@@@@@@@@@@@@ @Y + ;Y ;;%;%%%%%%@@@@@@@@@@@@@@ Y + ;Y ;;;+;%%%%%%@@@@@@@@@@@@@@@ Y + ;Y__;;;+;%%%%%%@@@@@@@@@@@@@@i;;__Y + iiY"";; "uu%@@@@@@@@@@uu" @"";;;> + Y "UUUUUUUUU" @@ + `; ___ _ @ + `;. ,====\\=. .;' + ``""""`==\\==' + `;===== + === + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f91523 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +Contents +======== +* [Introduction](#introduction) +* [Scope](#scope) +* [Installation](#installation) + +## Introduction +*Shitbox* is inspired by the project [busybox](https://busybox.net/) but with a +much more limited scope. While Busybox aims to be "*The swiss army knife of +embedded linux*" you can think of shitbox as being more like "*The Harbor Freight +multi tool of embedded linux*". + +All joking aside the utilities which are present function mostly as expected and +the code aims to be robust. It's written in Rust, not C, for whatever that's +worth. Like Busybox it is a multi-call binary which is therefore able to share +code between applets, making for an overall smaller binary. + +## Scope +*Shitbox* does not aim to supply an entire system of utilities, but rather a +a subset of the most common Unix shell utilities. Things which are out of scope +for the project include: +- Shells +- Network servers +- Kernel module handling utilities +The code aims to be portable across **Unix** variants, ie Linux and BSD, but not +MacOS or Windows. Development occurs on Linux, so if your OS is more exotic then +YMMV. + +## Installation +Building is done using the official Rust toolchain. It is recommended that you +install your toolchain using Rustup rather than distro packages, as old compiler +versions are not supported. +```Sh +cargo build --release +``` +The `bootstrap` applet provides facility for installing the binary, creating all +required symlinks and installing some nice to haves such as **Unix man pages** +and **shell completions** [see below]. +```Sh +target/release/shitbox help bootstrap +``` +### Supported shells for completions +- Bash +- Fish +- NuShell +- PowerShell +- Zsh + diff --git a/src/cmd/bootstrap/mod.rs b/src/cmd/bootstrap/mod.rs index ecd77d1..b669421 100644 --- a/src/cmd/bootstrap/mod.rs +++ b/src/cmd/bootstrap/mod.rs @@ -1,4 +1,4 @@ -use super::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE}; +use super::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE, NOLOGIN}; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_complete::{generate_to, shells, Generator}; use clap_complete_nushell::Nushell; @@ -48,16 +48,59 @@ impl Cmd for Bootstrap { .short('u') .long("usr") .action(ArgAction::SetTrue), + Arg::new("soft") + .help("Install soft links instead of hardlinks") + .short('s') + .long("soft") + .action(ArgAction::SetTrue), ]) .subcommands([ - Command::new("all").about("Install everything"), + Command::new("all").about("Install everything") + .args([ + Arg::new("soft") + .help("Install soft links instead of hardlinks") + .short('s') + .long("soft") + .action(ArgAction::SetTrue), + Arg::new("all") + .help("Install completions for all supported shells") + .short('a') + .long("all") + .action(ArgAction::SetTrue), + Arg::new("bash") + .help("Bash shell completions") + .short('b') + .long("bash") + .action(ArgAction::SetTrue), + Arg::new("fish") + .help("Fish shell completions") + .short('f') + .long("fish") + .action(ArgAction::SetTrue), + Arg::new("nu") + .help("Nushell completions") + .short('n') + .long("nu") + .action(ArgAction::SetTrue), + Arg::new("pwsh") + .help("PowerShell completions") + .short('p') + .long("pwsh") + .action(ArgAction::SetTrue), + Arg::new("zsh") + .help("Zshell completions") + .short('z') + .long("zsh") + .action(ArgAction::SetTrue), + ]), Command::new("links") .about("Install links for each applet") .arg( Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') - .long("soft"), + .long("soft") + .action(ArgAction::SetTrue), ), Command::new("manpages") .about("Install Unix man pages") @@ -91,6 +134,11 @@ impl Cmd for Bootstrap { .short('p') .long("pwsh") .action(ArgAction::SetTrue), + Arg::new("zsh") + .help("Zshell completions") + .short('z') + .long("zsh") + .action(ArgAction::SetTrue), ]), ]) } @@ -104,10 +152,22 @@ impl Cmd for Bootstrap { if let Some(prefix) = matches.get_one::("prefix") { let commands: Commands = Commands { items: vec![ - &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &TRUE, &SLEEP, &SHITBOX, + &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, ], }; let usr = matches.get_flag("usr"); + if let Some(progpath) = crate::progpath() { + let mut outpath = PathBuf::from(prefix); + outpath.push("bin"); + println!("Installing binary:"); + if !outpath.exists() { + fs::create_dir_all(&outpath)?; + println!(" mkdir: {}", outpath.display()); + } + outpath.push(env!("CARGO_PKG_NAME")); + fs::copy(&progpath, &outpath)?; + println!(" install: {} -> {}", progpath.display(), outpath.display()); + } match matches.subcommand() { Some(("links", matches)) => { commands.links(prefix, usr, matches)?; @@ -118,7 +178,8 @@ impl Cmd for Bootstrap { Some(("completions", matches)) => { commands.completions(prefix, matches)?; } - Some(("all", _matches)) => { + Some(("all", matches)) => { + commands.links(prefix, usr, matches)?; commands.manpages(prefix)?; commands.completions(prefix, matches)?; } @@ -216,6 +277,33 @@ impl<'a> Commands<'a> { fn links(&self, prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box> { println!("Generating links:"); + let mut binpath = PathBuf::from(prefix); + binpath.push("bin"); + if !binpath.exists() { + fs::create_dir_all(&binpath)?; + println!(" mkdir: {}", binpath.display()); + } + binpath.pop(); + binpath.push("sbin"); + if !binpath.exists() { + fs::create_dir_all(&binpath)?; + println!(" mkdir: {}", binpath.display()); + } + if usr { + binpath.pop(); + binpath.push("usr"); + binpath.push("bin"); + if !binpath.exists() { + fs::create_dir_all(&binpath)?; + println!(" mkdir: {}", binpath.display()); + } + binpath.pop(); + binpath.push("sbin"); + if !binpath.exists() { + fs::create_dir_all(&binpath)?; + println!(" mkdir: {}", binpath.display()); + } + } let soft = cmd.get_flag("soft"); self.items .iter() diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index af7664b..a1e74cf 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -16,6 +16,7 @@ mod ln; mod ls; mod mountpoint; mod mv; +pub mod nologin; mod pwd; mod rm; mod rmdir; @@ -28,8 +29,9 @@ pub use { self::hostname::{Hostname, HOSTNAME}, bootstrap::{Bootstrap, BOOTSTRAP}, echo::{Echo, ECHO}, - head::{Head, HEAD}, r#false::{False, FALSE}, + head::{Head, HEAD}, + nologin::{Nologin, NOLOGIN}, r#true::{True, TRUE}, shitbox::{Shitbox, SHITBOX}, sleep::{Sleep, SLEEP}, @@ -85,15 +87,14 @@ pub trait Cmd { } } }; - symlink(binpath, linkpath)?; + symlink(&binpath, &linkpath)?; + println!(" symlink: {} -> {}", binpath, linkpath.display()); } else { let mut binpath = PathBuf::from(prefix); - if usr { - binpath.push("usr"); - } binpath.push("bin"); - binpath.push("shitbox"); - fs::hard_link(binpath, linkpath)?; + binpath.push(env!("CARGO_PKG_NAME")); + fs::hard_link(&binpath, &linkpath)?; + println!(" link: {} -> {}", binpath.display(), linkpath.display()); } } Ok(()) diff --git a/src/cmd/nologin/mod.rs b/src/cmd/nologin/mod.rs new file mode 100644 index 0000000..a82a9cf --- /dev/null +++ b/src/cmd/nologin/mod.rs @@ -0,0 +1,35 @@ +use clap::Command; +use std::process; +use super::Cmd; + +pub struct Nologin { + name: &'static str, + path: Option, +} + +pub const NOLOGIN: Nologin = Nologin { + name: "nologin", + path: Some(crate::Path::Sbin), +}; + +impl Cmd for Nologin { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .version(env!("CARGO_PKG_VERSION")) + .author("Nathan Fisher") + .about("Denies a user account login ability") + } + + fn run(&self, _matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + eprintln!("I'm sorry, I can't let you do that, Dave"); + process::exit(42); + } + + fn path(&self) -> Option { + self.path + } +} diff --git a/src/cmd/shitbox/mod.rs b/src/cmd/shitbox/mod.rs index 4411bf0..5685e83 100644 --- a/src/cmd/shitbox/mod.rs +++ b/src/cmd/shitbox/mod.rs @@ -1,4 +1,4 @@ -use super::{Cmd, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, SLEEP, TRUE}; +use super::{Cmd, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, SLEEP, TRUE, NOLOGIN}; use clap::Command; use std::{ error::Error, @@ -30,6 +30,7 @@ impl Cmd for Shitbox { ECHO.cli(), FALSE.cli(), HEAD.cli(), + NOLOGIN.cli(), HOSTNAME.cli(), SLEEP.cli(), TRUE.cli(), @@ -43,9 +44,11 @@ impl Cmd for Shitbox { return Err(Box::new(io::Error::new(ErrorKind::Other, "No input"))); }; match matches.subcommand() { + Some(("bootstrap", matches)) => BOOTSTRAP.run(Some(matches))?, Some(("echo", _matches)) => ECHO.run(None)?, Some(("false", _matches)) => FALSE.run(None)?, Some(("head", matches)) => HEAD.run(Some(matches))?, + Some(("nologin", _matches)) => NOLOGIN.run(None)?, Some(("hostname", matches)) => HOSTNAME.run(Some(matches))?, Some(("sleep", matches)) => SLEEP.run(Some(matches))?, Some(("true", _matches)) => TRUE.run(None)?, diff --git a/src/lib.rs b/src/lib.rs index 80ac1dd..5fb2f03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ use std::{env, error::Error, path::PathBuf, string::ToString}; pub mod cmd; -use cmd::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE}; +use cmd::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE, NOLOGIN}; #[derive(Debug, Clone, Copy)] pub enum Path { @@ -24,6 +24,14 @@ pub fn progname() -> Option { .flatten() } +#[must_use] +pub fn progpath() -> Option { + match progname() { + Some(s) if s == "shitbox" => env::args().next().map(PathBuf::from), + _ => None, + } +} + pub fn run() -> Result<(), Box> { if let Some(progname) = progname() { match progname.as_str() { @@ -33,6 +41,7 @@ pub fn run() -> Result<(), Box> { HEAD.run(Some(&HEAD.cli().get_matches()))?; } "hostname" => HOSTNAME.run(Some(&HOSTNAME.cli().get_matches()))?, + "nologin" => NOLOGIN.run(None)?, "true" => TRUE.run(None)?, "shitbox" => { SHITBOX.run(Some(&SHITBOX.cli().get_matches()))?;