uuid_readable_rs/
lib.rs

1//! Generate easy to remember sentences that acts as human readable UUIDs.
2//!
3//! - Built on UUID v4
4//! - Optionally pass your UUID to derive a sentence from it
5//! - Grammatically _correct_ sentences
6//! - Easy to remember (or at least part of it)
7//! - Size choice (32-bit token or 128-bit token using `short()` or `generate()` respectively)
8//!
9//! ## Security
10//! This project does not mean to be crypto safe! **Don't use this as a secure random generator**.
11//!
12//! - `25^12` possible combinations for `generate()` (uses 128-bit Token)
13//! - `25^5` possible combinations for `short()` (uses 32-bit Token)
14//!
15//! Note that the sentence generated by `generate()` and the original UUID form a bijection, hence no loss of entropy.
16//!
17//! ## Sentence generated
18//! For the **long** - aka `generate()` - version, a typical sentence generated by this lib looks like:
19//! ```text
20//! Wildon Mollie Behka the bubbler of Arecibo moaned Chavey Haney Torbart and 10 calm kingfishers
21//! ```
22//! Internally this correspond to:
23//! - 12 bits for a name
24//! - 11 bits for a name
25//! - 14 bits for a name
26//! - 13 bits for a personal noun
27//! - 13 bits for a place
28//! - 10 bits for a verb
29//! - 12 bits for a name
30//! - 11 bits for a name
31//! - 14 bits for a name
32//! - 5 bits for a number
33//! - 6 bits for an adjective
34//! - 7 bits for an animal
35//!
36//! > To ensure no loss of entropy, taking the example of the verb which represents 10 bits, this means that we used a list of verbs of at least 2^10 possibilities (1024).
37//!
38//! For the **short** - aka `short()` - version, a typical sentence looks like:
39//! ```text
40//! Zink recorded by 127 large armadillos
41//! ```
42//! This correspond to:
43//! - 6 bits for a name
44//! - 6 bits for a verb
45//! - 7 bits for a number
46//! - 8 bits for an adjective
47//! - 5 bits for an animal
48//!
49//! > Since the short version is 32 bits long and is derived from a 128-bit UUID, it is not considered as secure or as random as the long version may be. It also does not form any bijection with the original UUID.
50//!
51//! ## Example
52//! ```rust
53//! use uuid::Uuid;
54//! use uuid_readable_rs::{generate_from, short_from, generate, short, generate_inverse};
55//!
56//! // You can define your own UUID and pass it to uuid_readable_rs like so
57//! let uuid = Uuid::new_v4();
58//! let sentence_128: String = generate_from(uuid);
59//! let sentence_32: String = short_from(uuid);
60//!
61//! // You can also get an UUID from a sentence that was previously generated
62//! let original_uuid: Uuid = generate_inverse(sentence_128).unwrap();
63//! assert_eq!(uuid, original_uuid);
64//!
65//! // Or let uuid_readable_rs handle the Uuid generation
66//! let sentence_128: String = generate();
67//! let sentence_32: String = short();
68//! ```
69
70#[macro_use]
71extern crate anyhow;
72
73use anyhow::{Context, Result};
74use data::{
75    adjectives::ADJECTIVES, animals::ANIMALS, names::NAMES, personal_nouns::PERSONAL_NOUNS,
76    places::PLACES, verbs::VERBS,
77};
78use uuid::Uuid;
79
80mod data;
81
82// TODO - Add a reverse method for sentence -> uuid
83
84/// Mask used for the long version, this allow us to convert a 16 items
85/// totalling 128 bit into 12 items for the same number of bits.
86/// - 12 => 2**12 = 4096    ==> NAMES
87/// - 11 => 2**11 = 2048    ==> NAMES
88/// - 14 => 2**14 = 16384   ==> NAMES
89/// - 13 => 2**13 = 8192    ==> PERSONAL_NOUNS
90/// - 13 => 2**13 = 8192    ==> PLACES
91/// - 10 => 2**10 = 1024    ==> VERBS
92/// - 12 => 2**12 = 4096    ==> NAMES
93/// - 11 => 2**11 = 2048    ==> NAMES
94/// - 14 => 2**14 = 16384   ==> NAMES
95/// - 5  => 2**5  = 32      ==> MAX 32 as u8
96/// - 6  => 2**6  = 64      ==> ADJECTIVES
97/// - 7  => 2**7  = 128     ==> ANIMALS
98const NORMAL: [u8; 12] = [12, 11, 14, 13, 13, 10, 12, 11, 14, 5, 6, 7];
99
100/// Used for low entropy in the short methods. Higher chances of collisions
101/// between two generated sentences. 32 bit into 5 items.
102/// - 6 => 2**6 = 64        ==> NAMES
103/// - 6 => 2**6 = 64        ==> VERBS
104/// - 7 => 2**7 = 128       ==> MAX 128 as u8
105/// - 8 => 2**8 = 256       ==> ADJECTIVES
106/// - 5 => 2**5 = 32        ==> ANIMALS
107const SHORT: [u8; 5] = [6, 6, 7, 8, 5];
108
109/// Convert an array of bytes to a Vec of individuals bits (1-0)
110fn to_bits(bytes: &[u8]) -> Vec<u8> {
111    let mut bits: Vec<u8> = Vec::with_capacity(128);
112
113    for b in bytes {
114        bits.extend(u16_to_bits(*b as u16, 8));
115    }
116
117    bits
118}
119
120/// Convert an array of bytes to a Vec of individuals bits (1-0)
121fn to_bits_parted(bytes: &[u16]) -> Vec<u8> {
122    let mut bits: Vec<u8> = Vec::with_capacity(128);
123
124    for (i, b) in bytes.iter().enumerate() {
125        bits.extend(u16_to_bits(*b, NORMAL[i]));
126    }
127
128    bits
129}
130
131/// Helper used to convert a single digit (u16) into a Vec of individuals bits (1-0)
132#[inline]
133fn u16_to_bits(mut b: u16, length: u8) -> Vec<u8> {
134    let mut bits = Vec::with_capacity(length as usize);
135
136    for _ in 0..length {
137        bits.push((b % 2) as u8);
138        b >>= 1;
139    }
140    bits.reverse();
141
142    bits
143}
144
145/// Convert an array of individuals bits to a byte
146fn to_byte(bits: &[u8]) -> u16 {
147    let mut _byte = 0u16;
148
149    for b in bits {
150        _byte = 2 * _byte + *b as u16;
151    }
152    _byte
153}
154
155/// Convert bytes to bits and group them into 12 distinct numbers
156fn partition(parts: &[u8], bytes: &[u8]) -> [usize; 12] {
157    let mut bits: Vec<u8> = to_bits(bytes);
158
159    let mut _bytes: [usize; 12] = [0; 12];
160    for (idx, p) in parts.iter().enumerate() {
161        let tmp = bits.drain(0..(*p as usize));
162        _bytes[idx] = to_byte(tmp.as_slice()) as usize;
163    }
164
165    _bytes
166}
167
168/// Convert bits to bytes, grouping them 8 by 8 because it's u8
169fn de_partition(bits: &[u8]) -> [u8; 16] {
170    let mut bytes = [0; 16];
171
172    for i in 0..16 {
173        bytes[i] = to_byte(&bits[8 * i..8 * (i + 1)]) as u8;
174    }
175
176    bytes
177}
178
179#[inline]
180fn _generate(uuid: &Uuid) -> String {
181    // Convert the Uuid to an array of bytes
182    let uuid = uuid.as_bytes();
183    // Get the partition (it's basically random numbers (12) from the uuid)
184    let words = partition(&NORMAL, uuid);
185    // Generate the sentence and return it
186    format!(
187        "{} {} {} the {} of {} {} {} {} {} and {} {} {}",
188        NAMES[words[0]],
189        NAMES[words[1]],
190        NAMES[words[2]],
191        PERSONAL_NOUNS[words[3]],
192        PLACES[words[4]],
193        VERBS[words[5]],
194        NAMES[words[6]],
195        NAMES[words[7]],
196        NAMES[words[8]],
197        words[9],
198        ADJECTIVES[words[10]],
199        ANIMALS[words[11]]
200    )
201}
202
203/// Create a long sentence using a new random UUID.
204///
205/// Example of return: `Joy Bolt Kahler the avenger of Esbon jumped Carey Fatma Sander and 8 large ducks`
206pub fn generate() -> String {
207    // Generate a new Uuid using the v4 RFC
208    let uuid = Uuid::new_v4();
209
210    // Create the sentence from the Uuid
211    _generate(&uuid)
212}
213
214/// Derive a long sentence from a UUID.
215///
216/// Example of return: `Joy Bolt Kahler the avenger of Esbon jumped Carey Fatma Sander and 8 large ducks`
217pub fn generate_from(uuid: Uuid) -> String {
218    // Create the sentence from the Uuid
219    _generate(&uuid)
220}
221
222/// Get the original uuid from a sentence.
223///
224/// Example of return: `0ee001c7-12f3-4b29-a4cc-f48838b3587a`
225pub fn generate_inverse<S: AsRef<str>>(sentence: S) -> Result<Uuid> {
226    // Split the sentence
227    let splitted: Vec<&str> = sentence.as_ref().split(' ').collect();
228    // Sanity check that we have enough values to work with
229    if splitted.len() < 15 {
230        return Err(anyhow!(
231            "The sentence does not correspond to a one from uuid-readable-rs."
232        ));
233    }
234    // Collect the index of each parts
235    let index_values = [
236        NAMES
237            .iter()
238            .position(|&r| r == splitted[0])
239            .context("NAMES (0) not found")? as u16,
240        NAMES
241            .iter()
242            .position(|&r| r == splitted[1])
243            .context("NAMES (1) not found")? as u16,
244        NAMES
245            .iter()
246            .position(|&r| r == splitted[2])
247            .context("NAMES (2) not found")? as u16,
248        PERSONAL_NOUNS
249            .iter()
250            .position(|&r| r == splitted[4])
251            .context("PERSONAL_NOUNS (4) not found")? as u16,
252        PLACES
253            .iter()
254            .position(|&r| r == splitted[6])
255            .context("PLACES (6) not found")? as u16,
256        VERBS
257            .iter()
258            .position(|&r| r == splitted[7])
259            .context("VERBS (7) not found")? as u16,
260        NAMES
261            .iter()
262            .position(|&r| r == splitted[8])
263            .context("NAMES (8) not found")? as u16,
264        NAMES
265            .iter()
266            .position(|&r| r == splitted[9])
267            .context("NAMES (9) not found")? as u16,
268        NAMES
269            .iter()
270            .position(|&r| r == splitted[10])
271            .context("NAMES (10) not found")? as u16,
272        splitted[12].parse::<u16>()?,
273        ADJECTIVES
274            .iter()
275            .position(|&r| r == splitted[13])
276            .context("ADJECTIVES (13) not found")? as u16,
277        ANIMALS
278            .iter()
279            .position(|&r| r == splitted[14])
280            .context("ANIMALS (14) not found")? as u16,
281    ];
282    // Convert the index into bits
283    let bits = to_bits_parted(&index_values);
284    // Convert the bits to bytes
285    let bytes = de_partition(&bits);
286
287    // Convert the bytes into the Uuid
288    Ok(Uuid::from_slice(&bytes)?)
289}
290
291#[inline]
292fn _short(uuid: &Uuid) -> String {
293    // Convert the Uuid to an array of bytes
294    let uuid = uuid.as_bytes();
295    // Get the partition (it's basically random numbers (12) from the uuid)
296    let words = partition(&SHORT, uuid);
297
298    // Generate the sentence and return it
299    format!(
300        "{} {} by {} {} {}",
301        NAMES[words[0]], VERBS[words[1]], words[2], ADJECTIVES[words[3]], ANIMALS[words[4]],
302    )
303}
304
305/// Create a short sentence using a new random UUID.
306///
307/// Example of return: `Alex sang by 60 narrow chickens`
308pub fn short() -> String {
309    // Generate a new Uuid using the v4 RFC
310    let uuid = Uuid::new_v4();
311
312    // Create the sentence from the Uuid
313    _short(&uuid)
314}
315
316/// Derive a short sentence from a UUID.
317///
318/// Example of return: `Alex sang by 60 narrow chickens`
319pub fn short_from(uuid: Uuid) -> String {
320    // Create the sentence from the Uuid
321    _short(&uuid)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_adjectives_sanity() {
330        let mut tmp_vec = ADJECTIVES.to_vec();
331
332        for x in &tmp_vec {
333            assert!(!x.contains(" "));
334        }
335
336        let original_length = tmp_vec.len();
337        tmp_vec.sort();
338        tmp_vec.dedup();
339        let final_length = tmp_vec.len();
340
341        assert_eq!(original_length, final_length);
342    }
343
344    #[test]
345    fn test_animals_sanity() {
346        let mut tmp_vec = ANIMALS.to_vec();
347
348        for x in &tmp_vec {
349            assert!(!x.contains(" "));
350        }
351
352        let original_length = tmp_vec.len();
353        tmp_vec.sort();
354        tmp_vec.dedup();
355        let final_length = tmp_vec.len();
356
357        assert_eq!(original_length, final_length);
358    }
359
360    #[test]
361    fn test_names_sanity() {
362        let mut tmp_vec = NAMES.to_vec();
363
364        for x in &tmp_vec {
365            assert!(!x.contains(" "));
366        }
367
368        let original_length = tmp_vec.len();
369        tmp_vec.sort();
370        tmp_vec.dedup();
371        let final_length = tmp_vec.len();
372
373        assert_eq!(original_length, final_length);
374    }
375
376    #[test]
377    fn test_personal_nouns_sanity() {
378        let mut tmp_vec = PERSONAL_NOUNS.to_vec();
379
380        for x in &tmp_vec {
381            assert!(!x.contains(" "));
382        }
383
384        let original_length = tmp_vec.len();
385        tmp_vec.sort();
386        tmp_vec.dedup();
387        let final_length = tmp_vec.len();
388
389        assert_eq!(original_length, final_length);
390    }
391
392    #[test]
393    fn test_places_sanity() {
394        let mut tmp_vec = PLACES.to_vec();
395
396        for x in &tmp_vec {
397            assert!(!x.contains(" "));
398        }
399
400        let original_length = tmp_vec.len();
401        tmp_vec.sort();
402        tmp_vec.dedup();
403        let final_length = tmp_vec.len();
404
405        assert_eq!(original_length, final_length);
406    }
407
408    #[test]
409    fn test_verbs_sanity() {
410        let mut tmp_vec = VERBS.to_vec();
411
412        for x in &tmp_vec {
413            assert!(!x.contains(" "));
414        }
415
416        let original_length = tmp_vec.len();
417        tmp_vec.sort();
418        tmp_vec.dedup();
419        let final_length = tmp_vec.len();
420
421        assert_eq!(original_length, final_length);
422    }
423
424    #[test]
425    fn test_generate() {
426        let uuid = Uuid::parse_str("0ee001c7-12f3-4b29-a4cc-f48838b3587a").unwrap();
427
428        let g = generate_from(uuid);
429        assert_eq!(
430            g,
431            "Purdy Fusco Kask the loki of Manteo observed Barbe Lehet Pardew and 26 hard herons"
432        );
433    }
434
435    #[test]
436    fn test_short() {
437        let uuid = Uuid::parse_str("0ee001c7-12f3-4b29-a4cc-f48838b3587a").unwrap();
438
439        let s = short_from(uuid);
440        assert_eq!(s, "Egidius filled by 0 calm hawks");
441    }
442
443    #[test]
444    fn test_inverse() {
445        let uuid = Uuid::parse_str("0ee001c7-12f3-4b29-a4cc-f48838b3587a").unwrap();
446        let i = generate_inverse(&generate_from(uuid)).unwrap();
447        assert_eq!(i, uuid);
448    }
449
450    #[test]
451    fn test_bits_conversion() {
452        let arr = [41];
453        let bits = to_bits(&arr);
454        assert_eq!(bits, vec![0, 0, 1, 0, 1, 0, 0, 1]);
455
456        let byte = to_byte(&bits);
457        assert_eq!(byte, 41);
458    }
459
460    #[test]
461    fn test_compatibility() {
462        let uuid = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
463        let zeroed = generate_from(uuid);
464        assert_eq!(zeroed, "Fusco Fusco Fusco the muleteer of Katy suspended Fusco Fusco Fusco and 0 mysterious rooks");
465
466        let uuid = Uuid::parse_str("ffffffff-ffff-ffff-ffff-ffffffffffff").unwrap();
467        let full = generate_from(uuid);
468        assert_eq!(full, "Antone Concordia Katharyn the minister of Mosinee trotted Antone Concordia Katharyn and 31 slow hogs");
469
470        let uuid = Uuid::parse_str("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF").unwrap();
471        let strange = generate_from(uuid);
472        assert_eq!(strange, "Antone Concordia Caravette the minister of Mosinee trotted Antone Concordia Katharyn and 31 slow hogs");
473    }
474
475    #[test]
476    fn test_bad_inverse() {
477        let sentence = "109812 ???./ ` the muleteer of Katy suspended Fusco Fusco Fusco and 0 mysterious rooks";
478        let rev = generate_inverse(sentence);
479        assert!(rev.is_err());
480
481        let sentence = "109812 ???./\0zdqdqz";
482        let rev = generate_inverse(sentence);
483        assert!(rev.is_err());
484    }
485}