// Phonograph - A friendly PostgreSQL wrapper for nerds of all stripes. // Copyright (C) 2025 Second System Technologies LLC // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or (at your // option) any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS // FOR A PARTICULAR PURPOSE. // // See the GNU Affero General Public License for more details. You should have // received a copy of the GNU Affero General Public License along with this // program. If not, see . //! A very simple utility crate for generating randomized, memorable names from //! the EFF passphrase wordlist. //! //! ## Examples //! //! ### Basic Usage //! //! ``` //! let name: String = phono_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]; // Embed wordlist file rather than attempt to fetch it from disk at runtime. for (i, word) in include_str!("./eff_large_wordlist.txt") .split('\n') .enumerate() { // The wordlist may contain a trailing newline. assert!(i <= WORDLIST_LEN); 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 { NameGenerator::::default() } /// Name generator client. pub struct NameGenerator { rng: T, sep: char, } impl Default for NameGenerator { fn default() -> Self { Self { rng: T::default(), sep: '_', } } } impl NameGenerator { /// 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)) } }