#![forbid(unsafe_code)]
use std::fmt;
pub const FORMAT_VERSION: u32 = 3;
#[rustfmt::skip]
pub const ALPHABET: [&str; 256] = [
"access", "account", "action", "active", "added", "address", "album", "align", "android",
"append", "area", "args", "array", "article", "author", "available", "background", "band",
"based", "black", "board", "body", "books", "border", "born", "break", "building", "built",
"button", "called", "card", "case", "category", "center", "central", "change", "character",
"check", "children", "city", "class", "click", "client", "close", "club", "code", "color",
"column", "command", "common", "community", "company", "component", "config", "console",
"const", "container", "content", "control", "country", "course", "created", "current",
"database", "date", "days", "default", "define", "design", "details", "device", "display",
"document", "door", "double", "download", "east", "element", "email", "error", "events",
"example", "export", "express", "external", "face", "false", "family", "father", "features",
"field", "files", "film", "final", "find", "float", "font", "football", "force", "format",
"found", "free", "function", "game", "general", "github", "global", "google", "green", "group",
"header", "height", "help", "high", "history", "home", "house", "https", "human", "images",
"import", "include", "index", "input", "install", "items", "json", "label", "language", "large",
"length", "level", "library", "light", "links", "local", "location", "login", "market",
"master", "match", "material", "media", "members", "message", "method", "models", "module",
"month", "music", "named", "network", "number", "object", "office", "online", "options",
"original", "output", "package", "params", "password", "people", "period", "person", "place",
"player", "points", "position", "power", "press", "price", "println", "private", "process",
"product", "program", "project", "property", "public", "python", "query", "question", "random",
"range", "react", "record", "region", "register", "related", "release", "render", "report",
"request", "require", "response", "results", "return", "review", "river", "route", "running",
"school", "score", "script", "search", "season", "section", "security", "select", "series",
"server", "service", "session", "share", "social", "software", "source", "space", "special",
"species", "split", "square", "start", "states", "static", "station", "story", "street",
"string", "student", "style", "success", "support", "system", "table", "target", "template",
"title", "token", "track", "training", "types", "union", "update", "username", "users",
"values", "version", "video", "views", "water", "width", "window", "words", "world"
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UnigramId<const N: usize>([u8; N]);
impl<const N: usize> UnigramId<N> {
pub const fn from_bytes(bytes: [u8; N]) -> Self {
Self(bytes)
}
pub fn try_random() -> Result<Self, getrandom::Error> {
let mut bytes = [0u8; N];
getrandom::fill(&mut bytes)?;
Ok(Self(bytes))
}
pub fn parse(text: &str) -> Result<Self, ParseError> {
if text.is_empty() {
return Err(DecodeError::Empty.into());
}
collect_exact(canonical_bytes(text)).map(Self)
}
pub fn recover(text: &str) -> Result<Self, ParseError> {
collect_exact(recovered_bytes(text)).map(Self)
}
pub const fn as_bytes(&self) -> &[u8; N] {
&self.0
}
pub const fn into_bytes(self) -> [u8; N] {
self.0
}
}
impl<const N: usize> fmt::Display for UnigramId<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, byte) in self.0.iter().enumerate() {
if index > 0 {
f.write_str(" ")?;
}
f.write_str(ALPHABET[*byte as usize])?;
}
Ok(())
}
}
impl<const N: usize> From<[u8; N]> for UnigramId<N> {
fn from(bytes: [u8; N]) -> Self {
Self(bytes)
}
}
impl<const N: usize> From<UnigramId<N>> for [u8; N] {
fn from(id: UnigramId<N>) -> Self {
id.0
}
}
const fn crc8(bytes: &[u8]) -> u8 {
let mut crc: u8 = 0;
let mut index = 0;
while index < bytes.len() {
crc ^= bytes[index];
let mut bit = 0;
while bit < 8 {
crc = if crc & 0x80 != 0 {
(crc << 1) ^ 0x07
} else {
crc << 1
};
bit += 1;
}
index += 1;
}
crc
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CheckedUnigramId<const N: usize>([u8; N]);
impl<const N: usize> CheckedUnigramId<N> {
pub const fn from_bytes(bytes: [u8; N]) -> Self {
Self(bytes)
}
pub fn try_random() -> Result<Self, getrandom::Error> {
UnigramId::<N>::try_random().map(|id| Self(id.into_bytes()))
}
pub fn parse(text: &str) -> Result<Self, ParseError> {
if text.is_empty() {
return Err(DecodeError::Empty.into());
}
Self::verify(collect_checked(canonical_bytes(text))?)
}
pub fn recover(text: &str) -> Result<Self, ParseError> {
Self::verify(collect_checked(recovered_bytes(text))?)
}
pub const fn as_bytes(&self) -> &[u8; N] {
&self.0
}
pub const fn into_bytes(self) -> [u8; N] {
self.0
}
pub const fn check_byte(&self) -> u8 {
crc8(&self.0)
}
fn verify((payload, found): ([u8; N], u8)) -> Result<Self, ParseError> {
let expected = crc8(&payload);
if expected != found {
return Err(ParseError::ChecksumMismatch { expected, found });
}
Ok(Self(payload))
}
}
impl<const N: usize> fmt::Display for CheckedUnigramId<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in &self.0 {
f.write_str(ALPHABET[*byte as usize])?;
f.write_str(" ")?;
}
f.write_str(ALPHABET[self.check_byte() as usize])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
UnknownWord { position: usize, word: String },
NotCanonical { position: usize },
Empty,
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownWord { position, word } => write!(
f,
"`{word}` (word {}) is not in the unigram alphabet",
position + 1
),
Self::NotCanonical { position } => write!(
f,
"word {} is not in canonical form (lowercase, single-space separated)",
position + 1
),
Self::Empty => f.write_str("no unigram words found"),
}
}
}
impl std::error::Error for DecodeError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
Decode(DecodeError),
WrongLength { expected: usize, found: usize },
ChecksumMismatch { expected: u8, found: u8 },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Decode(error) => error.fmt(f),
Self::WrongLength { expected, found } => {
write!(f, "expected {expected} words, found {found}")
}
Self::ChecksumMismatch { expected, found } => write!(
f,
"check word is `{}`, but the payload computes `{}`",
ALPHABET[*found as usize], ALPHABET[*expected as usize]
),
}
}
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Decode(error) => Some(error),
Self::WrongLength { .. } | Self::ChecksumMismatch { .. } => None,
}
}
}
impl From<DecodeError> for ParseError {
fn from(error: DecodeError) -> Self {
Self::Decode(error)
}
}
pub fn encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 8);
for (index, byte) in bytes.iter().enumerate() {
if index > 0 {
out.push(' ');
}
out.push_str(ALPHABET[*byte as usize]);
}
out
}
const WORD_PREVIEW: usize = 32;
fn preview(word: &str) -> String {
let mut out = String::with_capacity(WORD_PREVIEW);
for character in word.chars().take(WORD_PREVIEW) {
match character {
c if c.is_control() => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
c => out.push(c),
}
}
if word.chars().nth(WORD_PREVIEW).is_some() {
out.push('…');
}
out
}
fn classify(word: &str, position: usize) -> DecodeError {
if word.is_empty() {
return DecodeError::NotCanonical { position };
}
if word.is_ascii()
&& ALPHABET
.binary_search(&word.to_ascii_lowercase().as_str())
.is_ok()
{
return DecodeError::NotCanonical { position };
}
let mut pieces = recovered_words(word).peekable();
if pieces.peek().is_some()
&& pieces.all(|piece| {
ALPHABET
.binary_search(&piece.to_ascii_lowercase().as_str())
.is_ok()
})
{
return DecodeError::NotCanonical { position };
}
DecodeError::UnknownWord {
position,
word: preview(word),
}
}
fn recovered_words(text: &str) -> impl Iterator<Item = &str> {
text.split(|c: char| !c.is_ascii_alphabetic())
.filter(|word| !word.is_empty())
}
fn canonical_bytes(text: &str) -> impl Iterator<Item = Result<u8, DecodeError>> + '_ {
text.split(' ')
.enumerate()
.map(|(position, word)| match ALPHABET.binary_search(&word) {
Ok(index) => Ok(index as u8),
Err(_) => Err(classify(word, position)),
})
}
fn recovered_bytes(text: &str) -> impl Iterator<Item = Result<u8, DecodeError>> + '_ {
recovered_words(text).enumerate().map(|(position, word)| {
let lowered = word.to_ascii_lowercase();
match ALPHABET.binary_search(&lowered.as_str()) {
Ok(index) => Ok(index as u8),
Err(_) => Err(DecodeError::UnknownWord {
position,
word: preview(word),
}),
}
})
}
fn collect_exact<const N: usize>(
stream: impl Iterator<Item = Result<u8, DecodeError>>,
) -> Result<[u8; N], ParseError> {
let mut out = [0u8; N];
let mut found = 0usize;
for byte in stream {
let byte = byte?;
if let Some(slot) = out.get_mut(found) {
*slot = byte;
}
found += 1;
}
if found != N {
return Err(ParseError::WrongLength { expected: N, found });
}
Ok(out)
}
fn collect_checked<const N: usize>(
stream: impl Iterator<Item = Result<u8, DecodeError>>,
) -> Result<([u8; N], u8), ParseError> {
let mut payload = [0u8; N];
let mut check = 0u8;
let mut found = 0usize;
for byte in stream {
let byte = byte?;
if let Some(slot) = payload.get_mut(found) {
*slot = byte;
} else if found == N {
check = byte;
}
found += 1;
}
if found != N + 1 {
return Err(ParseError::WrongLength {
expected: N + 1,
found,
});
}
Ok((payload, check))
}
pub fn decode(text: &str) -> Result<Vec<u8>, DecodeError> {
if text.is_empty() {
return Err(DecodeError::Empty);
}
canonical_bytes(text).collect()
}
pub fn decode_recovered(text: &str) -> Result<Vec<u8>, DecodeError> {
let bytes: Vec<u8> = recovered_bytes(text).collect::<Result<_, _>>()?;
if bytes.is_empty() {
return Err(DecodeError::Empty);
}
Ok(bytes)
}
pub fn try_mint(bytes: usize) -> Result<String, getrandom::Error> {
let mut buffer = vec![0u8; bytes];
getrandom::fill(&mut buffer)?;
Ok(encode(&buffer))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_alphabet_is_sorted_unique_and_plain_lowercase() {
let mut sorted = ALPHABET;
sorted.sort_unstable();
assert_eq!(sorted, ALPHABET, "binary_search requires sorted order");
let unique: std::collections::HashSet<_> = ALPHABET.iter().collect();
assert_eq!(unique.len(), ALPHABET.len());
for word in ALPHABET {
assert!(
word.len() >= 4 && word.len() <= 11 && word.bytes().all(|b| b.is_ascii_lowercase()),
"`{word}`"
);
}
}
#[test]
fn the_alphabet_matches_its_frozen_digest() {
const FROZEN: u64 = 0x3e7c_f24c_a1a4_56f6;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for (index, word) in ALPHABET.iter().enumerate() {
if index > 0 {
hash = (hash ^ u64::from(b'\n')).wrapping_mul(PRIME);
}
for byte in word.bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(PRIME);
}
}
assert_eq!(
hash, FROZEN,
"the alphabet changed: every previously issued value now decodes differently"
);
}
#[test]
fn no_two_entries_are_within_one_edit_of_each_other() {
for (i, a) in ALPHABET.iter().enumerate() {
for b in &ALPHABET[i + 1..] {
assert!(!within_one_edit(a, b), "`{a}` and `{b}` are one edit apart");
}
}
}
fn within_one_edit(a: &str, b: &str) -> bool {
let (a, b) = if a.len() > b.len() { (b, a) } else { (a, b) };
let (short, long) = (a.as_bytes(), b.as_bytes());
match long.len() - short.len() {
0 => short.iter().zip(long).filter(|(x, y)| x != y).count() <= 1,
1 => {
let skip = short.iter().zip(long).take_while(|(x, y)| x == y).count();
short[skip..] == long[skip + 1..]
}
_ => false,
}
}
#[test]
fn no_entry_is_reachable_from_another_by_a_suffix() {
const SUFFIXES: [&str; 15] = [
"s", "es", "ing", "ed", "er", "ers", "ors", "ion", "ions", "ies", "ment", "ments",
"al", "ly", "y",
];
for entry in ALPHABET {
for suffix in SUFFIXES {
for stem in [
entry.to_string(),
entry.strip_suffix('e').unwrap_or(entry).to_string(),
format!("{}i", entry.strip_suffix('y').unwrap_or(entry)),
] {
let derived = format!("{stem}{suffix}");
if derived == entry {
continue;
}
assert!(
ALPHABET.binary_search(&derived.as_str()).is_err(),
"`{entry}` becomes `{derived}` by adding `{suffix}`, and both are entries"
);
}
}
}
}
#[test]
fn every_byte_round_trips() {
let all: Vec<u8> = (0..=255).collect();
assert_eq!(decode(&encode(&all)).unwrap(), all);
assert_eq!(decode_recovered(&encode(&all)).unwrap(), all);
}
#[test]
fn a_single_byte_round_trips_without_separators() {
let encoded = encode(&[7]);
assert!(!encoded.contains(' '));
assert_eq!(decode(&encoded).unwrap(), vec![7]);
}
#[test]
fn the_empty_encoding_is_refused_by_both_parsers() {
assert_eq!(encode(&[]), "");
assert_eq!(decode(""), Err(DecodeError::Empty));
assert_eq!(decode_recovered(""), Err(DecodeError::Empty));
assert_eq!(decode_recovered(" -- \n"), Err(DecodeError::Empty));
assert_eq!(try_mint(0).unwrap(), "");
}
#[test]
fn recovery_survives_the_mangling_a_round_trip_introduces() {
let bytes = [0x3d, 0x9a, 0x00, 0xff];
let encoded = encode(&bytes);
for mangled in [
encoded.to_uppercase(),
format!(" {encoded} "),
encoded.replace(' ', "-"),
encoded.replace(' ', ", "),
encoded.replace(' ', "\n"),
format!("\"{}\"", encoded.replace(' ', " ")),
] {
assert_eq!(decode_recovered(&mangled).unwrap(), bytes, "{mangled}");
}
}
#[test]
fn the_canonical_parser_refuses_what_recovery_accepts() {
let bytes = [0x3d, 0x9a, 0x00, 0xff];
let encoded = encode(&bytes);
for mangled in [
encoded.to_uppercase(),
format!(" {encoded} "),
encoded.replace(' ', "-"),
encoded.replace(' ', " "),
format!("\"{encoded}\""),
] {
assert!(decode(&mangled).is_err(), "canonical accepted `{mangled}`");
}
assert_eq!(decode(&encoded).unwrap(), bytes);
}
#[test]
fn a_case_slip_is_reported_as_non_canonical_not_unknown() {
let encoded = format!("{} {}", ALPHABET[1], ALPHABET[2].to_uppercase());
assert_eq!(
decode(&encoded),
Err(DecodeError::NotCanonical { position: 1 })
);
assert_eq!(decode_recovered(&encoded).unwrap(), vec![1, 2]);
}
#[test]
fn an_unknown_word_is_refused_and_named() {
let encoded = format!("{} zzzz {}", ALPHABET[1], ALPHABET[2]);
let expected = Err(DecodeError::UnknownWord {
position: 1,
word: "zzzz".to_string(),
});
assert_eq!(decode(&encoded), expected);
assert_eq!(decode_recovered(&encoded), expected);
}
#[test]
fn a_one_character_slip_is_refused_rather_than_read_as_another_byte() {
assert!(matches!(
decode_recovered("accesx"),
Err(DecodeError::UnknownWord { .. })
));
}
#[test]
fn recovery_accepts_ordinary_prose_made_of_alphabet_words() {
assert!(decode_recovered("error message").is_ok());
assert!(decode("error message").is_ok());
}
#[test]
fn an_id_round_trips_through_its_canonical_rendering() {
let id = UnigramId::from_bytes([0x3d, 0x9a, 0x00, 0xff]);
assert_eq!(id.to_string(), "created office access world");
assert_eq!(UnigramId::<4>::parse(&id.to_string()).unwrap(), id);
assert_eq!(
UnigramId::<4>::recover("CREATED-OFFICE-ACCESS-WORLD").unwrap(),
id
);
assert_eq!(id.as_bytes(), &[0x3d, 0x9a, 0x00, 0xff]);
assert_eq!(id.into_bytes(), [0x3d, 0x9a, 0x00, 0xff]);
}
#[test]
fn an_id_of_the_wrong_width_is_refused() {
let five = encode(&[1, 2, 3, 4, 5]);
assert_eq!(
UnigramId::<4>::parse(&five),
Err(ParseError::WrongLength {
expected: 4,
found: 5
})
);
assert!(UnigramId::<6>::parse(&five).is_err());
assert_eq!(
UnigramId::<5>::parse(&five).unwrap().as_bytes(),
&[1, 2, 3, 4, 5]
);
}
#[test]
fn a_random_id_is_the_requested_width_and_not_the_same_twice() {
let id: UnigramId<8> = UnigramId::try_random().unwrap();
assert_eq!(id.to_string().split(' ').count(), 8);
assert_eq!(UnigramId::<8>::parse(&id.to_string()).unwrap(), id);
assert_ne!(
UnigramId::<16>::try_random().unwrap(),
UnigramId::<16>::try_random().unwrap()
);
}
#[test]
fn minting_produces_one_word_per_requested_byte() {
let minted = try_mint(4).unwrap();
assert_eq!(minted.split(' ').count(), 4, "{minted}");
assert_eq!(decode(&minted).unwrap().len(), 4);
}
#[test]
fn the_check_word_catches_mutations_the_alphabet_cannot() {
let id = CheckedUnigramId::from_bytes([17u8, 42, 200, 7]);
let text = id.to_string();
assert_eq!(text.split(' ').count(), 5);
assert_eq!(CheckedUnigramId::<4>::parse(&text).unwrap(), id);
assert_eq!(id.as_bytes(), &[17u8, 42, 200, 7]);
let words: Vec<&str> = text.split(' ').collect();
let other = if words[0] == ALPHABET[0] {
ALPHABET[1]
} else {
ALPHABET[0]
};
let substituted = std::iter::once(other)
.chain(words[1..].iter().copied())
.collect::<Vec<_>>()
.join(" ");
let transposed = {
let mut w = words.clone();
w.swap(0, 1);
w.join(" ")
};
for mutation in [substituted, transposed] {
assert!(UnigramId::<5>::parse(&mutation).is_ok(), "{mutation}");
assert!(
matches!(
CheckedUnigramId::<4>::parse(&mutation),
Err(ParseError::ChecksumMismatch { .. })
),
"{mutation}"
);
}
assert!(matches!(
CheckedUnigramId::<4>::parse(&words[1..].join(" ")),
Err(ParseError::WrongLength { .. })
));
}
#[test]
fn a_single_word_substitution_is_always_caught() {
let id = CheckedUnigramId::from_bytes([3u8, 141, 92, 7, 220, 18]);
let text = id.to_string();
let words: Vec<&str> = text.split(' ').collect();
for position in 0..words.len() {
for replacement in ALPHABET {
if replacement == words[position] {
continue;
}
let mut mutated = words.clone();
mutated[position] = replacement;
assert!(
CheckedUnigramId::<6>::parse(&mutated.join(" ")).is_err(),
"substituting `{replacement}` at {position} went unnoticed"
);
}
}
}
#[test]
fn a_checked_value_survives_the_mangling_a_round_trip_introduces() {
let id: CheckedUnigramId<4> = CheckedUnigramId::try_random().unwrap();
let text = id.to_string();
assert_eq!(
CheckedUnigramId::<4>::recover(&text.to_uppercase().replace(' ', " - ")).unwrap(),
id
);
}
#[test]
fn spacing_and_separator_faults_are_reported_as_non_canonical() {
let good = encode(&[1, 2]);
for text in [
format!(" {good}"),
format!("{good} "),
good.replace(' ', " "),
good.replace(' ', "-"),
good.to_uppercase(),
] {
assert!(
matches!(decode(&text), Err(DecodeError::NotCanonical { .. })),
"`{text}` gave {:?}",
decode(&text)
);
}
assert!(matches!(
decode("account zzzz"),
Err(DecodeError::UnknownWord { .. })
));
}
#[test]
fn an_unknown_word_is_previewed_not_echoed() {
let huge = "q".repeat(10_000);
let Err(DecodeError::UnknownWord { word, .. }) = decode(&huge) else {
panic!("expected an unknown word");
};
assert!(word.chars().count() <= WORD_PREVIEW + 1, "{}", word.len());
let Err(DecodeError::UnknownWord { word, .. }) = decode("qqq\u{7}qqq") else {
panic!("expected an unknown word");
};
assert!(!word.contains('\u{7}'), "{word}");
}
#[test]
fn a_fixed_width_parse_reports_the_true_length_of_an_overlong_value() {
let long = encode(&vec![1u8; 5_000]);
assert_eq!(
UnigramId::<4>::parse(&long),
Err(ParseError::WrongLength {
expected: 4,
found: 5_000
})
);
}
#[test]
fn no_input_panics_either_parser() {
for text in [
"\u{0}",
"\u{200b}",
"🙂",
"access\u{200b}account",
&"a".repeat(10_000),
&"access ".repeat(1_000),
"-",
" ",
" access ",
] {
let _ = decode(text);
let _ = decode_recovered(text);
let _ = UnigramId::<4>::parse(text);
let _ = UnigramId::<4>::recover(text);
}
}
}