xkcd-password 0.1.1

Generate passwords
Documentation
extern crate rand;

mod utils;

use rand::Rng;

#[cfg(feature="built_in_dicts")]
/// The dictionary to be used for password generation.
///
/// This is also a list of supported dictionaries.
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Dictionary {
    en_US,
    //pt_BR,
}

#[cfg(feature="built_in_dicts")]
impl Dictionary {
    fn to_raw_dict(self) -> &'static str {
        match self {
            Dictionary::en_US => include_str!(concat!(env!("OUT_DIR"), "/dictionary/en_US")),
            //Dictionary::pt_BR => include_str!(concat!(env!("OUT_DIR"), "/dictionary/pt_BR")),
        }
    }
}

/// Attempts to generate an xkcd-password given a words count and a custom dict.
///
/// Warning: This function doesn't protect against side-channel attacks.
///
/// This function doesn't check for duplicate words in the dict.
///
/// # Panics
///
/// Panics if dict is in an invalid format.
///
/// Panics if you ask for too many words.
///
/// # Examples
/// 
/// ```
/// let res = xkcd_password::generate_password_custom_dict(4, &["Ia", "weanmeheno", "theshehimheryes"]);
/// // res is made up of the words "I" "a" "we" "an" "me" "he" "no" "the" "she" "him" "her" "yes"
/// ```
pub fn generate_password_custom_dict(word_count: usize, dict: &[&str]) -> String {
    // avoid using unnecessary memory in the next steps.
    let dict = utils::trim_right_by(dict, |e| e.is_empty());
    // index 0 = words of length 1, index 1 = words of length 2, etc. give us capacity for
    // word_count * max_word_length + spaces.
    let capacity = word_count * dict.len() + word_count;
    // calculate how many words for each length.
    let n_of_words: Vec<usize> = dict.iter().enumerate().map(|x| x.1.len() / (x.0 + 1)).collect(); 
    // and how many words in total
    let total_n_of_words = n_of_words.iter().sum();
    // make sure all lengths are correct.
    if dict.iter().zip(n_of_words.iter()).enumerate().any(|(wlen, (words, count))| (wlen+1)*count != words.len()) {
        panic!("Error in password generation: Dictionary invalid.");
    }
    // prepare target.
    let mut s = String::with_capacity(capacity);
    // select words.
    for word_n in (0..word_count).map(|_| rand::thread_rng().gen_range(0, total_n_of_words)) {
        let mut found: Option<&str> = None;
        let mut real_pos = 0;
        // iterate the length of the words, the amount of the words, and the words themselves.
        for (wlen, (n_words, words)) in n_of_words.iter().zip(dict.iter()).enumerate() {
            // fix up wlen
            let wlen = wlen + 1;
            // figure out where we should be in the full dict.
            real_pos += n_words;
            if word_n < real_pos {
                // figure out where we should be in the dict of `wlen`-sized words.
                let base_pos = real_pos - n_words;
                let inner_pos = word_n - base_pos;
                let inner_index = inner_pos * wlen;
                // extract word from dict.
                found = Some(&words[inner_index..inner_index+wlen]);
                break;
            }
        }
        s.push_str(found.expect("bug in xkcd-password"));
        s.push(' ');
    }
    s
}

/// Attempts to generate an xkcd-password given a words count and a custom dict.
///
/// Warning: This function doesn't protect against side-channel attacks.
///
/// # Panics
///
/// Panics if any words contain space or newline characters, or if it appears more than once.
///
/// Panics if you ask for too many words.
// TODO this function is slow if you need to call it many times.
pub fn generate_password_from_words<'a, I: IntoIterator<Item=&'a str>>(word_count: usize, dict: I) -> String {
    let iter = dict.into_iter();
    let mut dict: Vec<String> = vec![];
    for word in iter {
        if word.contains(' ') || word.contains('\n') {
            panic!("Word contains space or newline characters");
        }
        if dict.len() < word.len() {
            dict.resize(word.len(), String::default());
        }
        if dict[word.len() - 1].as_bytes().chunks(word.len()).any(|chunk| chunk == word.as_bytes()) {
            panic!("Duplicate word in dict");
        }
        dict[word.len() - 1].push_str(word);
    }
    // not much point in collecting it into a single String, the overhead here is rather small.
    let dict: Vec<&str> = dict.iter().map(String::as_str).collect();
    generate_password_custom_dict(word_count, &dict)
}

#[cfg(feature="built_in_dicts")]
/// Generates an xkcd-password with `word_count` words from built-in dictionary `dict`.
///
/// Warning: This function doesn't protect against side-channel attacks.
///
/// # Panics
///
/// Panics if you ask for too many words.
pub fn generate_password(word_count: usize, dict: Dictionary) -> String {
    let v: Vec<&str> = dict.to_raw_dict().lines().collect();
    generate_password_custom_dict(word_count, &v)
}