//! Shortid generation and validation. //! //! Per ADR-0005, the `shortid` user-facing type is a base58 //! random identifier of 10–12 characters with no ambiguous //! glyphs (no `0`/`O`/`I`/`l`). The generator is small enough //! to live in the DSL crate alongside the type definition. use rand::RngExt; /// Base58 alphabet — Bitcoin-style. 0 / O / I / l are excluded /// because they are easily confused in print. const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const DEFAULT_LEN: usize = 10; /// Length bounds accepted on user-supplied shortid values. pub const MIN_LEN: usize = 10; pub const MAX_LEN: usize = 12; /// Generate a fresh shortid using thread-local RNG. #[must_use] pub fn generate() -> String { generate_len(DEFAULT_LEN) } #[must_use] fn generate_len(len: usize) -> String { let mut rng = rand::rng(); let mut out = String::with_capacity(len); for _ in 0..len { let idx = rng.random_range(0..ALPHABET.len()); out.push(ALPHABET[idx] as char); } out } /// Validate a user-supplied shortid value. pub fn validate(value: &str) -> Result<(), String> { if value.len() < MIN_LEN || value.len() > MAX_LEN { return Err(format!( "shortid must be {MIN_LEN}–{MAX_LEN} characters; got {} character(s)", value.chars().count() )); } for c in value.chars() { if !ALPHABET.contains(&(c as u8)) { return Err(format!( "shortid contains '{c}', which is not in the base58 alphabet \ (no 0, O, I, or l; ASCII letters and digits otherwise)" )); } } Ok(()) } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn generated_ids_have_default_length() { let id = generate(); assert_eq!(id.len(), DEFAULT_LEN); } #[test] fn generated_ids_use_only_base58_alphabet() { for _ in 0..100 { let id = generate(); for c in id.chars() { assert!( ALPHABET.contains(&(c as u8)), "char {c:?} not in base58 alphabet" ); } } } #[test] fn generated_ids_are_not_all_identical() { // Probabilistically extremely unlikely with a good RNG; // catches a wholly broken generator (constant output). let a = generate(); let b = generate(); let c = generate(); assert!( a != b || b != c, "all three generated ids were identical: {a}, {b}, {c}" ); } #[test] fn validate_accepts_well_formed_values() { assert!(validate("23456789Ab").is_ok()); // 10 chars assert!(validate("23456789AbCD").is_ok()); // 12 chars } #[test] fn validate_rejects_too_short_or_too_long() { let err = validate("short").unwrap_err(); assert!(err.contains("characters")); let err = validate("waytoolongafornow").unwrap_err(); assert!(err.contains("characters")); } #[test] fn validate_rejects_ambiguous_glyphs() { for bad in ["0aaaaaaaaa", "Oaaaaaaaaa", "Iaaaaaaaaa", "laaaaaaaaa"] { let err = validate(bad).unwrap_err(); assert!(err.contains("base58"), "for {bad}: {err}"); } } }