use crate::dictionary4k::DICTIONARY;
use crate::error::{FourWordError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdentityWords {
Agent { words: [String; 4] },
Full {
agent_words: [String; 4],
user_words: [String; 4],
},
}
impl IdentityWords {
pub fn agent_words(&self) -> &[String; 4] {
match self {
IdentityWords::Agent { words } => words,
IdentityWords::Full { agent_words, .. } => agent_words,
}
}
pub fn user_words(&self) -> Option<&[String; 4]> {
match self {
IdentityWords::Agent { .. } => None,
IdentityWords::Full { user_words, .. } => Some(user_words),
}
}
pub fn is_full(&self) -> bool {
matches!(self, IdentityWords::Full { .. })
}
pub fn word_count(&self) -> usize {
match self {
IdentityWords::Agent { .. } => 4,
IdentityWords::Full { .. } => 8,
}
}
}
impl std::fmt::Display for IdentityWords {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IdentityWords::Agent { words } => {
write!(f, "{}", words.join(" "))
}
IdentityWords::Full {
agent_words,
user_words,
} => {
write!(f, "{} @ {}", agent_words.join(" "), user_words.join(" "))
}
}
}
}
pub struct IdentityEncoder;
impl IdentityEncoder {
pub fn new() -> Self {
IdentityEncoder
}
fn encode_hash_prefix(&self, hash: &[u8]) -> Result<[String; 4]> {
if hash.len() < 6 {
return Err(FourWordError::InvalidInput(format!(
"Hash must be at least 6 bytes (48 bits), got {} bytes",
hash.len()
)));
}
let mut n: u64 = 0;
for &byte in &hash[..6] {
n = (n << 8) | (byte as u64);
}
let mut words = Vec::with_capacity(4);
for i in (0..4).rev() {
let index = ((n >> (i * 12)) & 0xFFF) as u16;
let word = DICTIONARY
.get_word(index)
.ok_or(FourWordError::InvalidWordIndex(index))?
.to_string();
words.push(word);
}
Ok([
words[0].clone(),
words[1].clone(),
words[2].clone(),
words[3].clone(),
])
}
pub fn decode_to_prefix(&self, identity: &str) -> Result<[u8; 6]> {
let words: Vec<&str> = identity.split_whitespace().collect();
if words.len() != 4 {
return Err(FourWordError::InvalidInput(format!(
"Expected 4 words, got {}",
words.len()
)));
}
self.decode_words_to_prefix(&words)
}
fn decode_words_to_prefix(&self, words: &[&str]) -> Result<[u8; 6]> {
if words.len() != 4 {
return Err(FourWordError::InvalidInput(format!(
"Expected 4 words, got {}",
words.len()
)));
}
let mut n: u64 = 0;
for word in words {
let index = DICTIONARY
.get_index(word)
.ok_or_else(|| FourWordError::InvalidWord(word.to_string()))?;
n = (n << 12) | (index as u64);
}
let mut prefix = [0u8; 6];
for (i, byte) in prefix.iter_mut().enumerate() {
*byte = ((n >> (40 - i * 8)) & 0xFF) as u8;
}
Ok(prefix)
}
pub fn encode_agent(&self, agent_id: &[u8]) -> Result<IdentityWords> {
let words = self.encode_hash_prefix(agent_id)?;
Ok(IdentityWords::Agent { words })
}
pub fn encode_full(&self, agent_id: &[u8], user_id: &[u8]) -> Result<IdentityWords> {
let agent_words = self.encode_hash_prefix(agent_id)?;
let user_words = self.encode_hash_prefix(user_id)?;
Ok(IdentityWords::Full {
agent_words,
user_words,
})
}
pub fn encode_hex(&self, hex_str: &str) -> Result<IdentityWords> {
let bytes = hex::decode(hex_str.trim())
.map_err(|e| FourWordError::InvalidInput(format!("Invalid hex string: {e}")))?;
self.encode_agent(&bytes)
}
pub fn encode_hex_full(&self, agent_hex: &str, user_hex: &str) -> Result<IdentityWords> {
let agent_bytes = hex::decode(agent_hex.trim())
.map_err(|e| FourWordError::InvalidInput(format!("Invalid agent hex: {e}")))?;
let user_bytes = hex::decode(user_hex.trim())
.map_err(|e| FourWordError::InvalidInput(format!("Invalid user hex: {e}")))?;
self.encode_full(&agent_bytes, &user_bytes)
}
pub fn parse(&self, input: &str) -> Result<IdentityWords> {
if input.contains('@') {
let parts: Vec<&str> = input.split('@').collect();
if parts.len() != 2 {
return Err(FourWordError::InvalidInput(
"Full identity must have exactly one '@' separator".to_string(),
));
}
let agent_words: Vec<&str> = parts[0].split_whitespace().collect();
let user_words: Vec<&str> = parts[1].split_whitespace().collect();
if agent_words.len() != 4 {
return Err(FourWordError::InvalidInput(format!(
"Agent part must have 4 words, got {}",
agent_words.len()
)));
}
if user_words.len() != 4 {
return Err(FourWordError::InvalidInput(format!(
"User part must have 4 words, got {}",
user_words.len()
)));
}
for word in agent_words.iter().chain(user_words.iter()) {
if DICTIONARY.get_index(word).is_none() {
return Err(FourWordError::InvalidWord(word.to_string()));
}
}
Ok(IdentityWords::Full {
agent_words: [
agent_words[0].to_lowercase(),
agent_words[1].to_lowercase(),
agent_words[2].to_lowercase(),
agent_words[3].to_lowercase(),
],
user_words: [
user_words[0].to_lowercase(),
user_words[1].to_lowercase(),
user_words[2].to_lowercase(),
user_words[3].to_lowercase(),
],
})
} else {
let words: Vec<&str> = input.split_whitespace().collect();
if words.len() != 4 {
return Err(FourWordError::InvalidInput(format!(
"Agent identity must have 4 words, got {}",
words.len()
)));
}
for word in &words {
if DICTIONARY.get_index(word).is_none() {
return Err(FourWordError::InvalidWord(word.to_string()));
}
}
Ok(IdentityWords::Agent {
words: [
words[0].to_lowercase(),
words[1].to_lowercase(),
words[2].to_lowercase(),
words[3].to_lowercase(),
],
})
}
}
pub fn matches(&self, hash: &[u8], words: &str) -> Result<bool> {
let prefix = self.decode_to_prefix(words)?;
if hash.len() < 6 {
return Ok(false);
}
Ok(hash[..6] == prefix[..])
}
}
impl Default for IdentityEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
const BEN_AGENT_ID: &str = "dd6530452610619d468e4e82be82107e86384365c58efa6e3018d7762c7368da";
const DAVID_VPS_AGENT_ID: &str =
"da2233d6ba2f95696e5f5ba3bc4db193be1aa53d7ce1c048a8e8a67639337b75";
const THIRD_AGENT_ID: &str = "3e729de0469a594d1e042a672b29adde388e34aed2ced1e4c244a87f03053770";
#[test]
fn test_encode_agent_id() {
let encoder = IdentityEncoder::new();
let bytes = hex::decode(BEN_AGENT_ID).unwrap();
let identity = encoder.encode_agent(&bytes).unwrap();
assert!(matches!(identity, IdentityWords::Agent { .. }));
assert_eq!(identity.word_count(), 4);
let display = identity.to_string();
let words: Vec<&str> = display.split_whitespace().collect();
assert_eq!(words.len(), 4);
for word in &words {
assert!(
DICTIONARY.get_index(word).is_some(),
"Word '{}' not in dictionary",
word
);
}
println!("Ben's agent: {}", identity);
}
#[test]
fn test_encode_all_network_agents() {
let encoder = IdentityEncoder::new();
let agents = [
("Ben", BEN_AGENT_ID),
("David VPS", DAVID_VPS_AGENT_ID),
("Third", THIRD_AGENT_ID),
];
let mut seen = std::collections::HashSet::new();
for (name, hex_id) in &agents {
let identity = encoder.encode_hex(hex_id).unwrap();
let display = identity.to_string();
println!("{}: {} -> {}", name, &hex_id[..16], display);
assert!(
seen.insert(display.clone()),
"Collision detected for {}",
name
);
}
}
#[test]
fn test_round_trip_prefix() {
let encoder = IdentityEncoder::new();
let bytes = hex::decode(BEN_AGENT_ID).unwrap();
let identity = encoder.encode_agent(&bytes).unwrap();
let prefix = encoder.decode_to_prefix(&identity.to_string()).unwrap();
assert_eq!(&bytes[..6], &prefix[..]);
}
#[test]
fn test_round_trip_all_agents() {
let encoder = IdentityEncoder::new();
for hex_id in [BEN_AGENT_ID, DAVID_VPS_AGENT_ID, THIRD_AGENT_ID] {
let bytes = hex::decode(hex_id).unwrap();
let identity = encoder.encode_agent(&bytes).unwrap();
let prefix = encoder.decode_to_prefix(&identity.to_string()).unwrap();
assert_eq!(
&bytes[..6],
&prefix[..],
"Round-trip failed for {}",
&hex_id[..16]
);
}
}
#[test]
fn test_full_identity() {
let encoder = IdentityEncoder::new();
let full = encoder
.encode_hex_full(BEN_AGENT_ID, THIRD_AGENT_ID)
.unwrap();
assert!(full.is_full());
assert_eq!(full.word_count(), 8);
let display = full.to_string();
assert!(display.contains(" @ "), "Full identity must contain ' @ '");
let parts: Vec<&str> = display.split(" @ ").collect();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].split_whitespace().count(), 4);
assert_eq!(parts[1].split_whitespace().count(), 4);
println!("Full identity: {}", full);
}
#[test]
fn test_parse_agent_identity() {
let encoder = IdentityEncoder::new();
let bytes = hex::decode(BEN_AGENT_ID).unwrap();
let identity = encoder.encode_agent(&bytes).unwrap();
let display = identity.to_string();
let parsed = encoder.parse(&display).unwrap();
assert_eq!(identity, parsed);
}
#[test]
fn test_parse_full_identity() {
let encoder = IdentityEncoder::new();
let full = encoder
.encode_hex_full(BEN_AGENT_ID, THIRD_AGENT_ID)
.unwrap();
let display = full.to_string();
let parsed = encoder.parse(&display).unwrap();
assert_eq!(full, parsed);
}
#[test]
fn test_matches() {
let encoder = IdentityEncoder::new();
let bytes = hex::decode(BEN_AGENT_ID).unwrap();
let identity = encoder.encode_agent(&bytes).unwrap();
let display = identity.to_string();
assert!(encoder.matches(&bytes, &display).unwrap());
let other_bytes = hex::decode(DAVID_VPS_AGENT_ID).unwrap();
assert!(!encoder.matches(&other_bytes, &display).unwrap());
}
#[test]
fn test_different_agents_different_words() {
let encoder = IdentityEncoder::new();
let ben = encoder.encode_hex(BEN_AGENT_ID).unwrap().to_string();
let david = encoder.encode_hex(DAVID_VPS_AGENT_ID).unwrap().to_string();
let third = encoder.encode_hex(THIRD_AGENT_ID).unwrap().to_string();
assert_ne!(ben, david);
assert_ne!(ben, third);
assert_ne!(david, third);
}
#[test]
fn test_deterministic() {
let encoder = IdentityEncoder::new();
let a = encoder.encode_hex(BEN_AGENT_ID).unwrap().to_string();
let b = encoder.encode_hex(BEN_AGENT_ID).unwrap().to_string();
assert_eq!(a, b);
}
#[test]
fn test_family_name_pattern() {
let encoder = IdentityEncoder::new();
let full1 = encoder
.encode_hex_full(BEN_AGENT_ID, THIRD_AGENT_ID)
.unwrap();
let full2 = encoder
.encode_hex_full(DAVID_VPS_AGENT_ID, THIRD_AGENT_ID)
.unwrap();
assert_ne!(full1.agent_words(), full2.agent_words());
assert_eq!(full1.user_words(), full2.user_words());
println!("Agent 1: {}", full1);
println!("Agent 2: {}", full2);
println!(
"Same family name: {}",
full1.user_words().unwrap().join(" ")
);
}
#[test]
fn test_short_hash_rejected() {
let encoder = IdentityEncoder::new();
let short = vec![0u8; 5]; assert!(encoder.encode_agent(&short).is_err());
}
#[test]
fn test_invalid_word_rejected() {
let encoder = IdentityEncoder::new();
assert!(encoder.parse("not real words here").is_err());
}
#[test]
fn test_wrong_word_count_rejected() {
let encoder = IdentityEncoder::new();
let word = DICTIONARY.get_word(0).unwrap();
let three_words = format!("{} {} {}", word, word, word);
assert!(encoder.parse(&three_words).is_err());
}
}