1
0
Fork 0
forked from 2sys/phonograph
phonograph/interim-namegen/src/lib.rs
2025-10-09 08:01:01 +00:00

102 lines
3.3 KiB
Rust

//! A very simple utility crate for generating randomized, memorable names from
//! the EFF passphrase wordlist.
//!
//! ## Examples
//!
//! ### Basic Usage
//!
//! ```
//! let name: String = interim_namegen::default_generator().generate_name(3);
//! ```
use std::sync::LazyLock;
use rand::{Rng, rngs::ThreadRng, seq::SliceRandom};
// 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];
for (i, word) in include_str!("./eff_large_wordlist.txt")
.split('\n')
.enumerate()
{
assert!(i <= WORDLIST_LEN); // The wordlist may contain a trailing newline.
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))
}
}