diff --git a/Cargo.toml b/Cargo.toml index dc5eac2..eb14875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "dirname", "echo", "env", - "stdlib", + "coreutils", "wc", "whoami", ] @@ -21,4 +21,4 @@ clap = { version = "4.0", features = ["cargo", "derive"] } serde = { version = "1.0" } serde_json = { version = "1.0" } tabled = { version = "0.20" } -stdlib = { path = "./stdlib" } +coreutils = { path = "./coreutils" } diff --git a/README.md b/README.md index 8094451..38f4ae1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,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_large_square: | +| base32 | :white_check_mark: | | base64 | :white_large_square: | | basename | :white_large_square: | | cat | :white_large_square: | diff --git a/arch/Cargo.toml b/arch/Cargo.toml index 15fa592..30e5df9 100644 --- a/arch/Cargo.toml +++ b/arch/Cargo.toml @@ -10,5 +10,5 @@ platform-info = "1.0.1" clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -stdlib = { workspace = true } +coreutils = { workspace = true } tabled = { workspace = true } diff --git a/arch/src/main.rs b/arch/src/main.rs index 30fc7a4..d7625b1 100644 --- a/arch/src/main.rs +++ b/arch/src/main.rs @@ -2,7 +2,7 @@ use platform_info::*; use serde_json::json; use tabled::{builder::Builder, settings::Style}; -use stdlib::{clap_args, clap_base_command}; +use coreutils::{clap_args, clap_base_command}; clap_args!(Args {}); diff --git a/b2sum/Cargo.toml b/b2sum/Cargo.toml index 18dc4f3..7163a67 100644 --- a/b2sum/Cargo.toml +++ b/b2sum/Cargo.toml @@ -9,7 +9,7 @@ edition = "2024" blake2 = "0.10.4" clap = { workspace = true } shellexpand = "2.1.2" -stdlib = { workspace = true } +coreutils = { workspace = true } tabled = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/b2sum/src/main.rs b/b2sum/src/main.rs index fbe29ae..83aca17 100644 --- a/b2sum/src/main.rs +++ b/b2sum/src/main.rs @@ -8,10 +8,9 @@ use std::process; use blake2::{Blake2b512, Digest}; use clap::{Arg, ArgAction, arg}; -// use serde_json::{Map, Value, json}; use tabled::{builder::Builder, settings::Style}; -use stdlib::{clap_args, clap_base_command}; +use coreutils::{clap_args, clap_base_command}; clap_args!(Args { flag check: bool, diff --git a/base32/Cargo.toml b/base32/Cargo.toml index 9d67640..871ac47 100644 --- a/base32/Cargo.toml +++ b/base32/Cargo.toml @@ -1,10 +1,14 @@ [package] name = "base32" 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 } -base32 = "0.4.0" \ No newline at end of file +base32 = "0.4.0" +coreutils = { workspace = true } +tabled = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/base32/src/main.rs b/base32/src/main.rs index 97eeaa9..3363dad 100644 --- a/base32/src/main.rs +++ b/base32/src/main.rs @@ -1,82 +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 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: base32::Alphabet = base32::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 base32_file(args, args.file.to_string()) { - Err(why) => { - println!("base32: {why}"); - return 1; - } - Ok(hash) => hash, - }; - println!("{hash}"); +/// Compute the base32 hash of the input data +fn compute(args: &Args) -> Result { + if let Some(ref file) = args.file { + return base32_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_base32_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_base32_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 base32 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(["base32"]); + 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!({ + "base32": hash, + }); + println!("{}", serde_json::to_string(&output).unwrap()); + } + "yaml" => println!("base32: \"{hash}\""), + _ => println!("{}", wrap(args, &hash)), + } } - - let data = decode_base32_string(&buf); - - println!("{data}"); - } else { - output(args, encode_base32_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 != '=', ""); } /// Remove newlines embedded within the string, most likely from line wrapping. @@ -84,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 base32 of a file -fn base32_file(args: &Args, filename: String) -> Result { +fn base32_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!("base32: '{}': {}", filename, why), + ); + return Err(err_not_found); } Ok(buf) => buf.trim().to_string(), }; let data: String = if args.decode { - decode_base32_string(&buf) + decode_base32_string(&buf)? } else { encode_base32_string(&buf) }; @@ -117,36 +154,36 @@ fn base32_file(args: &Args, filename: String) -> Result { Ok(data) } -fn get_alphabet() -> base32::Alphabet { - let alpha: base32::Alphabet = base32::Alphabet::RFC4648 { padding: false }; - - alpha -} - -// Get the base32 of a String -fn encode_base32_string(str: &String) -> String { - let alpha = get_alphabet(); - base32::encode(alpha, str.as_bytes()) +/// Get the base32 of a String +fn encode_base32_string(str: &str) -> String { + base32::encode(ALPHABET, str.as_bytes()) } -fn decode_base32_string(str: &String) -> String { - println!("String: '{}'", str); - let alpha = get_alphabet(); - - let buf = match base32::decode(alpha, str) { - None => panic!("Got none!"), +/// Decode a base32 string into a String +fn decode_base32_string(str: &str) -> Result { + let buf = match base32::decode(ALPHABET, str) { + None => { + return Err(Error::new( + ErrorKind::InvalidData, + "base32: unable to decode", + )); + } Some(buf) => buf, }; // 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) => panic!("Error: {why}"), + Err(why) => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("base32: {}", why), + )); + } Ok(hash) => hash, }; - // let hash = str::from_utf8(&buf).unwrap(); - hash.to_string() + Ok(hash.to_string()) } #[cfg(test)] @@ -157,7 +194,7 @@ mod tests { fn test_base32() { let hello = String::from("hello, world"); let hash = encode_base32_string(&hello); - assert_eq!(hello, decode_base32_string(&hash)); + assert_eq!(hello, decode_base32_string(&hash).unwrap()); } #[test] @@ -168,6 +205,6 @@ mod tests { ); ignore_garbage(&mut input); - assert_eq!("hello, world", decode_base32_string(&input)); + assert_eq!("hello, world", decode_base32_string(&input).unwrap()); } } diff --git a/stdlib/Cargo.toml b/coreutils/Cargo.toml similarity index 82% rename from stdlib/Cargo.toml rename to coreutils/Cargo.toml index 85ae322..9f2fb2f 100644 --- a/stdlib/Cargo.toml +++ b/coreutils/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "stdlib" +name = "coreutils" version = "0.1.0" edition = "2024" diff --git a/stdlib/src/lib.rs b/coreutils/src/lib.rs similarity index 100% rename from stdlib/src/lib.rs rename to coreutils/src/lib.rs