use std::collections::HashSet;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
const WORDS_TXT: &str = include_str!("words.txt");
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MintError {
#[error("name pool exhausted: all {0} two-word combinations are occupied")]
Exhausted(usize),
}
pub trait Rng {
fn next_u64(&mut self) -> u64;
}
#[derive(Debug, Clone)]
pub struct SplitMix64 {
state: u64,
}
impl SplitMix64 {
pub fn from_seed(seed: u64) -> Self {
Self { state: seed }
}
pub fn from_entropy() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
Self::from_seed(nanos ^ (u64::from(std::process::id()) << 32))
}
}
impl Rng for SplitMix64 {
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
fn wordlist() -> Vec<&'static str> {
WORDS_TXT
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
fn pair(words: &[&str], idx: usize) -> String {
let span = words.len().saturating_sub(1).max(1);
let first = idx / span;
let second = idx % span;
let second = second + usize::from(second >= first);
let at = |i: usize| words.get(i).copied().unwrap_or_default();
format!("{}-{}", at(first), at(second))
}
fn mint_from(
words: &[&str],
rng: &mut dyn Rng,
occupied: &HashSet<String>,
) -> Result<String, MintError> {
let pool = words.len() * words.len().saturating_sub(1);
let start = (rng.next_u64() % pool.max(1) as u64) as usize;
for step in 0..pool {
let name = pair(words, (start + step) % pool);
if !occupied.contains(&name) {
return Ok(name);
}
}
Err(MintError::Exhausted(pool))
}
pub fn mint(rng: &mut dyn Rng, occupied: &HashSet<String>) -> Result<String, MintError> {
mint_from(&wordlist(), rng, occupied)
}
pub fn occupied(names_root: &Path, claimants: &[String]) -> HashSet<String> {
std::fs::read_dir(names_root)
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().into_string().ok())
.chain(claimants.iter().cloned())
.collect()
}
#[cfg(test)]
mod tests;