|  | @@ -2,43 +2,69 @@
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    generate passwords in useful ways
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -usage: pwgen -c [number of phrases, default=1]
 | 
	
		
			
				|  |  | -      pwgen [len default=20] [number of phrases, default=1]     //no conjunctions
 | 
	
		
			
				|  |  | +usage: pwgen [options] [number of phrases, default=1]
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  |  */
 | 
	
		
			
				|  |  |  use std::env;
 | 
	
		
			
				|  |  | -use std::cmp;
 | 
	
		
			
				|  |  |  use std::fs::File;
 | 
	
		
			
				|  |  |  use std::io::{BufReader, BufRead};
 | 
	
		
			
				|  |  |  use rand::Rng;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +#[derive(Debug)]  
 | 
	
		
			
				|  |  | +#[derive(PartialEq)]
 | 
	
		
			
				|  |  | +enum Padding {
 | 
	
		
			
				|  |  | +  Camel, Snake,
 | 
	
		
			
				|  |  | +  Dots, Spaces,
 | 
	
		
			
				|  |  | +  Punct, Numbers
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  #[derive(Debug)]
 | 
	
		
			
				|  |  |  struct Settings {
 | 
	
		
			
				|  |  | +  first_punct: bool,  last_punct: bool,
 | 
	
		
			
				|  |  | +  first_number: bool, last_number: bool,
 | 
	
		
			
				|  |  | +  capitalize: bool,
 | 
	
		
			
				|  |  | +  use_conjunctions: bool,
 | 
	
		
			
				|  |  |    target_len: usize,
 | 
	
		
			
				|  |  |    collecting_parameter: bool,
 | 
	
		
			
				|  |  | -  use_conjunction: bool,
 | 
	
		
			
				|  |  |    number: u16,
 | 
	
		
			
				|  |  | +  padding: Padding,
 | 
	
		
			
				|  |  |    help: bool,
 | 
	
		
			
				|  |  | -  pad_chars: String,
 | 
	
		
			
				|  |  | -  // debug: bool,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -const CONJUNCTIONS: &[&str] = &["and", "and a", "and the","or", 
 | 
	
		
			
				|  |  | +const DOTS:    &str   = ".,";
 | 
	
		
			
				|  |  | +const PUNCTS:  &str   = " !@#$%^&*()-_=+[{]}\\|;:/?.>,<~";
 | 
	
		
			
				|  |  | +const NUMBERS: &str   = "0123456789";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const CONJUNCTIONS : &[&str] = &["and", "and a", "and the","or", 
 | 
	
		
			
				|  |  |    "or the", "or a", "with a", "with", "with the", "by a", "by the",
 | 
	
		
			
				|  |  |     "on a", "on the","on", "in a", "in the", "in",
 | 
	
		
			
				|  |  |     "for", "for a", "for the", "but", "so"];
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +fn get_word(set:&Settings, words:&Vec<String>) -> String {
 | 
	
		
			
				|  |  | +  let mut rng = rand::thread_rng();
 | 
	
		
			
				|  |  | +  let m = &words[rng.gen_range(0 .. words.len())];
 | 
	
		
			
				|  |  | +  if set.capitalize || set.padding==Padding::Camel {
 | 
	
		
			
				|  |  | +    // m = m.capitalize();  from https://stackoverflow.com/questions/38406793/
 | 
	
		
			
				|  |  | +    let mut c = m.chars();
 | 
	
		
			
				|  |  | +    match c.next() {
 | 
	
		
			
				|  |  | +      None => String::new(),
 | 
	
		
			
				|  |  | +      Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    m.to_string()
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  fn main() -> std::io::Result<()> {
 | 
	
		
			
				|  |  |    let mut set = Settings {
 | 
	
		
			
				|  |  | -    number: 1, 
 | 
	
		
			
				|  |  | -    target_len: 20, 
 | 
	
		
			
				|  |  | +    first_punct: false, last_punct: false, first_number: false, last_number: false,
 | 
	
		
			
				|  |  | +    capitalize: false, use_conjunctions: false, 
 | 
	
		
			
				|  |  | +    number: 1, target_len: 20, 
 | 
	
		
			
				|  |  |      collecting_parameter: false, 
 | 
	
		
			
				|  |  | -    use_conjunction: false, 
 | 
	
		
			
				|  |  |      help: false, 
 | 
	
		
			
				|  |  | -      // owasp.org/www-community/password-special-characters
 | 
	
		
			
				|  |  | -    pad_chars: " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".to_string() 
 | 
	
		
			
				|  |  | -      // princeton IT
 | 
	
		
			
				|  |  | -    //pad_chars: " ~`!@#$%^&*()-_+={}[]|\\;:\"<>,./?".to_string()
 | 
	
		
			
				|  |  | +    padding: Padding::Spaces,
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    let args: Vec<String> = env::args().collect();
 | 
	
	
		
			
				|  | @@ -55,78 +81,117 @@ fn main() -> std::io::Result<()> {
 | 
	
		
			
				|  |  |      .map(|x| x.expect("bad line"))
 | 
	
		
			
				|  |  |      .filter(|x| !x.ends_with("'s"))
 | 
	
		
			
				|  |  |      .collect();
 | 
	
		
			
				|  |  | -  let num_words = words.len();
 | 
	
		
			
				|  |  |        
 | 
	
		
			
				|  |  |    let mut rng = rand::thread_rng();
 | 
	
		
			
				|  |  |    for _i in 0 .. set.number {
 | 
	
		
			
				|  |  | -    let w0 = &words[rng.gen_range(0 .. num_words)];
 | 
	
		
			
				|  |  | -    let w1 = &words[rng.gen_range(0 .. num_words)];
 | 
	
		
			
				|  |  | -    let w2 = &words[rng.gen_range(0 .. num_words)];
 | 
	
		
			
				|  |  | -    let mut words_len = w0.len() + w1.len();
 | 
	
		
			
				|  |  | -      
 | 
	
		
			
				|  |  | -    let padding = set.pad_chars.chars()
 | 
	
		
			
				|  |  | -      .nth(rng.gen_range(0 .. set.pad_chars.len()))
 | 
	
		
			
				|  |  | -      .unwrap()
 | 
	
		
			
				|  |  | -      .to_string();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    if set.use_conjunction {
 | 
	
		
			
				|  |  | -      let mut conj = CONJUNCTIONS[rng.gen_range(0 .. CONJUNCTIONS.len())].to_string();
 | 
	
		
			
				|  |  | -      if conj.ends_with(" a") && w1.starts_with(|x| "aAeEiIoOuU".contains(x)) {
 | 
	
		
			
				|  |  | -        conj.push('n');
 | 
	
		
			
				|  |  | +    let mut w = Vec::from([get_word(&set, &words)]);
 | 
	
		
			
				|  |  | +    let num_extra_chars = if set.first_punct {1} else {0}  
 | 
	
		
			
				|  |  | +        + if set.last_punct   {1} else {0}
 | 
	
		
			
				|  |  | +        + if set.first_number {1} else {0} 
 | 
	
		
			
				|  |  | +        + if set.last_number  {1} else {0};
 | 
	
		
			
				|  |  | +    while w.iter().map(|a| a.len()).sum::<usize>() 
 | 
	
		
			
				|  |  | +        + if set.padding==Padding::Camel {0} else {w.len()-1} 
 | 
	
		
			
				|  |  | +        + num_extra_chars < set.target_len {
 | 
	
		
			
				|  |  | +      if set.use_conjunctions {
 | 
	
		
			
				|  |  | +        let c = CONJUNCTIONS[rng.gen_range(0..CONJUNCTIONS.len())]; 
 | 
	
		
			
				|  |  | +        w.extend_from_slice(c.split_whitespace()
 | 
	
		
			
				|  |  | +            .map(|x| x.to_string())
 | 
	
		
			
				|  |  | +            .collect::<Vec<_>>()
 | 
	
		
			
				|  |  | +            .as_ref());
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      println!("{} {} {}", w0, conj, w1);
 | 
	
		
			
				|  |  | -    } else {
 | 
	
		
			
				|  |  | -      let pw;
 | 
	
		
			
				|  |  | -      let r:usize;
 | 
	
		
			
				|  |  | -      // if 2 words is enough
 | 
	
		
			
				|  |  | -      if words_len+3 >= set.target_len {
 | 
	
		
			
				|  |  | -        r = cmp::max(set.target_len, words_len+1) - words_len;
 | 
	
		
			
				|  |  | -        pw = format!("{}{}{}",w0,padding.repeat(r),w1); 
 | 
	
		
			
				|  |  | -      } else {
 | 
	
		
			
				|  |  | -          // we need 3 words
 | 
	
		
			
				|  |  | -          words_len = words_len + w2.len();
 | 
	
		
			
				|  |  | -          r = cmp::max(set.target_len, words_len+2) - words_len;
 | 
	
		
			
				|  |  | -          pw = format!("{}{}{}{}{}",w0,padding.repeat(r/2),w1,padding.repeat(r-r/2),w2);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      w.push(get_word(&set, &words));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      let w_len = w.len();
 | 
	
		
			
				|  |  | +      if w[w_len-2] == "a" && "aeiouAEIOU".find(w[w_len-1].chars().nth(0).unwrap()).is_some() {
 | 
	
		
			
				|  |  | +        w[w_len - 2] = "an".to_string();
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      println!("{}",pw);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +    let mut ret : String;
 | 
	
		
			
				|  |  | +    match set.padding {
 | 
	
		
			
				|  |  | +    Padding::Camel   => ret = w.join(""),
 | 
	
		
			
				|  |  | +    Padding::Spaces  => ret = w.join(" "),
 | 
	
		
			
				|  |  | +    Padding::Dots    => ret = w.join(&DOTS.chars().nth(rng.gen_range(0 .. DOTS.len())).unwrap().to_string()),
 | 
	
		
			
				|  |  | +    Padding::Punct   => ret = w.join(&PUNCTS.chars().nth(rng.gen_range(0 .. PUNCTS.len())).unwrap().to_string()),
 | 
	
		
			
				|  |  | +    Padding::Numbers => ret = w.join(&NUMBERS.chars().nth(rng.gen_range(0 .. NUMBERS.len())).unwrap().to_string()),
 | 
	
		
			
				|  |  | +    Padding::Snake   => ret = w.join("_"),
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    let n = NUMBERS.chars().nth(rng.gen_range(0 .. NUMBERS.len())).unwrap().to_string();
 | 
	
		
			
				|  |  | +    if set.first_number {
 | 
	
		
			
				|  |  | +      ret = n.clone() + &ret;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    if set.last_number {
 | 
	
		
			
				|  |  | +      ret = ret + &n;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    let p = PUNCTS.chars().nth(rng.gen_range(0 .. PUNCTS.len())).unwrap().to_string();
 | 
	
		
			
				|  |  | +    if set.first_punct {
 | 
	
		
			
				|  |  | +      ret = p.clone() + &ret;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    if set.last_punct {
 | 
	
		
			
				|  |  | +      ret = ret + &p;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    println!("{}",ret);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |    Ok(())
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  fn usage() {
 | 
	
		
			
				|  |  | -  println!("pygen    v1.1    generate passphrase; inspired by xkcd/936
 | 
	
		
			
				|  |  | -Usage: pygen [-p | -P | -n | -N] [-L <n> | --length <n>] [num_phrases]
 | 
	
		
			
				|  |  | -       pygen -c [num_phrases]
 | 
	
		
			
				|  |  | -       pygen -h | --help
 | 
	
		
			
				|  |  | -    -p         pad with only spaces (for smartphone)
 | 
	
		
			
				|  |  | -    -P         pad with spaces, period, comma (for smartphone)
 | 
	
		
			
				|  |  | -    -n         pad with numbers
 | 
	
		
			
				|  |  | -    -N         pad with numbers, spaces, periods, comma
 | 
	
		
			
				|  |  | -    -L --length     add extra filler to reach length (default=20)
 | 
	
		
			
				|  |  | -    -c         make 2 word phrases with a conjunction filler
 | 
	
		
			
				|  |  | -    num_phrases make multiple passphrases (default=1)");
 | 
	
		
			
				|  |  | +  println!("pygen    v1.4    generate passphrase; inspired by xkcd/936
 | 
	
		
			
				|  |  | +Usage: pwgen.py [-sSpPkKnNcCaMX] [-L length] [num_phrases] 
 | 
	
		
			
				|  |  | +         pwgen.py -h | --help | --version
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +       -s           pad with only spaces (default; for smartphone)
 | 
	
		
			
				|  |  | +       -S           pad with period and comma (for smartphone)
 | 
	
		
			
				|  |  | +       -p           pad with special characters
 | 
	
		
			
				|  |  | +       -P           pad with numbers
 | 
	
		
			
				|  |  | +       -k           connect the words as CamelCase
 | 
	
		
			
				|  |  | +       -K           connect the words as snake_case
 | 
	
		
			
				|  |  | +       -n           add a number at the beginning
 | 
	
		
			
				|  |  | +       -N           add a number at the end
 | 
	
		
			
				|  |  | +       -c           add a special character at the begnning
 | 
	
		
			
				|  |  | +       -C           add a special character at the end
 | 
	
		
			
				|  |  | +       -M           capitalize the words
 | 
	
		
			
				|  |  | +       -a           connect the words with a conjuction filler
 | 
	
		
			
				|  |  | +       -X           same as -MNCS (capital, periods & commas, extra number & char)
 | 
	
		
			
				|  |  | +       -L <n>       make pass phrases at least this long; default=20
 | 
	
		
			
				|  |  | +       num_phrases  make multiple pass phrases, one per line; default=1\n");
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  fn parse_args(a:&Vec<String>, s:&mut Settings) {
 | 
	
		
			
				|  |  |    // dbg!(a);
 | 
	
		
			
				|  |  |    for item in &a[1 .. ] {
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |      if s.collecting_parameter == false {
 | 
	
		
			
				|  |  | -      match (item.as_str(),item.parse::<u16>()) {
 | 
	
		
			
				|  |  | -      ("-p", ..) => s.pad_chars = " ".to_string(),
 | 
	
		
			
				|  |  | -      ("-P", ..) => s.pad_chars = " ,.".to_string(),
 | 
	
		
			
				|  |  | -      ("-n", ..) => s.pad_chars = "0123456789".to_string(),
 | 
	
		
			
				|  |  | -      ("-N", ..) => s.pad_chars = " ,.0123456789".to_string(),
 | 
	
		
			
				|  |  | -      ("-L", ..) | ("--length", ..) => s.collecting_parameter = true, 
 | 
	
		
			
				|  |  | -      ("-c", ..) => s.use_conjunction = true,
 | 
	
		
			
				|  |  | -      ("-h", ..) | ("--help", ..) => s.help = true,
 | 
	
		
			
				|  |  | -      (.., Ok(x)) => s.number = x,
 | 
	
		
			
				|  |  | -      _ => println!("invalid option: {}", item),
 | 
	
		
			
				|  |  | +      let mut chr_itr = item.as_str().chars(); // pick off the first char
 | 
	
		
			
				|  |  | +      if chr_itr.next() == Some('-') {
 | 
	
		
			
				|  |  | +        for b in chr_itr {  // TODO: skip the first, we've already digested it
 | 
	
		
			
				|  |  | +          // println!("got a char {}", b);
 | 
	
		
			
				|  |  | +          match b {
 | 
	
		
			
				|  |  | +          'h' => s.help = true,
 | 
	
		
			
				|  |  | +          's' => s.padding = Padding::Spaces,
 | 
	
		
			
				|  |  | +          'S' => s.padding = Padding::Dots,
 | 
	
		
			
				|  |  | +          'p' => s.padding = Padding::Punct,
 | 
	
		
			
				|  |  | +          'P' => s.padding = Padding::Numbers,
 | 
	
		
			
				|  |  | +          'k' => s.padding = Padding::Camel,
 | 
	
		
			
				|  |  | +          'K' => s.padding = Padding::Snake,
 | 
	
		
			
				|  |  | +          'c' => s.first_punct  = true,
 | 
	
		
			
				|  |  | +          'C' => s.last_punct   = true,
 | 
	
		
			
				|  |  | +          'n' => s.first_number = true,
 | 
	
		
			
				|  |  | +          'N' => s.last_number  = true,
 | 
	
		
			
				|  |  | +          'X' => {s.capitalize = true; s.padding = Padding::Dots;
 | 
	
		
			
				|  |  | +                s.last_punct = true;  s.last_number = true; }, 
 | 
	
		
			
				|  |  | +          'M' => s.capitalize = true,
 | 
	
		
			
				|  |  | +          'a' => s.use_conjunctions = true,
 | 
	
		
			
				|  |  | +          'L' => s.collecting_parameter = true,
 | 
	
		
			
				|  |  | +          _ => println!("unexpected command option: {}", b),
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      } else { // not a hyphen-option phrase, check for an int
 | 
	
		
			
				|  |  | +        //println!("got a command line option {}",item.to_string());
 | 
	
		
			
				|  |  | +        s.number = item.parse::<u16>().expect("phrase count must be a positive integer");
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -    } else {
 | 
	
		
			
				|  |  | +    } else { // pick up the value of the -L argument
 | 
	
		
			
				|  |  |        s.collecting_parameter = false;
 | 
	
		
			
				|  |  |        s.target_len = item.parse::<usize>().expect("length must be a positive integer");
 | 
	
		
			
				|  |  |      }
 |