diff --git a/Cargo.lock b/Cargo.lock index c891613..a06f722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bit-set" version = "0.5.3" @@ -432,6 +438,7 @@ name = "rcli" version = "0.1.0" dependencies = [ "anyhow", + "base64", "clap", "csv", "rand", diff --git a/Cargo.toml b/Cargo.toml index eb47e1f..b13ae4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/fixtures/b64.txt b/fixtures/b64.txt new file mode 100644 index 0000000..df73588 --- /dev/null +++ b/fixtures/b64.txt @@ -0,0 +1 @@ +W3BhY2thZ2VdCm5hbWUgPSAicmNsaSIKdmVyc2lvbiA9ICIwLjEuMCIKYXV0aG9ycyA9IFsiVHlyIENoZW4gPHR5ci5jaGVuQGdtYWlsLmNvbT4iXQplZGl0aW9uID0gIjIwMjEiCmxpY2Vuc2UgPSAiTUlUIgoKIyBTZWUgbW9yZSBrZXlzIGFuZCB0aGVpciBkZWZpbml0aW9ucyBhdCBodHRwczovL2RvYy5ydXN0LWxhbmcub3JnL2NhcmdvL3JlZmVyZW5jZS9tYW5pZmVzdC5odG1sCgpbZGVwZW5kZW5jaWVzXQphbnlob3cgPSAiMS4wLjgxIgpiYXNlNjQgPSAiMC4yMi4wIgpjbGFwID0geyB2ZXJzaW9uID0gIjQuNS4zIiwgZmVhdHVyZXMgPSBbImRlcml2ZSJdIH0KY3N2ID0gIjEuMy4wIgpyYW5kID0gIjAuOC41IgpzZXJkZSA9IHsgdmVyc2lvbiA9ICIxLjAuMTk3IiwgZmVhdHVyZXMgPSBbImRlcml2ZSJdIH0Kc2VyZGVfanNvbiA9ICIxLjAuMTE0IgpzZXJkZV95YW1sID0gIjAuOS4zMyIKenhjdmJuID0gIjIuMi4yIgo diff --git a/src/cli/base64.rs b/src/cli/base64.rs new file mode 100644 index 0000000..d5a7bd6 --- /dev/null +++ b/src/cli/base64.rs @@ -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 { + format.parse() +} + +impl FromStr for Base64Format { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "standard" => Ok(Base64Format::Standard), + "urlsafe" => Ok(Base64Format::UrlSafe), + _ => Err(anyhow::anyhow!("Invalid format")), + } + } +} + +impl From 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)) + } +} diff --git a/src/opts.rs b/src/cli/csv.rs similarity index 56% rename from src/opts.rs rename to src/cli/csv.rs index f5f11a2..94e8640 100644 --- a/src/opts.rs +++ b/src/cli/csv.rs @@ -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 { @@ -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 { - if Path::new(filename).exists() { - Ok(filename.into()) - } else { - Err("File does not exist") - } -} - fn parse_format(format: &str) -> Result { format.parse() } diff --git a/src/cli/genpass.rs b/src/cli/genpass.rs new file mode 100644 index 0000000..d5f5f37 --- /dev/null +++ b/src/cli/genpass.rs @@ -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, +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..41f3fb9 --- /dev/null +++ b/src/cli/mod.rs @@ -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 { + // 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")); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8cda814..140d2e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::*; diff --git a/src/main.rs b/src/main.rs index d87813d..12c8442 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); @@ -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(()) diff --git a/src/process/b64.rs b/src/process/b64.rs new file mode 100644 index 0000000..a496adb --- /dev/null +++ b/src/process/b64.rs @@ -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> { + let reader: Box = 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(); + } +} diff --git a/src/process/csv_convert.rs b/src/process/csv_convert.rs index 8550779..a069676 100644 --- a/src/process/csv_convert.rs +++ b/src/process/csv_convert.rs @@ -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")] diff --git a/src/process/mod.rs b/src/process/mod.rs index 5cc9a4f..96e9fd7 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -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;