human_hash/
lib.rs

1//! Generate human readable digests for UUIDs
2//!
3//! Based on https://github.com/zacharyvoase/humanhash
4//! Should be compatible
5
6extern crate uuid;
7use uuid::Uuid;
8
9/// Class for custom humanhashers
10pub struct HumanHasher {
11    words: Wordlist,
12}
13
14/// List of 256 strings usable for human readable hash digests
15pub type Wordlist = &'static [&'static str; 256];
16
17/// Instance of human hasher with default wordlist
18const DEFAULT_HUMANIZER: HumanHasher = HumanHasher { words: DEFAULT_WORDLIST };
19
20/// Default Wordlist, chosen to match the original Python Human Hash
21pub const DEFAULT_WORDLIST: Wordlist = &[
22    "ack", "alabama", "alanine", "alaska", "alpha", "angel", "apart", "april",
23    "arizona", "arkansas", "artist", "asparagus", "aspen", "august", "autumn",
24    "avocado", "bacon", "bakerloo", "batman", "beer", "berlin", "beryllium",
25    "black", "blossom", "blue", "bluebird", "bravo", "bulldog", "burger",
26    "butter", "california", "carbon", "cardinal", "carolina", "carpet", "cat",
27    "ceiling", "charlie", "chicken", "coffee", "cola", "cold", "colorado",
28    "comet", "connecticut", "crazy", "cup", "dakota", "december", "delaware",
29    "delta", "diet", "don", "double", "early", "earth", "east", "echo",
30    "edward", "eight", "eighteen", "eleven", "emma", "enemy", "equal",
31    "failed", "fanta", "fifteen", "fillet", "finch", "fish", "five", "fix",
32    "floor", "florida", "football", "four", "fourteen", "foxtrot", "freddie",
33    "friend", "fruit", "gee", "georgia", "glucose", "golf", "green", "grey",
34    "hamper", "happy", "harry", "hawaii", "helium", "high", "hot", "hotel",
35    "hydrogen", "idaho", "illinois", "india", "indigo", "ink", "iowa",
36    "island", "item", "jersey", "jig", "johnny", "juliet", "july", "jupiter",
37    "kansas", "kentucky", "kilo", "king", "kitten", "lactose", "lake", "lamp",
38    "lemon", "leopard", "lima", "lion", "lithium", "london", "louisiana",
39    "low", "magazine", "magnesium", "maine", "mango", "march", "mars",
40    "maryland", "massachusetts", "may", "mexico", "michigan", "mike",
41    "minnesota", "mirror", "mississippi", "missouri", "mobile", "mockingbird",
42    "monkey", "montana", "moon", "mountain", "muppet", "music", "nebraska",
43    "neptune", "network", "nevada", "nine", "nineteen", "nitrogen", "north",
44    "november", "nuts", "october", "ohio", "oklahoma", "one", "orange",
45    "oranges", "oregon", "oscar", "oven", "oxygen", "papa", "paris", "pasta",
46    "pennsylvania", "pip", "pizza", "pluto", "potato", "princess", "purple",
47    "quebec", "queen", "quiet", "red", "river", "robert", "robin", "romeo",
48    "rugby", "sad", "salami", "saturn", "september", "seven", "seventeen",
49    "shade", "sierra", "single", "sink", "six", "sixteen", "skylark", "snake",
50    "social", "sodium", "solar", "south", "spaghetti", "speaker", "spring",
51    "stairway", "steak", "stream", "summer", "sweet", "table", "tango", "ten",
52    "tennessee", "tennis", "texas", "thirteen", "three", "timing", "triple",
53    "twelve", "twenty", "two", "uncle", "undress", "uniform", "uranus", "utah",
54    "vegan", "venus", "vermont", "victor", "video", "violet", "virginia",
55    "washington", "west", "whiskey", "white", "william", "winner", "winter",
56    "wisconsin", "wolfram", "wyoming", "xray", "yankee", "yellow", "zebra",
57    "zulu" ];
58
59/// Human Hasher
60impl HumanHasher {
61    /// Create a new hasher with a custom wordlist
62    pub fn new(words: Wordlist) -> HumanHasher {
63        HumanHasher { words: words }
64    }
65
66    /// Create a human readable digest for a UUID. Makes the collision space worse,
67    /// reducing it to 1:(2^(8*`words_out`)-1).
68    pub fn humanize(&self, uuid: &Uuid, words_out: usize) -> String {
69        compress(uuid.as_bytes(), words_out)
70            .iter()
71            .map(|&x| self.words[x as usize].to_string())
72            .collect::<Vec<String>>()
73            .join("-")
74    }
75}
76
77/// Break a slice of u8s into (at least) `target` `u8`s.
78///
79/// WARNING: If the slice is not evenly divisible, there will be one extra u8
80/// from the remainder. output `u8`s are created by XORing the input bytes.
81fn compress(bytes: &[u8], target: usize) -> Vec<u8> {
82    let seg_size = bytes.len() / target;
83    bytes.chunks(seg_size)
84        .map(|c| c.iter().fold(0u8, |acc, &x| acc ^ x))
85        .collect::<Vec<u8>>()
86}
87
88/// Create a human readable digest for a UUID. Makes the collision space worse,
89/// reducing it to 1:(2^(8*`words_out`)-1).
90pub fn humanize(uuid: &Uuid, words_out: usize) -> String {
91    DEFAULT_HUMANIZER.humanize(uuid, words_out)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::uuid::Uuid;
97    use super::DEFAULT_WORDLIST;
98    use super::{humanize, HumanHasher};
99
100    const TEST_UUID: &'static str = "bc0f47f93dd046578d7eee645999b95e";
101
102    #[test]
103    fn it_works() {
104        let tuid = Uuid::parse_str(TEST_UUID).unwrap();
105
106        assert_eq!(humanize(&tuid, 4), "august-yankee-lima-coffee");
107
108        assert_eq!("pip", humanize(&tuid, 1));
109        assert_eq!("washington-hot", humanize(&tuid, 2));
110        assert_eq!("august-yankee-lima-coffee", humanize(&tuid, 4));
111        assert_eq!("princess-sad-victor-bakerloo-whiskey-mike-saturn-uniform",
112                   humanize(&tuid, 8));
113
114    }
115
116    #[test]
117    fn class_works() {
118        let tuid = Uuid::parse_str(TEST_UUID).unwrap();
119
120        let hzr = HumanHasher::new(&DEFAULT_WORDLIST);
121
122        assert_eq!(humanize(&tuid, 4), hzr.humanize(&tuid, 4));
123    }
124}