visual-hashing 0.1.1

Human-friendly visual fingerprints for keys and checksums: a nameable 64-emoji BLAKE3 hash (emojihash) and OpenSSH-style drunken-bishop randomart
Documentation
// SPDX-FileCopyrightText: 2026 Blackcat Informaticsยฎ Inc. <paudley@blackcatinformatics.ca>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! A nameable 64-emoji visual hash.
//!
//! A BLAKE3-XOF digest is sliced into 6-bit symbols, each indexing a fixed
//! alphabet that favours common animals and then familiar foods over abstract,
//! confusable symbols โ€” a fingerprint only helps if a human can read it back.

/// The 64-entry emoji alphabet (a 6-bit digit set).
pub const EMOJI: [&str; 64] = [
    "๐Ÿต", "๐Ÿถ", "๐Ÿบ", "๐ŸฆŠ", "๐Ÿฑ", "๐Ÿฆ", "๐Ÿฏ", "๐Ÿด", "๐Ÿฆ„", "๐Ÿฆ“", "๐ŸฆŒ", "๐Ÿฎ", "๐Ÿท", "๐Ÿ—", "๐Ÿญ", "๐Ÿน",
    "๐Ÿฐ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿธ", "๐Ÿฒ", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ†", "๐Ÿฆ…", "๐Ÿฆ‰", "๐Ÿฆ‡", "๐Ÿข", "๐Ÿ", "๐ŸฆŽ", "๐ŸŠ",
    "๐Ÿณ", "๐Ÿฌ", "๐ŸŸ", "๐Ÿ ", "๐Ÿก", "๐Ÿฆˆ", "๐Ÿ™", "๐Ÿฆ‘", "๐Ÿฆ€", "๐Ÿฆž", "๐Ÿฆ", "๐Ÿฆ‹", "๐ŸŒ", "๐Ÿž", "๐Ÿ", "๐Ÿœ",
    "๐Ÿฆ‚", "๐ŸŽ", "๐Ÿ", "๐ŸŠ", "๐Ÿ‹", "๐ŸŒ", "๐Ÿ‰", "๐Ÿ‡", "๐Ÿ“", "๐Ÿ’", "๐Ÿ", "๐Ÿฅ", "๐Ÿ‘", "๐Ÿฅฅ", "๐Ÿฅ•", "๐ŸŒฝ",
];

/// The stable label for each entry in [`EMOJI`], by index.
pub const LABELS: [&str; 64] = [
    "monkey",
    "dog",
    "wolf",
    "fox",
    "cat",
    "lion",
    "tiger",
    "horse",
    "unicorn",
    "zebra",
    "deer",
    "cow",
    "pig",
    "boar",
    "mouse",
    "hamster",
    "rabbit",
    "bear",
    "panda",
    "koala",
    "frog",
    "dragon",
    "chicken",
    "penguin",
    "duck",
    "eagle",
    "owl",
    "bat",
    "turtle",
    "snake",
    "lizard",
    "crocodile",
    "whale",
    "dolphin",
    "fish",
    "tropical-fish",
    "blowfish",
    "shark",
    "octopus",
    "squid",
    "crab",
    "lobster",
    "shrimp",
    "butterfly",
    "snail",
    "lady-beetle",
    "bee",
    "ant",
    "scorpion",
    "apple",
    "pear",
    "orange",
    "lemon",
    "banana",
    "watermelon",
    "grapes",
    "strawberry",
    "cherries",
    "pineapple",
    "kiwi",
    "peach",
    "coconut",
    "carrot",
    "corn",
];

/// The number of distinct emoji digits (a 6-bit alphabet).
pub const ALPHABET_SIZE: usize = 64;

/// Return `length` 6-bit digest symbols (each in `0..64`).
pub fn emoji_indices(data: &[u8], length: usize) -> Vec<usize> {
    let wanted = length.max(1);
    let nbytes = (wanted * 6).div_ceil(8);
    let mut digest = vec![0u8; nbytes];
    blake3::Hasher::new()
        .update(data)
        .finalize_xof()
        .fill(&mut digest);

    let mut out = Vec::with_capacity(wanted);
    let mut acc: u64 = 0;
    let mut bits: u32 = 0;
    for byte in digest {
        acc = (acc << 8) | u64::from(byte);
        bits += 8;
        while bits >= 6 && out.len() < wanted {
            bits -= 6;
            out.push(((acc >> bits) & 0x3f) as usize);
        }
        acc &= (1u64 << bits) - 1; // keep only the unconsumed low bits
    }
    out.truncate(wanted);
    out
}

/// Map `data` to a space-joined string of `length` emoji digits.
pub fn emojihash(data: &[u8], length: usize) -> String {
    emoji_indices(data, length)
        .into_iter()
        .map(|i| EMOJI[i])
        .collect::<Vec<_>>()
        .join(" ")
}

/// The stable label names for [`emojihash`] output (space-joined).
pub fn emojihash_labels(data: &[u8], length: usize) -> String {
    emoji_indices(data, length)
        .into_iter()
        .map(|i| LABELS[i])
        .collect::<Vec<_>>()
        .join(" ")
}