diff --git a/README.md b/README.md index 38f4ae1..b83528d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This began as, and continues to be, a learning exercise to better understand the | arch | :white_check_mark: | | b2sum | :white_check_mark: | | base32 | :white_check_mark: | -| base64 | :white_large_square: | +| base64 | :white_check_mark: | | basename | :white_large_square: | | cat | :white_large_square: | | chcon | :white_large_square: | diff --git a/base64/Cargo.toml b/base64/Cargo.toml index 715954c..9c2e57b 100644 --- a/base64/Cargo.toml +++ b/base64/Cargo.toml @@ -1,10 +1,14 @@ [package] name = "base64" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { workspace = true } -base64 = "0.13.1" \ No newline at end of file +base64 = "0.13.1" +coreutils = { workspace = true } +tabled = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/base64/src/main.rs b/base64/src/main.rs index 41a3a44..af7f67c 100644 --- a/base64/src/main.rs +++ b/base64/src/main.rs @@ -1,84 +1,109 @@ use std::fs::read_to_string; use std::io; use std::io::prelude::*; -use std::io::ErrorKind; +use std::io::{Error, ErrorKind}; use std::process; use std::str; -use base64::{decode, encode}; -use clap::Parser; +use clap::{Arg, ArgAction, arg}; +use serde_json::json; +use tabled::{builder::Builder, settings::Style, settings::Width}; -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// decode data - #[arg(short, long)] - decode: bool, +use coreutils::{clap_args, clap_base_command}; - /// when decoding, ignore non-alphabet characters - #[arg(short, long)] - ignore_garbage: bool, +// const ALPHABET: base64::Alphabet = base64::Alphabet::RFC4648 { padding: false }; +const DEFAULT_WRAP: i32 = 76; - #[arg(short, long, default_value_t = 76)] - wrap: i32, - - /// accept a single filename - #[clap(default_value_t)] - file: String, -} +clap_args!(Args { + flag decode: bool, + flag ignore_garbage: bool, + value(DEFAULT_WRAP) wrap: i32, + maybe file: Option, +}); fn main() { - let args = Args::parse(); + let matches = clap_base_command() + .arg(arg!(-d --decode "decode data")) + .arg( + Arg::new("ignore_garbage") + .long("ignore-garbage") + .action(ArgAction::SetTrue) + .help("ignore non-alphabet characters when decoding"), + ) + .arg(arg!(-w --wrap "wrap output lines after LENGTH characters (plain, table)")) + .arg( + Arg::new("file") + .action(ArgAction::Set) + .help("the name of the file to read from"), + ) + .get_matches(); + + let args = Args::from_matches(&matches); let retval = run(&args); process::exit(retval); } -fn run(args: &Args) -> i32 { - let retval = 0; - - if !args.file.is_empty() { - let hash = match base64_file(args, args.file.to_string()) { - Err(why) => { - println!("base64: {why}"); - return 1; - } - Ok(hash) => hash, - }; - println!("{hash}"); +/// Compute the base64 hash of the input data +fn compute(args: &Args) -> Result { + if let Some(ref file) = args.file { + return base64_file(args, file); + } + let mut buf = String::new(); + io::stdin().lock().read_to_string(&mut buf).unwrap(); + buf = buf.trim().to_string(); + if args.decode { + remove_newlines(&mut buf); + if args.ignore_garbage { + ignore_garbage(&mut buf); + } + decode_base64_string(&buf) } else { - let stdin = io::stdin(); - let mut buf = String::new(); - - // Slurp the data from stdin - stdin.lock().read_to_string(&mut buf).unwrap(); - - // Trim the whitespace. We've got a trailing newline - buf = buf.trim().to_string(); + Ok(encode_base64_string(&buf)) + } +} - if args.decode { - // Remove the newlines from the wrapped string - remove_newlines(&mut buf); - if args.ignore_garbage { - ignore_garbage(&mut buf); +/// Run the base64 command with the given arguments +fn run(args: &Args) -> i32 { + match compute(args) { + Ok(hash) => { + if let Some(output) = &args.output { + match output.as_str() { + "table" => { + let mut builder = Builder::new(); + builder.push_column(["base64"]); + builder.push_record([hash]); + let mut table = builder.build(); + println!( + "{}", + table + .with(Style::rounded()) + .with(Width::wrap(get_wrap(args) as usize)) + ); + } + "json" => { + let output = json!({ + "base64": hash, + }); + println!("{}", serde_json::to_string(&output).unwrap()); + } + "yaml" => println!("base64: \"{hash}\""), + _ => println!("{}", wrap(args, &hash)), + } } - - let data = decode_base64_string(&buf); - - println!("{data}"); - } else { - output(args, encode_base64_string(&buf)); + 0 + } + Err(why) => { + eprintln!("{}", why); + 1 } } - - retval } /// Ignore non-alphabet characters fn ignore_garbage(s: &mut String) { - *s = str::replace(s, |c: char| !c.is_alphanumeric(), ""); - // *s = str::replace(s, |c: char| !c.is_alphanumeric() && c != '=', ""); + *s = str::replace(s, |c: char| !c.is_alphanumeric() && c != '=', ""); } /// Remove newlines embedded within the string, most likely from line wrapping. @@ -86,32 +111,42 @@ fn remove_newlines(s: &mut String) { s.retain(|c| c != '\n'); } -/// Output the string with wrapping -fn output(args: &Args, data: String) { +fn get_wrap(args: &Args) -> i32 { + if args.wrap > 0 { + args.wrap + } else { + DEFAULT_WRAP + } +} + +/// Wrap the hash into multiple lines +fn wrap(args: &Args, data: &str) -> String { // https://users.rust-lang.org/t/solved-how-to-split-string-into-multiple-sub-strings-with-given-length/10542/3 let lines = data .as_bytes() - .chunks(args.wrap as usize) + .chunks(get_wrap(args) as usize) .map(str::from_utf8) .collect::, _>>() .unwrap(); - for line in lines { - println!("{line}"); - } + lines.join("\n") } /// Get the base64 of a file -fn base64_file(args: &Args, filename: String) -> Result { +fn base64_file(args: &Args, filename: &str) -> Result { let buf = match read_to_string(filename) { Err(why) => { - return Err(why.kind()); + let err_not_found = Error::new( + ErrorKind::NotFound, + format!("base64: '{}': {}", filename, why), + ); + return Err(err_not_found); } Ok(buf) => buf.trim().to_string(), }; let data: String = if args.decode { - decode_base64_string(&buf) + decode_base64_string(&buf)? } else { encode_base64_string(&buf) }; @@ -119,26 +154,36 @@ fn base64_file(args: &Args, filename: String) -> Result { Ok(data) } -// fn get_alphabet() -> base64::Alphabet { -// let alpha: base64::Alphabet = base64::Alphabet::RFC4648 { padding: false }; - -// alpha -// } - -// Get the base64 of a String -fn encode_base64_string(str: &String) -> String { - // let alpha = get_alphabet(); - // base64::encode(alpha, str.as_bytes()) - encode(str) +/// Get the base64 of a String +fn encode_base64_string(str: &str) -> String { + base64::encode(str.as_bytes()) } -fn decode_base64_string(str: &String) -> String { - let buf = &decode(str).unwrap()[..]; +/// Decode a base64 string into a String +fn decode_base64_string(str: &str) -> Result { + let buf = match base64::decode(str) { + Err(why) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("base64: {}", why), + )); + } + Ok(buf) => buf, + }; - match str::from_utf8(buf) { - Ok(v) => v.to_string(), - Err(e) => panic!("Invalid UTF-8 sequence: {}", e), - } + // after we've stripped garbage from a string, this might fail so we need + // error checking + let hash = match str::from_utf8(&buf) { + Err(why) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("base64: {}", why), + )); + } + Ok(hash) => hash, + }; + + Ok(hash.to_string()) } #[cfg(test)] @@ -149,19 +194,17 @@ mod tests { fn test_base64() { let hello = String::from("hello, world"); let hash = encode_base64_string(&hello); - assert_eq!(hello, decode_base64_string(&hash)); + assert_eq!(hello, decode_base64_string(&hash).unwrap()); } #[test] fn test_ignore_garbage() { let mut input = String::from( - "aGVsbG8s - IHdvcmxk - ", + "aGVsbG8sI + Hdvcmxk", ); ignore_garbage(&mut input); - println!("input: '{}'", input); - assert_eq!("hello, world", decode_base64_string(&input)); + assert_eq!("hello, world", decode_base64_string(&input).unwrap()); } }