From 80bc50428836fd4c09e5fe9b01437b6b43f83157 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Thu, 26 Mar 2026 23:55:19 -0400 Subject: [PATCH 1/3] Rename stdlib to coreutils Renamed the `stdlib` package to coreutils so that running any command with `--version` will report coreutils instead of stdlib. --- Cargo.toml | 4 +-- arch/Cargo.toml | 2 +- arch/src/main.rs | 2 +- b2sum/Cargo.toml | 2 +- b2sum/src/main.rs | 3 +- base32/Cargo.toml | 6 +++- base32/src/main.rs | 55 ++++++++++++++++++++++---------- {stdlib => coreutils}/Cargo.toml | 2 +- {stdlib => coreutils}/src/lib.rs | 0 9 files changed, 50 insertions(+), 26 deletions(-) rename {stdlib => coreutils}/Cargo.toml (82%) rename {stdlib => coreutils}/src/lib.rs (100%) 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/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..66d6f02 100644 --- a/base32/Cargo.toml +++ b/base32/Cargo.toml @@ -7,4 +7,8 @@ edition = "2021" [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..13405be 100644 --- a/base32/src/main.rs +++ b/base32/src/main.rs @@ -5,29 +5,50 @@ use std::io::ErrorKind; use std::process; use std::str; -use clap::Parser; +use clap::{arg, Arg, ArgAction}; +// use tabled::{builder::Builder, settings::Style}; -#[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, +clap_args!(Args { + flag decode: bool, + flag ignore_garbage: bool, + value(76) wrap: i32, + value("".to_string()) file: String, +}); - #[arg(short, long, default_value_t = 76)] - wrap: i32, +// #[derive(Parser, Debug)] +// #[command(author, version, about, long_about = None)] +// struct Args { +// /// decode data +// #[arg(short, long)] +// decode: bool, - /// accept a single filename - #[clap(default_value_t)] - file: String, -} +// /// when decoding, ignore non-alphabet characters +// #[arg(short, long)] +// ignore_garbage: bool, + +// #[arg(short, long, default_value_t = 76)] +// wrap: i32, + +// /// accept a single filename +// #[clap(default_value_t)] +// file: String, +// } fn main() { - let args = Args::parse(); + let matches = clap_base_command() + .arg(arg!(-d --decode "decode data")) + .arg(arg!(-i --ignore_garbage "ignore non-alphabet characters when decoding")) + .arg(arg!(-w --wrap "wrap output lines after LENGTH characters")) + .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); 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 From 4704bdbceedb09f7932e3332104b4b9f7bd8308b Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 28 Mar 2026 20:47:00 -0400 Subject: [PATCH 2/3] refactor: base32 Refactor's `base32` to use the shared `coreutils` library + general cleanup, better error handling, eetc. --- base32/Cargo.toml | 2 +- base32/src/main.rs | 196 ++++++++++++++++++++++++--------------------- 2 files changed, 107 insertions(+), 91 deletions(-) diff --git a/base32/Cargo.toml b/base32/Cargo.toml index 66d6f02..871ac47 100644 --- a/base32/Cargo.toml +++ b/base32/Cargo.toml @@ -1,7 +1,7 @@ [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 diff --git a/base32/src/main.rs b/base32/src/main.rs index 13405be..3363dad 100644 --- a/base32/src/main.rs +++ b/base32/src/main.rs @@ -1,46 +1,36 @@ 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::{arg, Arg, ArgAction}; -// use tabled::{builder::Builder, settings::Style}; +use clap::{Arg, ArgAction, arg}; +use serde_json::json; +use tabled::{builder::Builder, settings::Style, settings::Width}; use coreutils::{clap_args, clap_base_command}; +const ALPHABET: base32::Alphabet = base32::Alphabet::RFC4648 { padding: false }; +const DEFAULT_WRAP: i32 = 76; + clap_args!(Args { flag decode: bool, flag ignore_garbage: bool, - value(76) wrap: i32, - value("".to_string()) file: String, + value(DEFAULT_WRAP) wrap: i32, + maybe file: Option, }); -// #[derive(Parser, Debug)] -// #[command(author, version, about, long_about = None)] -// struct Args { -// /// decode data -// #[arg(short, long)] -// decode: bool, - -// /// when decoding, ignore non-alphabet characters -// #[arg(short, long)] -// ignore_garbage: bool, - -// #[arg(short, long, default_value_t = 76)] -// wrap: i32, - -// /// accept a single filename -// #[clap(default_value_t)] -// file: String, -// } - fn main() { let matches = clap_base_command() .arg(arg!(-d --decode "decode data")) - .arg(arg!(-i --ignore_garbage "ignore non-alphabet characters when decoding")) - .arg(arg!(-w --wrap "wrap output lines after LENGTH characters")) + .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) @@ -55,49 +45,65 @@ fn main() { 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. @@ -105,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) }; @@ -138,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)] @@ -178,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] @@ -189,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()); } } From 24c0aba84718467140434995d0941e6fd26c57d6 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Sat, 28 Mar 2026 20:50:09 -0400 Subject: [PATCH 3/3] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: |