Skip to content

Commit

Permalink
feature: support base64 encode/decode cli
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrchen committed Mar 24, 2024
1 parent d7cecba commit bbccc7d
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 46 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license = "MIT"

[dependencies]
anyhow = "1.0.81"
base64 = "0.22.0"
clap = { version = "4.5.3", features = ["derive"] }
csv = "1.3.0"
rand = "0.8.5"
Expand Down
1 change: 1 addition & 0 deletions fixtures/b64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
W3BhY2thZ2VdCm5hbWUgPSAicmNsaSIKdmVyc2lvbiA9ICIwLjEuMCIKYXV0aG9ycyA9IFsiVHlyIENoZW4gPHR5ci5jaGVuQGdtYWlsLmNvbT4iXQplZGl0aW9uID0gIjIwMjEiCmxpY2Vuc2UgPSAiTUlUIgoKIyBTZWUgbW9yZSBrZXlzIGFuZCB0aGVpciBkZWZpbml0aW9ucyBhdCBodHRwczovL2RvYy5ydXN0LWxhbmcub3JnL2NhcmdvL3JlZmVyZW5jZS9tYW5pZmVzdC5odG1sCgpbZGVwZW5kZW5jaWVzXQphbnlob3cgPSAiMS4wLjgxIgpiYXNlNjQgPSAiMC4yMi4wIgpjbGFwID0geyB2ZXJzaW9uID0gIjQuNS4zIiwgZmVhdHVyZXMgPSBbImRlcml2ZSJdIH0KY3N2ID0gIjEuMy4wIgpyYW5kID0gIjAuOC41IgpzZXJkZSA9IHsgdmVyc2lvbiA9ICIxLjAuMTk3IiwgZmVhdHVyZXMgPSBbImRlcml2ZSJdIH0Kc2VyZGVfanNvbiA9ICIxLjAuMTE0IgpzZXJkZV95YW1sID0gIjAuOS4zMyIKenhjdmJuID0gIjIuMi4yIgo
66 changes: 66 additions & 0 deletions src/cli/base64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::{fmt, str::FromStr};

use clap::Parser;

use super::verify_input_file;

#[derive(Debug, Parser)]
pub enum Base64SubCommand {
#[command(name = "encode", about = "Encode a string to base64")]
Encode(Base64EncodeOpts),
#[command(name = "decode", about = "Decode a base64 string")]
Decode(Base64DecodeOpts),
}

#[derive(Debug, Parser)]
pub struct Base64EncodeOpts {
#[arg(short, long, value_parser = verify_input_file, default_value = "-")]
pub input: String,
#[arg(long, value_parser = parse_base64_format, default_value = "standard")]
pub format: Base64Format,
}

#[derive(Debug, Parser)]
pub struct Base64DecodeOpts {
#[arg(short, long, value_parser = verify_input_file, default_value = "-")]
pub input: String,
#[arg(long, value_parser = parse_base64_format, default_value = "standard")]
pub format: Base64Format,
}

#[derive(Debug, Clone, Copy)]
pub enum Base64Format {
Standard,
UrlSafe,
}

fn parse_base64_format(format: &str) -> Result<Base64Format, anyhow::Error> {
format.parse()
}

impl FromStr for Base64Format {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"standard" => Ok(Base64Format::Standard),
"urlsafe" => Ok(Base64Format::UrlSafe),
_ => Err(anyhow::anyhow!("Invalid format")),
}
}
}

impl From<Base64Format> for &'static str {
fn from(format: Base64Format) -> Self {
match format {
Base64Format::Standard => "standard",
Base64Format::UrlSafe => "urlsafe",
}
}
}

impl fmt::Display for Base64Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Into::<&str>::into(*self))
}
}
44 changes: 2 additions & 42 deletions src/opts.rs → src/cli/csv.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
use super::verify_input_file;
use clap::Parser;
use std::{fmt, path::Path, str::FromStr};

#[derive(Debug, Parser)]
#[command(name = "rcli", version, author, about, long_about = None)]
pub struct Opts {
#[command(subcommand)]
pub cmd: SubCommand,
}

#[derive(Debug, Parser)]
pub enum SubCommand {
#[command(name = "csv", about = "Show CSV, or convert CSV to other formats")]
Csv(CsvOpts),
#[command(name = "genpass", about = "Generate a random password")]
GenPass(GenPassOpts),
}
use std::{fmt, str::FromStr};

#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
Expand All @@ -40,32 +26,6 @@ pub struct CsvOpts {
pub header: bool,
}

#[derive(Debug, Parser)]
pub struct GenPassOpts {
#[arg(short, long, default_value_t = 16)]
pub length: u8,

#[arg(long, default_value_t = true)]
pub uppercase: bool,

#[arg(long, default_value_t = true)]
pub lowercase: bool,

#[arg(long, default_value_t = true)]
pub number: bool,

#[arg(long, default_value_t = true)]
pub symbol: bool,
}

fn verify_input_file(filename: &str) -> Result<String, &'static str> {
if Path::new(filename).exists() {
Ok(filename.into())
} else {
Err("File does not exist")
}
}

fn parse_format(format: &str) -> Result<OutputFormat, anyhow::Error> {
format.parse()
}
Expand Down
19 changes: 19 additions & 0 deletions src/cli/genpass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use clap::Parser;

#[derive(Debug, Parser)]
pub struct GenPassOpts {
#[arg(short, long, default_value_t = 16)]
pub length: u8,

#[arg(long, default_value_t = true)]
pub uppercase: bool,

#[arg(long, default_value_t = true)]
pub lowercase: bool,

#[arg(long, default_value_t = true)]
pub number: bool,

#[arg(long, default_value_t = true)]
pub symbol: bool,
}
52 changes: 52 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
mod base64;
mod csv;
mod genpass;

use std::path::Path;

use self::{csv::CsvOpts, genpass::GenPassOpts};
use clap::Parser;

pub use self::{
base64::{Base64Format, Base64SubCommand},
csv::OutputFormat,
};

#[derive(Debug, Parser)]
#[command(name = "rcli", version, author, about, long_about = None)]
pub struct Opts {
#[command(subcommand)]
pub cmd: SubCommand,
}

#[derive(Debug, Parser)]
pub enum SubCommand {
#[command(name = "csv", about = "Show CSV, or convert CSV to other formats")]
Csv(CsvOpts),
#[command(name = "genpass", about = "Generate a random password")]
GenPass(GenPassOpts),
#[command(subcommand)]
Base64(Base64SubCommand),
}

fn verify_input_file(filename: &str) -> Result<String, &'static str> {
// if input is "-" or file exists
if filename == "-" || Path::new(filename).exists() {
Ok(filename.into())
} else {
Err("File does not exist")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_verify_input_file() {
assert_eq!(verify_input_file("-"), Ok("-".into()));
assert_eq!(verify_input_file("*"), Err("File does not exist"));
assert_eq!(verify_input_file("Cargo.toml"), Ok("Cargo.toml".into()));
assert_eq!(verify_input_file("not-exist"), Err("File does not exist"));
}
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod opts;
mod cli;
mod process;

pub use opts::{Opts, SubCommand};
pub use cli::{Base64Format, Base64SubCommand, Opts, SubCommand};
pub use process::*;
13 changes: 12 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// rcli csv -i input.csv -o output.json --header -d ','

use clap::Parser;
use rcli::{process_csv, process_genpass, Opts, SubCommand};
use rcli::{
process_csv, process_decode, process_encode, process_genpass, Base64SubCommand, Opts,
SubCommand,
};

fn main() -> anyhow::Result<()> {
let opts = Opts::parse();
Expand All @@ -23,6 +26,14 @@ fn main() -> anyhow::Result<()> {
opts.symbol,
)?;
}
SubCommand::Base64(subcmd) => match subcmd {
Base64SubCommand::Encode(opts) => {
process_encode(&opts.input, opts.format)?;
}
Base64SubCommand::Decode(opts) => {
process_decode(&opts.input, opts.format)?;
}
},
}

Ok(())
Expand Down
64 changes: 64 additions & 0 deletions src/process/b64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::Base64Format;
use anyhow::Result;
use base64::{
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
Engine as _,
};
use std::{fs::File, io::Read};

pub fn process_encode(input: &str, format: Base64Format) -> Result<()> {
let mut reader = get_reader(input)?;
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
let encoded = match format {
Base64Format::Standard => STANDARD.encode(&buf),
Base64Format::UrlSafe => URL_SAFE_NO_PAD.encode(&buf),
};
println!("{}", encoded);
Ok(())
}

pub fn process_decode(input: &str, format: Base64Format) -> Result<()> {
let mut reader = get_reader(input)?;
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
// avoid accidental newlines
let buf = buf.trim();

let decoded = match format {
Base64Format::Standard => STANDARD.decode(buf)?,
Base64Format::UrlSafe => URL_SAFE_NO_PAD.decode(buf)?,
};
// TODO: decoded data might not be string (but for this example, we assume it is)
let decoded = String::from_utf8(decoded)?;
println!("{}", decoded);
Ok(())
}

fn get_reader(input: &str) -> Result<Box<dyn Read>> {
let reader: Box<dyn Read> = if input == "-" {
Box::new(std::io::stdin())
} else {
Box::new(File::open(input)?)
};
Ok(reader)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_process_encode() {
let input = "Cargo.toml";
let format = Base64Format::Standard;
assert!(process_encode(input, format).is_ok());
}

#[test]
fn test_process_decode() {
let input = "fixtures/b64.txt";
let format = Base64Format::UrlSafe;
process_decode(input, format).unwrap();
}
}
2 changes: 1 addition & 1 deletion src/process/csv_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use csv::Reader;
use serde::{Deserialize, Serialize};
use std::fs;

use crate::opts::OutputFormat;
use crate::cli::OutputFormat;

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
Expand Down
2 changes: 2 additions & 0 deletions src/process/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod b64;
mod csv_convert;
mod gen_pass;

pub use b64::{process_decode, process_encode};
pub use csv_convert::process_csv;
pub use gen_pass::process_genpass;

0 comments on commit bbccc7d

Please sign in to comment.