diff --git a/Cargo.lock b/Cargo.lock index 550927e..211d17b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ dependencies = [ "roff", ] +[[package]] +name = "data-encoding" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + [[package]] name = "errno" version = "0.2.8" @@ -186,6 +192,7 @@ dependencies = [ "clap_complete", "clap_complete_nushell", "clap_mangen", + "data-encoding", "hostname", "once_cell", ] diff --git a/Cargo.toml b/Cargo.toml index e6dc5df..4a4af54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ clap = "4.0.29" clap_complete = "4.0.6" clap_complete_nushell = "0.1.8" clap_mangen = "0.2.5" +data-encoding = "2.3.3" hostname = { version = "0.3", features = ["set"] } once_cell = "1.16.0" diff --git a/src/cmd/base32/mod.rs b/src/cmd/base32/mod.rs new file mode 100644 index 0000000..6692ef7 --- /dev/null +++ b/src/cmd/base32/mod.rs @@ -0,0 +1,161 @@ +use super::Cmd; +use clap::{Arg, ArgAction, Command}; +use data_encoding::BASE32; +use std::{ + fs, + io::{self, Read}, + process, +}; + +#[derive(Debug)] +pub struct Base32 { + name: &'static str, + path: Option, +} + +pub const BASE_32: Base32 = Base32 { + name: "base32", + path: Some(crate::Path::UsrBin), +}; + +impl Cmd for Base32 { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new("base32") + .version(env!("CARGO_PKG_VERSION")) + .author("The JeanG3nie ") + .about("Base32 encode/decode data and print to standard output") + .args([ + Arg::new("INPUT") + .help("The input file to use") + .num_args(1..), + Arg::new("DECODE") + .help("Decode rather than encode") + .short('d') + .long("decode") + .action(ArgAction::SetTrue), + Arg::new("IGNORE") + .help("Ignore whitespace when decoding") + .short('i') + .long("ignore-space") + .action(ArgAction::SetTrue), + Arg::new("WRAP") + .help("Wrap encoded lines after n characters") + .short('w') + .long("wrap") + .default_value("76"), + Arg::new("VERBOSE") + .help("Display a header naming each file") + .short('v') + .long("verbose") + .action(ArgAction::SetTrue), + Arg::new("QUIET") + .help("Do not display header, even with multiple files") + .short('q') + .long("quiet") + .action(ArgAction::SetTrue), + ]) + } + + fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + let matches = match matches { + Some(m) => m, + None => return Err(io::Error::new(io::ErrorKind::Other, "No input").into()), + }; + let files: Vec<_> = match matches.get_many::("INPUT") { + Some(c) => c.map(|x| x.to_owned()).collect(), + None => vec![String::from("-")], + }; + let len = files.len(); + for (index, file) in files.into_iter().enumerate() { + if { len > 1 || matches.get_flag("VERBOSE") } && !matches.get_flag("QUIET") { + match index { + 0 => println!("===> {} <===", file), + _ => println!("\n===> {} <===", file), + }; + } else if index > 0 { + println!(); + } + let contents = get_contents(&file); + if matches.get_flag("DECODE") { + decode_base32(contents, matches.get_flag("IGNORE")); + } else { + encode_base32( + &contents, + match matches.get_one::("WRAP") { + Some(c) => *c, + None => { + return Err(io::Error::new(io::ErrorKind::Other, "Invalid wrap").into()) + } + }, + ); + } + } + Ok(()) + } + + fn path(&self) -> Option { + self.path + } +} + +fn decode_base32(mut contents: String, ignore: bool) { + if ignore { + contents.retain(|c| !c.is_whitespace()); + } else { + contents = contents.replace('\n', ""); + } + let decoded = match BASE32.decode(contents.as_bytes()) { + Ok(c) => c, + Err(e) => { + eprintln!("base32: {}", e); + process::exit(1); + } + }; + let output = match String::from_utf8(decoded) { + Ok(c) => c, + Err(e) => { + eprintln!("base32: {}", e); + process::exit(1); + } + }; + println!("{}", output.trim_end()); +} + +fn encode_base32(contents: &str, wrap: usize) { + let encoded = BASE32 + .encode(contents.as_bytes()) + .chars() + .collect::>() + .chunks(wrap) + .map(|c| c.iter().collect::()) + .collect::>(); + for line in &encoded { + println!("{}", line); + } +} + +fn get_contents(file: &str) -> String { + let mut contents = String::new(); + if file == "-" { + match io::stdin().read_to_string(&mut contents) { + Ok(_) => true, + Err(e) => { + eprintln!("base32: {}", e); + process::exit(1); + } + }; + } else { + contents = match fs::read_to_string(&file) { + Ok(c) => c, + Err(e) => { + eprintln!("base32: {}", e); + process::exit(1); + } + }; + } + contents +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 9f41f0c..51be188 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -9,6 +9,7 @@ use std::{ path::{Path, PathBuf}, }; +pub mod base32; pub mod bootstrap; mod cat; mod chmod; @@ -35,6 +36,7 @@ pub mod r#true; pub use { self::hostname::{Hostname, HOSTNAME}, + base32::{Base32, BASE_32}, bootstrap::{Bootstrap, BOOTSTRAP}, echo::{Echo, ECHO}, head::{Head, HEAD}, diff --git a/src/cmd/shitbox/mod.rs b/src/cmd/shitbox/mod.rs index 029547e..640626e 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, NOLOGIN, SLEEP, TRUE}; +use super::{Cmd, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, NOLOGIN, SLEEP, TRUE, BASE_32}; use clap::Command; use std::{ error::Error, @@ -27,6 +27,7 @@ impl Cmd for Shitbox { .version(env!("CARGO_PKG_VERSION")) .arg_required_else_help(true) .subcommands([ + BASE_32.cli(), BOOTSTRAP.cli(), ECHO.cli(), FALSE.cli(), @@ -45,6 +46,7 @@ impl Cmd for Shitbox { return Err(Box::new(io::Error::new(ErrorKind::Other, "No input"))); }; match matches.subcommand() { + Some(("base32", matches)) => BASE_32.run(Some(matches))?, Some(("bootstrap", matches)) => BOOTSTRAP.run(Some(matches))?, Some(("echo", _matches)) => ECHO.run(None)?, Some(("false", _matches)) => FALSE.run(None)?, diff --git a/src/lib.rs b/src/lib.rs index 1629b13..882d945 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, Commands, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, NOLOGIN, SHITBOX, SLEEP, TRUE}; +use cmd::{Cmd, Commands, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, NOLOGIN, SHITBOX, SLEEP, TRUE, BASE_32}; #[derive(Debug, Clone, Copy)] pub enum Path { @@ -37,13 +37,14 @@ pub fn run() -> Result<(), Box> { cmd::COMMANDS .set(Commands { items: vec![ - &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, + &BASE_32, &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, ], }) .expect("Cannot register commands"); } if let Some(progname) = progname() { match progname.as_str() { + "base32" => BASE_32.run(Some(&BASE_32.cli().get_matches()))?, "echo" => ECHO.run(None)?, "false" => FALSE.run(None)?, "head" => {