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
556 changes: 556 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ license = "MIT"
anyhow = "1.0.100"
clap = { version = "4.5.53", features = ["derive"] }
csv = "1.4.0"
rand = "0.9.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
serde_yaml = "0.9.34"
toml = "0.9.8"
zxcvbn = "3.1.0"
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 process;

pub use opts::{Opts, SubCommand};
pub use process::process_csv;
pub use opts::{GenPassOpts, Opts, SubCommand};
pub use process::*;
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use clap::Parser;
use rcli::{Opts, SubCommand, process_csv};
use rcli::{Opts, SubCommand, process_csv, process_genpass};

// rcli csv -i input.csv -o output.json --header -d ','

Expand All @@ -9,6 +9,9 @@ fn main() -> anyhow::Result<()> {
SubCommand::Csv(opts) => {
process_csv(&opts)?;
}
SubCommand::GenPass(opts) => {
process_genpass(&opts)?;
}
}

Ok(())
Expand Down
21 changes: 21 additions & 0 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub struct Opts {
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),
}

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -75,6 +78,24 @@ pub struct CsvOpts {
pub delimiter: char,
}

#[derive(Debug, Parser)]
pub struct GenPassOpts {
#[arg(short, long, help = "Length of the password", default_value_t = 12)]
pub length: u8,

#[arg(long = "no-uppercase", help = "Exclude uppercase characters", default_value_t = true, action = clap::ArgAction::SetFalse)]
pub uppercase: bool,

#[arg(long = "no-lowercase", help = "Exclude lowercase characters", default_value_t = true, action = clap::ArgAction::SetFalse)]
pub lowercase: bool,

#[arg(long = "no-numbers", help = "Exclude numbers", default_value_t = true, action = clap::ArgAction::SetFalse)]
pub numbers: bool,

#[arg(long = "no-special", help = "Exclude special characters", default_value_t = true, action = clap::ArgAction::SetFalse)]
pub special: bool,
}

fn verify_input_file(filename: &str) -> Result<String, String> {
if Path::new(filename).exists() {
Ok(filename.to_string())
Expand Down
File renamed without changes.
54 changes: 54 additions & 0 deletions src/process/gen_pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use rand::{Rng, seq::SliceRandom};
use zxcvbn::zxcvbn;

use crate::opts::GenPassOpts;

const UPPER_CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ";
const LOWER_CHARSET: &[u8] = b"abcdefghijkmnopqrstuvwxyz";
const DIGIT_CHARSET: &[u8] = b"123456789";
const SYMBOL_CHARSET: &[u8] = b"!@#$%^&*()_";

pub fn process_genpass(opts: &GenPassOpts) -> anyhow::Result<()> {
println!("Generating password with length: {}", opts.length);
let mut password = Vec::with_capacity(opts.length as usize);
let mut chars = Vec::with_capacity(opts.length as usize);
let mut rng = rand::rng();

if opts.length < 6 {
anyhow::bail!("Password length must be at least 6");
}

// Ensure at least one character from each selected set is included
for (include, charset) in [
(opts.uppercase, UPPER_CHARSET),
(opts.lowercase, LOWER_CHARSET),
(opts.numbers, DIGIT_CHARSET),
(opts.special, SYMBOL_CHARSET),
] {
if include {
chars.extend_from_slice(charset);
password.push(charset[rng.random_range(0..charset.len())]);
}
}

if chars.is_empty() {
anyhow::bail!("At least one character type must be selected");
}

while password.len() < opts.length as usize {
let idx = rng.random_range(0..chars.len());
password.push(chars[idx]);
}

password.shuffle(&mut rng);

let password = String::from_utf8_lossy(&password);

println!("Generated password: {}", password);

let estimate = zxcvbn(&password, &[]);
// output password strength in stderr
eprintln!("Password strength: {}", estimate.score());

Ok(())
}
5 changes: 5 additions & 0 deletions src/process/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod csv;
mod gen_pass;

pub use csv::process_csv;
pub use gen_pass::process_genpass;