2025-10-07 06:23:50 +00:00
|
|
|
//! A very simple utility crate for generating randomized, memorable names from
|
|
|
|
|
//! the EFF passphrase wordlist.
|
|
|
|
|
//!
|
|
|
|
|
//! ## Examples
|
|
|
|
|
//!
|
|
|
|
|
//! ### Basic Usage
|
|
|
|
|
//!
|
|
|
|
|
//! ```
|
2025-11-19 01:45:58 +00:00
|
|
|
//! let name: String = phono_namegen::default_generator().generate_name(3);
|
2025-10-07 06:23:50 +00:00
|
|
|
//! ```
|
|
|
|
|
|
|
|
|
|
use std::sync::LazyLock;
|
|
|
|
|
|
2025-11-19 01:45:58 +00:00
|
|
|
use rand::{rngs::ThreadRng, seq::SliceRandom, Rng};
|
2025-10-07 06:23:50 +00:00
|
|
|
|
|
|
|
|
// EFF wordlist for random password phrases.
|
|
|
|
|
//
|
|
|
|
|
// > We took all words between 3 and 9 characters from the list, prioritizing
|
|
|
|
|
// > the most recognized words and then the most concrete words. We manually
|
|
|
|
|
// > checked and attempted to remove as many profane, insulting, sensitive, or
|
|
|
|
|
// > emotionally-charged words as possible, and also filtered based on several
|
|
|
|
|
// > public lists of vulgar English words (for example this one published by
|
|
|
|
|
// > Luis von Ahn). We further removed words which are difficult to spell as
|
|
|
|
|
// > well as homophones (which might be confused during recall). We also ensured
|
|
|
|
|
// > that no word is an exact prefix of any other word.
|
|
|
|
|
//
|
|
|
|
|
// [Deep Dive: EFF's New Wordlists for Random Passphrases](
|
|
|
|
|
// https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases
|
|
|
|
|
// )
|
|
|
|
|
//
|
|
|
|
|
// Note that the wordlist file should have the dice values removed, such that
|
|
|
|
|
// each line contains only the word. This is to simplify data processing as well
|
|
|
|
|
// as to help shrink the raw string data packaged with the compiled binary.
|
|
|
|
|
const WORDLIST_LEN: usize = 7776;
|
|
|
|
|
// TODO: Rewrite this as a compile time macro generating a const instead of a
|
|
|
|
|
// LazyLock.
|
|
|
|
|
static WORDLIST: LazyLock<[&str; WORDLIST_LEN]> = LazyLock::new(|| {
|
|
|
|
|
let mut words = [""; WORDLIST_LEN];
|
2025-11-19 01:45:58 +00:00
|
|
|
// Embed wordlist file rather than attempt to fetch it from disk at runtime.
|
2025-10-07 06:23:50 +00:00
|
|
|
for (i, word) in include_str!("./eff_large_wordlist.txt")
|
|
|
|
|
.split('\n')
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
2025-11-19 01:45:58 +00:00
|
|
|
// The wordlist may contain a trailing newline.
|
|
|
|
|
assert!(i <= WORDLIST_LEN);
|
2025-10-07 06:23:50 +00:00
|
|
|
if i < WORDLIST_LEN {
|
|
|
|
|
words[i] = word;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assert that wordlist fills the entire array--in other words, that
|
|
|
|
|
// `WORDLIST_LEN` is accurate.
|
|
|
|
|
assert!(!words[WORDLIST_LEN - 1].is_empty());
|
|
|
|
|
|
|
|
|
|
words
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// Constructs a [`NameGenerator`] configured to use [`rand::ThreadRng`].
|
|
|
|
|
pub fn default_generator() -> NameGenerator<ThreadRng> {
|
|
|
|
|
NameGenerator::<ThreadRng>::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Name generator client.
|
|
|
|
|
pub struct NameGenerator<T: Rng> {
|
|
|
|
|
rng: T,
|
|
|
|
|
sep: char,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: Default + Rng> Default for NameGenerator<T> {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
rng: T::default(),
|
|
|
|
|
sep: '_',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: Rng> NameGenerator<T> {
|
|
|
|
|
/// Set the separator between words in generated names (`'_'` by default).
|
|
|
|
|
pub fn with_separator(mut self, sep: char) -> Self {
|
|
|
|
|
self.sep = sep;
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return `n` randomly selected words from the wordlist, without
|
|
|
|
|
/// repetition.
|
|
|
|
|
pub fn choose_words(&mut self, n: usize) -> Vec<&'static str> {
|
|
|
|
|
WORDLIST
|
|
|
|
|
.choose_multiple(&mut self.rng, n)
|
|
|
|
|
.copied()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate a randomized name with `n` words, joined into a single
|
|
|
|
|
/// [`String`].
|
|
|
|
|
pub fn generate_name(&mut self, n: usize) -> String {
|
|
|
|
|
// Temporarily store the UTF8 representation of the [`char`] on the
|
|
|
|
|
// stack to avoid a heap allocation for the conversion to [`&str`].
|
|
|
|
|
let mut sep_buf: [u8; 4] = [0; 4];
|
|
|
|
|
|
|
|
|
|
self.choose_words(n)
|
|
|
|
|
.join(self.sep.encode_utf8(&mut sep_buf))
|
|
|
|
|
}
|
|
|
|
|
}
|