Added README.md, CONTRIBUTING.md and LICENSE. Added /sbin/nologin

applet.
This commit is contained in:
Nathan Fisher 2022-12-25 22:27:17 -05:00
parent 1af14e7ff7
commit cf99f2005a
8 changed files with 297 additions and 14 deletions

71
CONTRIBUTING.md Normal file
View File

@ -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<dyn std::error::Error>> {
println!("If there's anything more important than my ego around, I want it caught and shot now.");
Ok(())
}
fn path(&self) -> Option<crate::Path> {
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.

28
LICENSE Normal file
View File

@ -0,0 +1,28 @@
-----------------------------------------------------------------------------
"THE BEER-WARE LICENSE" (Revision 42):
<jeang3nie@HitchHiker-Linux.org> 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" @@
`; ___ _ @
`;. ,====\\=. .;'
``""""`==\\=='
`;=====
===

48
README.md Normal file
View File

@ -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

View File

@ -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::<String>("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<dyn Error>> {
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()

View File

@ -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(())

35
src/cmd/nologin/mod.rs Normal file
View File

@ -0,0 +1,35 @@
use clap::Command;
use std::process;
use super::Cmd;
pub struct Nologin {
name: &'static str,
path: Option<crate::Path>,
}
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<dyn std::error::Error>> {
eprintln!("I'm sorry, I can't let you do that, Dave");
process::exit(42);
}
fn path(&self) -> Option<crate::Path> {
self.path
}
}

View File

@ -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)?,

View File

@ -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<String> {
.flatten()
}
#[must_use]
pub fn progpath() -> Option<PathBuf> {
match progname() {
Some(s) if s == "shitbox" => env::args().next().map(PathBuf::from),
_ => None,
}
}
pub fn run() -> Result<(), Box<dyn Error>> {
if let Some(progname) = progname() {
match progname.as_str() {
@ -33,6 +41,7 @@ pub fn run() -> Result<(), Box<dyn Error>> {
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()))?;