diff --git a/Cargo.lock b/Cargo.lock index 40208bd..3afe80c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[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 3efc2eb..26ce759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT" [dependencies] anyhow = "1.0.82" +base64 = "0.22.0" clap = { version = "4.5.4", features = ["derive"] } csv = "1.3.0" rand = "0.8.5" diff --git a/src/cli/base64_opts.rs b/src/cli/base64_opts.rs new file mode 100644 index 0000000..7b097bb --- /dev/null +++ b/src/cli/base64_opts.rs @@ -0,0 +1,69 @@ +use clap::Parser; +use std::{fmt::Display, str::FromStr}; + +use super::parse_input_file; +#[derive(Debug, Parser)] +pub struct Base64Opts { + #[command(subcommand)] + pub subcmd: Base64SubCommand, +} + +#[derive(Debug, Parser)] +pub enum Base64SubCommand { + #[command(name = "encode", about = "base64 encode")] + Encode(Base64EncodeOpts), + #[command(name = "decode", about = "base64 decode")] + Decode(Base64DecodeOpts), +} + +#[derive(Debug, Parser)] +pub struct Base64EncodeOpts { + #[arg(short, long, value_parser=parse_input_file, required = true, help = "input file path, or '-' for stdin")] + pub input: String, + #[arg(long, default_value = "standard", value_parser=Base64Format::from_str, help = "base64 format: [standard, urlsafe, nopadding]")] + pub format: Base64Format, +} + +#[derive(Debug, Parser)] +pub struct Base64DecodeOpts { + #[arg(short, long, value_parser=parse_input_file, help = "input file path, or '-' for stdin")] + pub input: String, + #[arg(long, value_parser=Base64Format::from_str, default_value = "standard", help = "base64 format: [standard, urlsafe, nopadding]")] + pub format: Base64Format, +} + +#[derive(Debug, Clone, Copy)] +pub enum Base64Format { + Standard, + UrlSafe, + NoPadding, +} + +impl FromStr for Base64Format { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "standard" => Ok(Base64Format::Standard), + "urlsafe" => Ok(Base64Format::UrlSafe), + "nopadding" => Ok(Base64Format::NoPadding), + v => Err(anyhow::anyhow!("invalid base64 format: {}", v)), + } + } +} + +impl From for &'static str { + fn from(f: Base64Format) -> Self { + match f { + Base64Format::Standard => "standard", + Base64Format::UrlSafe => "urlsafe", + Base64Format::NoPadding => "nopadding", + } + } +} + +impl Display for Base64Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Into::<&str>::into(*self)) + } +} diff --git a/src/cli/csv_opts.rs b/src/cli/csv_opts.rs new file mode 100644 index 0000000..40e5324 --- /dev/null +++ b/src/cli/csv_opts.rs @@ -0,0 +1,50 @@ +use super::parse_input_file; +use clap::Parser; +use std::{fmt::Display, str::FromStr}; + +#[derive(Debug, Parser)] +pub struct CsvOpts { + #[arg(long, default_value_t = true)] + pub header: bool, + #[arg(short, long, default_value_t = ',')] + pub delimiter: char, + #[arg(short, long, required = true, value_parser=parse_input_file)] + pub input: String, + #[arg(short, long)] // default_value_t = "output.json".into() + pub output: Option, + #[arg(long, default_value = "json", value_parser=OutputFormat::from_str)] + pub format: OutputFormat, +} + +#[derive(Debug, Clone, Copy)] +pub enum OutputFormat { + Json, + Yaml, +} + +impl FromStr for OutputFormat { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "json" => Ok(OutputFormat::Json), + "yaml" => Ok(OutputFormat::Yaml), + v => Err(anyhow::anyhow!("invalid output format: {}", v)), + } + } +} + +impl From for &'static str { + fn from(f: OutputFormat) -> Self { + match f { + OutputFormat::Json => "json", + OutputFormat::Yaml => "yaml", + } + } +} + +impl Display for OutputFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Into::<&str>::into(*self)) + } +} diff --git a/src/cli/genpass_opts.rs b/src/cli/genpass_opts.rs new file mode 100644 index 0000000..cb685be --- /dev/null +++ b/src/cli/genpass_opts.rs @@ -0,0 +1,14 @@ +use clap::Parser; +#[derive(Debug, Parser)] +pub struct GenpassOpts { + #[arg(long, default_value_t = false)] + pub no_upper: bool, + #[arg(long, default_value_t = false)] + pub no_lower: bool, + #[arg(long, default_value_t = false)] + pub no_number: bool, + #[arg(long, default_value_t = false)] + pub no_symbol: bool, + #[arg(short, long, default_value_t = 16)] + pub length: u8, +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..2f247b0 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,52 @@ +mod base64_opts; +mod csv_opts; +mod genpass_opts; + +use std::fs; + +pub use self::base64_opts::{Base64Format, Base64Opts, Base64SubCommand}; +pub use self::csv_opts::{CsvOpts, OutputFormat}; +pub use self::genpass_opts::GenpassOpts; +use clap::Parser; + +#[derive(Debug, Parser)] +#[command(name = "rcli", version, about, author, long_about=None)] +pub struct Opts { + #[command(subcommand)] + pub subcmd: SubCommand, +} + +#[derive(Debug, Parser)] +pub enum SubCommand { + // rcli csv --header xx -delimiter , -input /tmp/1.csv -output output.json + #[command(name = "csv", about = "csv file processor")] + Csv(CsvOpts), + // rcli genpass --upper xx --lower --symbol --number --length + #[command(name = "genpass", about = "generate password")] + Genpass(GenpassOpts), + // rcli base64 --encode/decode --output + #[command(name = "base64", about = "base64 encode/decode")] + Base64(Base64Opts), +} + +// 模块级别的函数,共享input file的解析逻辑 +fn parse_input_file(path: &str) -> Result { + if path == "-" || fs::metadata(path).is_ok() { + Ok(path.to_string()) + } else { + Err(format!("file not found: {}", path)) + } +} + +// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_input_file() { + assert_eq!(parse_input_file("-"), Ok("-".to_string())); + assert_eq!(parse_input_file("*"), Err("file not found: *".to_string())); + assert_eq!(parse_input_file("Cargo.toml"), Ok("Cargo.toml".to_string())); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0240e33..53c1681 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -mod opts; +mod cli; mod process; -pub use opts::{CsvOpts, Opts, SubCommand}; -pub use process::{process_csv, process_genpass}; +pub use cli::{Base64SubCommand, Opts, SubCommand}; +pub use process::{process_b64decode, process_b64encode, process_csv, process_genpass}; diff --git a/src/main.rs b/src/main.rs index c7767c9..c8da891 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ -// usage: -// 使用 rcli -- csv --input input.csv --output output.csv --format json -// 使用 rcli -- genpass 进行密码生成 -mod opts; +mod cli; mod process; - use clap::Parser; -use opts::{Opts, SubCommand}; -use process::{process_csv, process_genpass}; +use cli::{Base64SubCommand, Opts, SubCommand}; +use process::{process_b64decode, process_b64encode, process_csv, process_genpass}; + +// usage: +// 使用 rcli -- csv --input input.csv --output output.csv --format json +// 使用 rcli -- genpass 进行密码生成 +// 使用 rcli -- base64 --encode/--decode --format 进行base64编码/解码 fn main() -> anyhow::Result<()> { let cli = Opts::parse(); match cli.subcmd { @@ -26,6 +27,15 @@ fn main() -> anyhow::Result<()> { opts.length, )?; } + // Todo: implement base64 subcommand + SubCommand::Base64(base64_opts) => match base64_opts.subcmd { + Base64SubCommand::Encode(opts) => { + process_b64encode(&opts.input, opts.format)?; + } + Base64SubCommand::Decode(opts) => { + process_b64decode(&opts.input, opts.format)?; + } + }, } Ok(()) } diff --git a/src/opts.rs b/src/opts.rs deleted file mode 100644 index 619e0b1..0000000 --- a/src/opts.rs +++ /dev/null @@ -1,88 +0,0 @@ -use clap::Parser; -use std::{fmt, fs, str::FromStr}; - -#[derive(Debug, Parser)] -#[command(name = "rcli", version, about, author, long_about=None)] -pub struct Opts { - #[command(subcommand)] - pub subcmd: SubCommand, -} - -#[derive(Debug, Parser)] -pub enum SubCommand { - // rcli csv --header xx -delimiter , -input /tmp/1.csv -output output.json - #[command(name = "csv", about = "csv file processor")] - Csv(CsvOpts), - // rcli genpass --upper xx --lower --symbol --number --length - #[command(name = "genpass", about = "generate password")] - Genpass(GenpassOpts), -} - -#[derive(Debug, Parser)] -pub struct CsvOpts { - #[arg(long, default_value_t = true)] - pub header: bool, - #[arg(short, long, default_value_t = ',')] - pub delimiter: char, - #[arg(short, long, required = true, value_parser(verify_file_exists))] - pub input: String, - #[arg(short, long)] // default_value_t = "output.json".into() - pub output: Option, - #[arg(long, default_value = "json", value_parser=OutputFormat::from_str)] - pub format: OutputFormat, -} - -#[derive(Debug, Parser)] -pub struct GenpassOpts { - #[arg(long, default_value_t = false)] - pub no_upper: bool, - #[arg(long, default_value_t = false)] - pub no_lower: bool, - #[arg(long, default_value_t = false)] - pub no_number: bool, - #[arg(long, default_value_t = false)] - pub no_symbol: bool, - #[arg(short, long, default_value_t = 16)] - pub length: u8, -} - -#[derive(Debug, Clone, Copy)] -pub enum OutputFormat { - Json, - Yaml, -} - -fn verify_file_exists(path: &str) -> Result { - if fs::metadata(path).is_ok() { - Ok(path.to_string()) - } else { - Err(format!("file not found: {}", path)) - } -} - -impl FromStr for OutputFormat { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "json" => Ok(OutputFormat::Json), - "yaml" => Ok(OutputFormat::Yaml), - v => Err(anyhow::anyhow!("invalid output format: {}", v)), - } - } -} - -impl From for &'static str { - fn from(f: OutputFormat) -> Self { - match f { - OutputFormat::Json => "json", - OutputFormat::Yaml => "yaml", - } - } -} - -impl fmt::Display for OutputFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", Into::<&str>::into(*self)) - } -} diff --git a/src/process/base64_processor.rs b/src/process/base64_processor.rs new file mode 100644 index 0000000..283fd77 --- /dev/null +++ b/src/process/base64_processor.rs @@ -0,0 +1,42 @@ +use std::io::Read; + +use crate::cli::Base64Format; +use base64::prelude::*; + +// input 可以是 '-', 表示从 stdin 读取,或者是文件路径, 输出到 stdout, 返回一个 reader +fn get_reader(input: &str) -> anyhow::Result> { + match input { + "-" => Ok(Box::new(std::io::stdin())), + path => Ok(Box::new(std::fs::File::open(path)?)), + } +} + +// base64 encoder, +pub fn process_encode(input: &str, format: Base64Format) -> anyhow::Result<()> { + let mut reader = get_reader(input)?; + // 读取所有的数据 + let mut data = Vec::new(); + reader.read_to_end(&mut data)?; + let encoded = match format { + Base64Format::Standard => BASE64_STANDARD.encode(&data), + Base64Format::UrlSafe => BASE64_URL_SAFE.encode(&data), + Base64Format::NoPadding => BASE64_URL_SAFE_NO_PAD.encode(&data), + }; + println!("{}", encoded); + Ok(()) +} + +pub fn process_decode(input: &str, format: Base64Format) -> anyhow::Result<()> { + let mut reader = get_reader(input)?; + // 读取所有的数据 + let mut data = String::new(); + reader.read_to_string(&mut data)?; + let data = data.trim_end(); + let decode = match format { + Base64Format::Standard => BASE64_STANDARD.decode(data)?, + Base64Format::UrlSafe => BASE64_URL_SAFE.decode(data)?, + Base64Format::NoPadding => BASE64_URL_SAFE_NO_PAD.decode(data)?, + }; + println!("{}", String::from_utf8(decode)?); + Ok(()) +} diff --git a/src/process/csv_processor.rs b/src/process/csv_processor.rs index 170976f..993eb54 100644 --- a/src/process/csv_processor.rs +++ b/src/process/csv_processor.rs @@ -2,7 +2,7 @@ use std::fs; use csv::Reader; -use crate::opts::OutputFormat; +use crate::cli::OutputFormat; // 将 csv 文件转换为 json 或者 yaml 格式 pub fn process(input: &str, output: String, output_format: OutputFormat) -> anyhow::Result<()> { diff --git a/src/process/pass_generator.rs b/src/process/genpass_processor.rs similarity index 100% rename from src/process/pass_generator.rs rename to src/process/genpass_processor.rs diff --git a/src/process/mod.rs b/src/process/mod.rs index e203be3..542de99 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,5 +1,8 @@ +mod base64_processor; mod csv_processor; -mod pass_generator; +mod genpass_processor; +pub use base64_processor::process_decode as process_b64decode; +pub use base64_processor::process_encode as process_b64encode; pub use csv_processor::process as process_csv; -pub use pass_generator::process as process_genpass; +pub use genpass_processor::process as process_genpass;