1
0
Fork 0
forked from 2sys/phonograph
phonograph/phono-namegen/src/lib.rs

121 lines
4.2 KiB
Rust
Raw Normal View History

2025-11-18 20:38:01 -08:00
// 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 <http://www.gnu.org/licenses/>.
//! 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;
2025-11-18 20:38:01 -08:00
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<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))
}
}