Something went wrong. Try again.
Identities for entities did.bot
agent llm did
Something went wrong. Try again.
28 kB · 714 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715//! Names that are computed rather than drawn from a list.//!//! [`fragments::Fragments`](crate::fragments::Fragments) is finite in a way//! that matters at scale: `mineral+creature` is 128 words against 112, about//! fourteen thousand names, and [`Naming::issue`](../../didbot_pds/struct.Naming.html)'s//! retry loop makes the usable ceiling nearer eleven thousand. See//! `plan/zone-scale.md`. The namers in this module do not draw from a fixed//! set at all, so "how many names are left" is either "effectively never//! runs out" or "as many as the deployment configured", and each one says//! which in its own docs.//!//! Every namer here still goes through [`check`], the same DNS-label//! validator [`fragments::Fragments`](crate::fragments::Fragments) and//! [`command::CommandNamer`](crate::command::CommandNamer) use, and that//! validator is [`didbot_identity::validate_label`] — the same function that//! decides whether a label can become part of a `did:web` identifier. There//! is one legality rule, not one for DNS and a second one somebody hoped//! agreed with it.//!//! # What a generated name gives up//!//! A word pair leaks nothing about the deployment: `basalt-otter` says//! nothing about when it was minted or how many agents came before it. Every//! namer below trades that away for something else, and what it trades away//! is documented on the type rather than left to be discovered from traffic://!//! * [`Counter`] tells any observer exactly how many accounts this deployment//! has ever minted — the counter's value *is* that count. So does//! [`fragments::Fragments`](crate::fragments::Fragments)'s `{n}` slot, which//! draws from the same counter: `basalt-otter-4813` discloses everything//! `agent-4813` does.//! * [`Timestamp`] tells an observer when an account was minted, to the//! second. So does//! [`fragments::Fragments`](crate::fragments::Fragments)'s `{t}` slot,//! which reads the same clock.//! * [`Uuid`] and [`Random`] leak nothing beyond their own existence: a//! uniformly random identifier carries no information about the population//! it was drawn from. These are the private-by-default choice among the//! generated forms, the same role the word lists play among the fixed ones.
use std::fmt;use std::sync::Arc;
use crate::{check, tidy, NameError, Namer, Seed};
/// Where a [`Counter`] gets its next value, and remembers it past a restart.////// This crate has no notion of disk or of a write-ahead log — see the crate/// docs — so it does not implement this itself. `didbot-pds` does, backed by/// the same log [`crate::fragments`]'s deployment already writes claims and/// releases to: a value is durable before [`CounterSource::next`] returns it,/// the same check-before-apply order the rest of that log keeps. A counter/// that only lived in memory would hand out `0` again after every restart,/// which is not monotonic, it only looks monotonic between restarts.pub trait CounterSource: Send + Sync + fmt::Debug { /// The next value, guaranteed not equal to any value this has returned /// before — including before a restart, if the implementation is durable. /// /// Fallible, unlike everything else a namer touches here, because a /// durable implementation's whole reason to exist is the write reaching /// disk before the value is handed out: an implementation that could not /// make that promise this call must say so rather than hand out a value /// it cannot back. The error is a message rather than a type, so this /// crate is not made to know the shape of whatever failed underneath it. fn next(&self) -> Result<u64, String>;}
/// How a number — a counter's value or a timestamp — is written into a label.////// Both spellings are lowercase digits and letters at every value, so a label/// carrying one is legal wherever a label carrying the other is. They differ/// in length and in what a person reads back: decimal is the number itself,/// base32 is shorter once the count passes a million.#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]pub enum NumberEncoding { /// Decimal digits, no padding: `0`, `4813`, `18446744073709551615`. The /// count as anyone would write it, and 20 characters at its longest. #[default] Decimal, /// Crockford base32, lowercase, no padding: `0`, `4nd`, `fzzzzzzzzzzzz`. /// The alphabet drops `i`, `l`, `o` and `u`, so nothing in a label is /// confusable with a digit or spells a word, and the longest value is 13 /// characters. Base32,}
impl NumberEncoding { /// The label text for `value`. pub fn render(self, value: u64) -> String { match self { Self::Decimal => value.to_string(), Self::Base32 => to_crockford(value), } }
/// How many characters the longest value takes, which is the room a /// template has to leave for the slot. pub const fn widest(self) -> usize { match self { // u64::MAX is 20 digits; 13 base32 symbols carry 65 bits. Self::Decimal => 20, Self::Base32 => 13, } }
/// What the configuration calls this, and what parsing one takes. pub const fn spelling(self) -> &'static str { match self { Self::Decimal => "decimal", Self::Base32 => "base32", } }}
impl fmt::Display for NumberEncoding { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.spelling()) }}
impl std::str::FromStr for NumberEncoding { type Err = NameError;
fn from_str(raw: &str) -> Result<Self, Self::Err> { match raw { "decimal" => Ok(Self::Decimal), "base32" => Ok(Self::Base32), other => Err(NameError::Misconfigured(format!( "{other:?} is not a number encoding; write `decimal` or `base32`" ))), } }}
/// A namer backed by a counter that only ever goes up.////// # Pool size////// `u64::MAX`, which is not a number a deployment will reach: at one/// provisioning a second, sustained, it is filled in a little under six/// hundred thousand centuries. Call it unbounded. It is also the only namer/// in this crate that cannot collide with itself — every value/// [`CounterSource::next`] returns is one no earlier call returned — so it/// asks the caller for at most one attempt; see/// [`Namer::max_useful_attempts`].////// # What it leaks////// The count itself. `agent-4813` says this deployment has minted at least/// four thousand eight hundred and thirteen accounts, to anyone who can see/// one label. A deployment that would rather not publish its population uses/// a word list, [`Uuid`], or [`Random`] instead.////// [`fragments::Fragments`](crate::fragments::Fragments)'s `{n}` slot draws/// from the same source and writes it with the same [`NumberEncoding`], so a/// deployment can have a word and a count in one label.#[derive(Debug, Clone)]pub struct Counter { source: Arc<dyn CounterSource>, prefix: String, encoding: NumberEncoding,}
impl Counter { /// A counter namer drawing from `source`, labelled `agent-<n>`. pub fn new(source: Arc<dyn CounterSource>) -> Self { Self { source, prefix: "agent-".to_owned(), encoding: NumberEncoding::default(), } }
/// Uses `prefix` instead of `agent-`. /// /// Not validated until the first name is asked for — [`check`] runs on /// the assembled label either way, so a prefix that would make every name /// illegal is caught there rather than twice. #[must_use] pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self { self.prefix = prefix.into(); self }
/// Writes the count in `encoding` instead of decimal. #[must_use] pub fn with_encoding(mut self, encoding: NumberEncoding) -> Self { self.encoding = encoding; self }}
impl Namer for Counter { fn suggest(&self, _seed: &Seed) -> Result<String, NameError> { let value = self .source .next() .map_err(|error| NameError::Failed(format!("counter: {error}")))?; let name = tidy(&format!("{}{}", self.prefix, self.encoding.render(value))); check(&name)?; Ok(name) }
fn describe(&self) -> String { format!( "counter, {} (unbounded; never collides with itself; the label discloses how many accounts this deployment has ever minted)", self.encoding ) }
fn max_useful_attempts(&self) -> Option<u32> { // A collision here is not the counter's bad luck to try again for — // it means something else holds a name the counter never handed out, // and asking the same source for the same-shaped next value again // does not change that. One attempt is the whole budget it can use. Some(1) }}
/// The current unix time, in whole seconds.////// The one clock this crate reads, so [`Timestamp`] and/// [`fragments::Fragments`](crate::fragments::Fragments)'s `{t}` slot always/// agree about what time it is. Zero before the epoch, which is a clock so/// wrong that naming is not what is broken.pub fn unix_seconds() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|elapsed| elapsed.as_secs()) .unwrap_or(0)}
/// A namer that stamps the current time, in whole seconds.////// # Pool size////// One name per second. Two accounts minted in the same second collide,/// which is why this namer does *not* claim/// [`Namer::max_useful_attempts`]: it uses the ordinary retry budget, and/// folds the attempt number into the label on any attempt past the first so/// a retry is not the same collision twice.////// # What it leaks////// When each account was minted, to the second. A deployment that publishes/// hostnames publicly is publishing its provisioning timeline alongside them.////// [`fragments::Fragments`](crate::fragments::Fragments)'s `{t}` slot reads/// the same clock and writes it with the same [`NumberEncoding`], so a/// deployment can have a word and a time in one label.#[derive(Debug, Clone, Copy, Default)]pub struct Timestamp { encoding: NumberEncoding,}
impl Timestamp { /// A namer that stamps the clock, labelled `t<seconds>`. pub fn new() -> Self { Self::default() }
/// Writes the seconds in `encoding` instead of decimal. #[must_use] pub fn with_encoding(mut self, encoding: NumberEncoding) -> Self { self.encoding = encoding; self }
/// The label this namer makes at `seconds`, on `attempt`. /// /// The clock is read once, by [`Namer::suggest`], and handed in here, so /// a test can say what moment it is naming at. pub(crate) fn label(&self, seconds: u64, attempt: u32) -> String { let mut name = format!("t{}", self.encoding.render(seconds)); if attempt > 0 { name.push('-'); name.push_str(&attempt.to_string()); } name }}
impl Namer for Timestamp { fn suggest(&self, seed: &Seed) -> Result<String, NameError> { let name = tidy(&self.label(unix_seconds(), seed.attempt)); check(&name)?; Ok(name) }
fn describe(&self) -> String { format!( "timestamp, {} (one name per second; the label discloses when each account was minted, to the second)", self.encoding ) }}
/// A namer that draws a fresh UUID (version 4, i.e. all-random) per name.////// # Pool size////// 2^122 — 122 bits of randomness after the version and variant bits are/// fixed, per RFC 9562. The birthday bound on that is far past anything a/// deployment will ever mint; a collision is not a case this namer plans for/// any harder than [`Random`] does.////// # Canonical form only////// [`Uuid::new`] emits the canonical `8-4-4-4-12` hyphenated lowercase hex/// form. Its hyphens land in fixed interior positions — never first, never/// last — which is exactly what [`check`] requires, and that is not a/// coincidence worth assuming holds for other encodings: the RFC 4648/// base64 alphabet includes `+` and `/`, and this crate's own [`Random`]/// alphabets exist because an *uppercase* rendering would fail the same/// check that lowercase hex passes. [`Uuid::compact`] drops the hyphens/// entirely — 32 lowercase hex characters, still legal — for a shorter label;/// nothing here offers any encoding beyond those two, on purpose.////// # What it leaks////// Nothing beyond its own existence. A uniformly random 122-bit value carries/// no information about how many agents came before it or when it was/// minted — the private-by-default generated form, alongside [`Random`].#[derive(Debug, Clone, Copy, Default)]pub struct Uuid { compact: bool,}
impl Uuid { /// Canonical `8-4-4-4-12` hyphenated form, 36 characters. pub fn new() -> Self { Self { compact: false } }
/// 32 lowercase hex characters, no hyphens. pub fn compact() -> Self { Self { compact: true } }}
impl Namer for Uuid { fn suggest(&self, _seed: &Seed) -> Result<String, NameError> { let mut bytes = rand::random::<[u8; 16]>(); // Version 4 and the RFC 9562 variant bits. Cosmetic here — nothing // reads a version out of a hostname label — but a "UUID" that skips // them is not one, and setting them costs two lines. bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; let hex = to_hex(&bytes); let name = if self.compact { hex } else { format!( "{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32] ) }; check(&name)?; Ok(name) }
fn describe(&self) -> String { if self.compact { "uuid v4, compact (2^122 possible names; discloses nothing about the population)" .to_owned() } else { "uuid v4, canonical (2^122 possible names; discloses nothing about the population)" .to_owned() } }}
/// Which characters a [`Random`] namer draws from.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Alphabet { /// `234567abcdefghijklmnopqrstuvwxyz` — base32, missing the digits and /// letters most often confused for each other or for punctuation /// (`0`/`o`, `1`/`l`/`i`). What an operator reads off a screen and types /// back is what this is for. Base32, /// `0123456789abcdefghijklmnopqrstuvwxyz` — base36, every lowercase /// letter and digit. Denser than base32 at the same length, at the cost /// of the ambiguous characters base32 leaves out. Base36,}
impl Alphabet { fn chars(self) -> &'static [u8] { match self { Self::Base32 => b"234567abcdefghijklmnopqrstuvwxyz", Self::Base36 => b"0123456789abcdefghijklmnopqrstuvwxyz", } }}
/// A namer that draws a fixed-length random string per name.////// # Pool size////// `alphabet_size ^ length`, reported by [`Random::pool_size`] and folded/// into [`Random::describe`] so a deployment configuring one can see the/// number rather than compute it: base36 at 10 characters is a little over/// 3.6 × 10^15, base32 at the same length a little under 1.1 × 10^15. Like/// any drawn-not-derived namer it can collide, so it takes the ordinary/// sixteen-attempt retry budget rather than claiming/// [`Namer::max_useful_attempts`].////// # What it leaks////// Nothing beyond its own existence — see [`Uuid`]'s docs, which apply here/// unchanged.#[derive(Debug, Clone)]pub struct Random { alphabet: Alphabet, length: usize,}
impl Random { /// A namer drawing `length` characters from `alphabet`. /// /// Refused at construction, not at the first name, for the same reason /// [`Fragments::new`](crate::fragments::Fragments::new) checks its slots /// up front: a namer that starts and then cannot produce a single legal /// name is worse than one that refuses to start. pub fn new(alphabet: Alphabet, length: usize) -> Result<Self, NameError> { if length == 0 { return Err(NameError::Misconfigured( "a random name needs at least one character".to_owned(), )); } if length > crate::MAX_NAME { return Err(NameError::Misconfigured(format!( "a random name of {length} characters is longer than a DNS label may be ({})", crate::MAX_NAME ))); } Ok(Self { alphabet, length }) }
/// How many distinct names this can produce, before a collision is even /// possible: `alphabet_size ^ length`. /// /// Saturating, because [`Random::new`] accepts every length a DNS label /// admits and the answer stops fitting in a `u128` long before that: 25 /// base36 characters already exceeds it. Past that point the exact count /// is not a number anybody acts on — it is far beyond any birthday bound /// a deployment can reach — whereas an arithmetic overflow in what is /// only ever a reported figure is a panic on the path that reports it. pub fn pool_size(&self) -> u128 { (self.alphabet.chars().len() as u128).saturating_pow(self.length as u32) }}
impl Namer for Random { fn suggest(&self, _seed: &Seed) -> Result<String, NameError> { let alphabet = self.alphabet.chars(); let mut name = String::with_capacity(self.length); for _ in 0..self.length { // Not cryptographic and does not need to be: a collision here // is an availability problem, not a security one, since nothing // is authorized by the label. let index = (rand::random::<u32>() as usize) % alphabet.len(); name.push(char::from(alphabet[index])); } let name = tidy(&name); check(&name)?; Ok(name) }
fn describe(&self) -> String { format!( "random {:?}, {} characters ({} possible names)", self.alphabet, self.length, self.pool_size() ) }}
/// Renders `value` as lowercase Crockford base32, with no padding.////// Crockford's alphabet rather than RFC 4648's, because the four symbols it/// leaves out — `i`, `l`, `o` and `u` — are the ones a person reading a/// hostname off a screen confuses with `1` and `0`, and dropping `u` keeps/// the encoding from spelling anything. Lowercase, because [`check`] refuses/// an uppercase label.fn to_crockford(mut value: u64) -> String { const DIGITS: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz"; if value == 0 { return "0".to_owned(); } let mut out = Vec::new(); while value > 0 { out.push(DIGITS[(value % 32) as usize]); value /= 32; } out.reverse(); String::from_utf8(out).expect("crockford digits are ascii")}
/// Lowercase hex, dependency-free: this is a handful of lines, not a reason/// to add a crate.fn to_hex(bytes: &[u8]) -> String { const DIGITS: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(bytes.len() * 2); for byte in bytes { out.push(char::from(DIGITS[(byte >> 4) as usize])); out.push(char::from(DIGITS[(byte & 0x0f) as usize])); } out}
#[cfg(test)]mod tests { use super::*; use std::sync::atomic::{AtomicU64, Ordering};
/// The zone this crate's tests mint labels under. They assert against /// `didbot_identity::AccountDid::mint` rather than a local copy of the /// rules, so a test can only pass by agreeing with the actual authority. fn mints(label: &str) -> bool { let zone = didbot_identity::Zone::new("agents.example") .expect("agents.example is a valid zone host"); didbot_identity::AccountDid::mint(&zone, label).is_ok() }
#[derive(Debug)] struct TestSource(AtomicU64);
impl CounterSource for TestSource { fn next(&self) -> Result<u64, String> { Ok(self.0.fetch_add(1, Ordering::SeqCst)) } }
#[test] fn a_counter_never_repeats_and_always_mints() { for encoding in [NumberEncoding::Decimal, NumberEncoding::Base32] { let counter = Counter::new(Arc::new(TestSource(AtomicU64::new(0)))).with_encoding(encoding); let mut seen = std::collections::HashSet::new(); for attempt in 0..500 { let seed = Seed::new("tok", "agents.example").with_parent(None); let seed = Seed { attempt, ..seed }; let name = counter.suggest(&seed).expect("counter always succeeds"); assert!(check(&name).is_ok(), "{name:?} failed check()"); assert!(mints(&name), "{name:?} did not mint"); assert!(seen.insert(name.clone()), "{name:?} repeated"); } assert_eq!(counter.max_useful_attempts(), Some(1)); assert!(counter.describe().contains(encoding.spelling())); } }
/// The last value the counter can reach still mints, in either encoding: /// a deployment does not discover at some future count that its namer /// has stopped producing legal labels. #[test] fn a_counter_at_its_last_value_still_mints() { for encoding in [NumberEncoding::Decimal, NumberEncoding::Base32] { let counter = Counter::new(Arc::new(TestSource(AtomicU64::new(u64::MAX)))) .with_encoding(encoding); let name = counter .suggest(&Seed::new("tok", "agents.example")) .expect("counter always succeeds"); assert!(check(&name).is_ok(), "{name:?} failed check()"); assert!(mints(&name), "{name:?} did not mint"); assert!(name.ends_with(&encoding.render(u64::MAX)), "{name:?}"); } }
#[test] fn a_counter_with_a_custom_prefix_still_mints() { let counter = Counter::new(Arc::new(TestSource(AtomicU64::new(0)))).with_prefix("worker-"); let seed = Seed::new("tok", "agents.example"); let name = counter.suggest(&seed).expect("a legal prefix mints"); assert!(name.starts_with("worker-")); assert!(mints(&name), "{name:?}"); }
#[test] fn timestamps_mint_and_vary_with_the_attempt() { for encoding in [NumberEncoding::Decimal, NumberEncoding::Base32] { for attempt in [0u32, 1, 2, 15] { let seed = Seed { attempt, ..Seed::new("tok", "agents.example") }; let name = Timestamp::new() .with_encoding(encoding) .suggest(&seed) .expect("clock is readable"); assert!(check(&name).is_ok(), "{name:?}"); assert!(mints(&name), "{name:?}"); if attempt > 0 { assert!(name.ends_with(&format!("-{attempt}")), "{name:?}"); } } } }
/// A clock that has run far past today still names: the label is legal /// in both encodings at the year 2100 and at the last second a `u64` /// can hold. #[test] fn a_timestamp_label_is_legal_at_the_far_end_of_the_clock() { // 2100-01-01T00:00:00Z, and the end of the range itself. for seconds in [4_102_444_800u64, u64::MAX] { for encoding in [NumberEncoding::Decimal, NumberEncoding::Base32] { for attempt in [0u32, 15] { let name = tidy( &Timestamp::new() .with_encoding(encoding) .label(seconds, attempt), ); assert!(check(&name).is_ok(), "{name:?} failed check()"); assert!(mints(&name), "{name:?} did not mint"); } } } }
#[test] fn many_uuids_are_legal_labels_in_both_forms() { for uuid in [Uuid::new(), Uuid::compact()] { for _ in 0..500 { let name = uuid .suggest(&Seed::new("tok", "agents.example")) .expect("uuid generation cannot fail"); assert!(check(&name).is_ok(), "{name:?} failed check()"); assert!(mints(&name), "{name:?} did not mint"); } } }
#[test] fn a_canonical_uuid_has_hyphens_only_in_the_legal_positions() { let name = Uuid::new() .suggest(&Seed::new("tok", "agents.example")) .expect("uuid generation cannot fail"); assert_eq!(name.len(), 36); for (index, ch) in name.char_indices() { if [8, 13, 18, 23].contains(&index) { assert_eq!(ch, '-', "{name:?} at {index}"); } else { assert!( ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase(), "{name:?} at {index}" ); } } }
#[test] fn a_compact_uuid_has_no_hyphens() { let name = Uuid::compact() .suggest(&Seed::new("tok", "agents.example")) .expect("uuid generation cannot fail"); assert_eq!(name.len(), 32); assert!(!name.contains('-')); }
#[test] fn random_names_of_every_configured_length_mint() { for alphabet in [Alphabet::Base32, Alphabet::Base36] { for length in [1usize, 4, 16, 63] { let namer = Random::new(alphabet, length).expect("a legal length constructs"); for _ in 0..200 { let name = namer .suggest(&Seed::new("tok", "agents.example")) .expect("random generation cannot fail"); assert_eq!(name.len(), length, "{name:?}"); assert!(check(&name).is_ok(), "{name:?} failed check()"); assert!(mints(&name), "{name:?} did not mint"); } } } }
/// [`Random::new`] accepts any length a DNS label admits, and `describe` /// is called on the provisioning path — including from the log line the /// personal data server writes when a namer refuses. The reported figure /// overflowing must not take that path down with it. #[test] fn a_random_namer_long_enough_to_overflow_still_describes_itself() { for alphabet in [Alphabet::Base32, Alphabet::Base36] { let namer = Random::new(alphabet, crate::MAX_NAME).expect("a legal length"); assert_eq!(namer.pool_size(), u128::MAX); assert!(namer.describe().contains("possible names")); // And it still names, which is the point of allowing the length. let name = namer .suggest(&Seed::new("token-a", "agents.example")) .expect("a legal length names"); assert_eq!(name.len(), crate::MAX_NAME); } }
#[test] fn random_pool_size_is_alphabet_to_the_length() { let namer = Random::new(Alphabet::Base36, 10).expect("legal"); assert_eq!(namer.pool_size(), 36u128.pow(10)); }
#[test] fn random_refuses_zero_length_and_over_length_labels() { assert!(Random::new(Alphabet::Base36, 0).is_err()); assert!(Random::new(Alphabet::Base36, crate::MAX_NAME + 1).is_err()); assert!(Random::new(Alphabet::Base36, crate::MAX_NAME).is_ok()); }}