Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
8 changes: 6 additions & 2 deletions base64/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
base64 = "0.13.1"
coreutils = { workspace = true }
tabled = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
217 changes: 130 additions & 87 deletions base64/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,144 +1,189 @@
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<String>,
});

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 <LENGTH> "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<String, Error> {
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.
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::<Result<Vec<&str>, _>>()
.unwrap();

for line in lines {
println!("{line}");
}
lines.join("\n")
}

/// Get the base64 of a file
fn base64_file(args: &Args, filename: String) -> Result<String, ErrorKind> {
fn base64_file(args: &Args, filename: &str) -> Result<String, Error> {
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)
};

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<String, Error> {
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)]
Expand All @@ -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());
}
}
Loading