Skip to main content

murk_cli/
codename.rs

1//! Computed codenames for vault files.
2//!
3//! Deterministically derives a memorable name (e.g. "breezy-mocha-goblin")
4//! from file contents. Same bytes → same name. Different bytes → different name.
5//! Format: adjective-color-animal.
6
7use sha2::{Digest, Sha256};
8
9const ADJECTIVES: &[&str] = &[
10    "ancient", "bold", "breezy", "bright", "calm", "clever", "cosmic", "crisp", "daring", "dusty",
11    "eager", "faded", "fierce", "gentle", "golden", "gusty", "hasty", "hidden", "hollow", "icy",
12    "jolly", "keen", "lazy", "lively", "lucky", "mighty", "misty", "narrow", "nimble", "noble",
13    "noisy", "odd", "pale", "plain", "proud", "quick", "quiet", "rapid", "rocky", "rosy", "rough",
14    "rusty", "sandy", "sharp", "shy", "silent", "sleek", "snowy", "solar", "spicy", "steep",
15    "stormy", "stout", "sunny", "swift", "tame", "thorny", "tidy", "tough", "twisted", "vast",
16    "vivid", "warm", "wild", "windy", "wispy", "witty", "young", "zesty",
17];
18
19const COLORS: &[&str] = &[
20    "amber", "aqua", "azure", "beige", "black", "blue", "brass", "bronze", "brown", "cedar",
21    "charcoal", "cherry", "cobalt", "copper", "coral", "cream", "crimson", "cyan", "ebony",
22    "ember", "garnet", "gold", "granite", "green", "grey", "hazel", "indigo", "ivory", "jade",
23    "khaki", "lemon", "lilac", "lime", "maple", "maroon", "mauve", "mint", "mocha", "navy",
24    "ochre", "olive", "onyx", "orange", "orchid", "peach", "pearl", "pine", "plum", "quartz",
25    "rose", "ruby", "rust", "sage", "sand", "scarlet", "silver", "slate", "smoke", "steel",
26    "stone", "tan", "teal", "topaz", "umber", "violet", "walnut", "wheat", "white", "wine",
27];
28
29const ANIMALS: &[&str] = &[
30    "badger", "bat", "bear", "beetle", "bison", "bobcat", "cobra", "condor", "crane", "crow",
31    "deer", "dingo", "eagle", "elk", "falcon", "ferret", "finch", "fox", "gecko", "goat", "goblin",
32    "grouse", "hare", "hawk", "heron", "horse", "ibex", "iguana", "jackal", "jay", "koala",
33    "lemur", "lion", "lizard", "llama", "lynx", "marten", "moose", "moth", "newt", "otter", "owl",
34    "panda", "parrot", "pelican", "pike", "puma", "quail", "rabbit", "raccoon", "raven", "robin",
35    "salmon", "shark", "snail", "snake", "sparrow", "squid", "stork", "swan", "tiger", "toad",
36    "toucan", "turtle", "viper", "walrus", "wasp", "whale", "wolf", "wren",
37];
38
39/// Derive a three-word codename from raw bytes.
40///
41/// Hashes the input with SHA-256 and uses different portions of the digest
42/// to index into each word list. Deterministic: same input → same codename.
43pub fn from_bytes(data: &[u8]) -> String {
44    let hash = Sha256::digest(data);
45
46    // Use separate parts of the hash for each word to avoid correlation.
47    let adj_idx = u16::from_le_bytes([hash[0], hash[1]]) as usize % ADJECTIVES.len();
48    let color_idx = u16::from_le_bytes([hash[2], hash[3]]) as usize % COLORS.len();
49    let animal_idx = u16::from_le_bytes([hash[4], hash[5]]) as usize % ANIMALS.len();
50
51    format!(
52        "{}-{}-{}",
53        ADJECTIVES[adj_idx], COLORS[color_idx], ANIMALS[animal_idx]
54    )
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn deterministic() {
63        assert_eq!(from_bytes(b"hello"), from_bytes(b"hello"));
64    }
65
66    #[test]
67    fn different_input_different_name() {
68        assert_ne!(from_bytes(b"hello"), from_bytes(b"world"));
69    }
70
71    #[test]
72    fn three_words() {
73        let name = from_bytes(b"test");
74        assert_eq!(name.split('-').count(), 3);
75    }
76
77    #[test]
78    fn not_empty() {
79        let name = from_bytes(b"");
80        assert!(!name.is_empty());
81        assert!(name.len() > 5);
82    }
83}