From 9f9182f4cde6178de6658d637e198f72c4a1020c Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Tue, 3 Jan 2023 23:02:43 -0500 Subject: [PATCH] Add base64 applet --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/cmd/base64/mod.rs | 134 +++++++++++++++++++++++++++++++++++++++++ src/cmd/mod.rs | 1 + src/cmd/shitbox/mod.rs | 2 +- src/lib.rs | 7 ++- 6 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/cmd/base64/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 211d17b..b4b42ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "bitflags" version = "1.3.2" @@ -188,6 +194,7 @@ dependencies = [ name = "shitbox" version = "0.1.0" dependencies = [ + "base64", "clap", "clap_complete", "clap_complete_nushell", diff --git a/Cargo.toml b/Cargo.toml index 8d3d990..be3299c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +base64 = "0.20.0" clap = "4.0.29" clap_complete = "4.0.6" clap_complete_nushell = "0.1.8" diff --git a/src/cmd/base64/mod.rs b/src/cmd/base64/mod.rs new file mode 100644 index 0000000..6fbb25e --- /dev/null +++ b/src/cmd/base64/mod.rs @@ -0,0 +1,134 @@ +use super::Cmd; +use base64::{decode, encode}; +use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; +use std::{ + error::Error, + fs, + io::{self, Read}, +}; + +#[derive(Debug)] +pub struct Base64 { + name: &'static str, + path: Option, +} + +pub const BASE_64: Base64 = Base64 { + name: "base64", + path: Some(crate::Path::UsrBin), +}; + +impl Cmd for Base64 { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> Command { + Command::new("base64") + .author("Nathan Fisher") + .about("Base64 encode/decode data and print to standard output") + .args([ + Arg::new("INPUT") + .help("The input file to use") + .num_args(0..), + 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") + .value_parser(value_parser!(usize)), + 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<&ArgMatches>) -> Result<(), Box> { + let Some(matches) = matches else { + return Err(Box::new(io::Error::new(io::ErrorKind::Other, "No input"))); + }; + let files: Vec<_> = match matches.get_many::("INPUT") { + Some(c) => c.map(|x| x.clone()).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_base64(contents, matches.get_flag("IGNORE"))?; + } else { + encode_base64( + &contents, + match matches.get_one("WRAP") { + Some(c) => *c, + None => 76, + }, + ); + } + } + Ok(()) + } + + fn path(&self) -> Option { + self.path + } +} + +fn decode_base64(mut contents: String, ignore: bool) -> Result<(), Box> { + if ignore { + contents.retain(|c| !c.is_whitespace()); + } else { + contents = contents.replace('\n', ""); + } + let decoded = decode(&contents)?.to_vec(); + let output = String::from_utf8(decoded)?; + println!("{}", output.trim_end()); + Ok(()) +} + +fn encode_base64(contents: &str, wrap: usize) { + let encoded = 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) -> Result> { + let mut contents = String::new(); + if file == "-" { + io::stdin().read_to_string(&mut contents)?; + } else { + fs::read_to_string(&file)?; + } + Ok(contents) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 51be188..a3c5d3d 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -10,6 +10,7 @@ use std::{ }; pub mod base32; +pub mod base64; pub mod bootstrap; mod cat; mod chmod; diff --git a/src/cmd/shitbox/mod.rs b/src/cmd/shitbox/mod.rs index 640626e..86f108f 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, BASE_32}; +use super::{Cmd, BASE_32, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, NOLOGIN, SLEEP, TRUE}; use clap::Command; use std::{ error::Error, diff --git a/src/lib.rs b/src/lib.rs index 882d945..f0e6d7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,9 @@ 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, BASE_32}; +use cmd::{ + Cmd, Commands, BASE_32, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, NOLOGIN, SHITBOX, SLEEP, TRUE, +}; #[derive(Debug, Clone, Copy)] pub enum Path { @@ -37,7 +39,8 @@ pub fn run() -> Result<(), Box> { cmd::COMMANDS .set(Commands { items: vec![ - &BASE_32, &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, + &BASE_32, &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, + &SHITBOX, ], }) .expect("Cannot register commands");