1use crate::dictionary4k::DICTIONARY;
56use crate::error::{FourWordError, Result};
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum IdentityWords {
61 Agent { words: [String; 4] },
63 Full {
65 agent_words: [String; 4],
66 user_words: [String; 4],
67 },
68}
69
70impl IdentityWords {
71 pub fn agent_words(&self) -> &[String; 4] {
73 match self {
74 IdentityWords::Agent { words } => words,
75 IdentityWords::Full { agent_words, .. } => agent_words,
76 }
77 }
78
79 pub fn user_words(&self) -> Option<&[String; 4]> {
81 match self {
82 IdentityWords::Agent { .. } => None,
83 IdentityWords::Full { user_words, .. } => Some(user_words),
84 }
85 }
86
87 pub fn is_full(&self) -> bool {
89 matches!(self, IdentityWords::Full { .. })
90 }
91
92 pub fn word_count(&self) -> usize {
94 match self {
95 IdentityWords::Agent { .. } => 4,
96 IdentityWords::Full { .. } => 8,
97 }
98 }
99}
100
101impl std::fmt::Display for IdentityWords {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 IdentityWords::Agent { words } => {
105 write!(f, "{}", words.join(" "))
106 }
107 IdentityWords::Full {
108 agent_words,
109 user_words,
110 } => {
111 write!(f, "{} @ {}", agent_words.join(" "), user_words.join(" "))
112 }
113 }
114 }
115}
116
117pub struct IdentityEncoder;
122
123impl IdentityEncoder {
124 pub fn new() -> Self {
126 IdentityEncoder
127 }
128
129 fn encode_hash_prefix(&self, hash: &[u8]) -> Result<[String; 4]> {
135 if hash.len() < 6 {
136 return Err(FourWordError::InvalidInput(format!(
137 "Hash must be at least 6 bytes (48 bits), got {} bytes",
138 hash.len()
139 )));
140 }
141
142 let mut n: u64 = 0;
144 for &byte in &hash[..6] {
145 n = (n << 8) | (byte as u64);
146 }
147
148 let mut words = Vec::with_capacity(4);
150 for i in (0..4).rev() {
151 let index = ((n >> (i * 12)) & 0xFFF) as u16;
152 let word = DICTIONARY
153 .get_word(index)
154 .ok_or(FourWordError::InvalidWordIndex(index))?
155 .to_string();
156 words.push(word);
157 }
158
159 Ok([
160 words[0].clone(),
161 words[1].clone(),
162 words[2].clone(),
163 words[3].clone(),
164 ])
165 }
166
167 pub fn decode_to_prefix(&self, identity: &str) -> Result<[u8; 6]> {
172 let words: Vec<&str> = identity.split_whitespace().collect();
173 if words.len() != 4 {
174 return Err(FourWordError::InvalidInput(format!(
175 "Expected 4 words, got {}",
176 words.len()
177 )));
178 }
179
180 self.decode_words_to_prefix(&words)
181 }
182
183 fn decode_words_to_prefix(&self, words: &[&str]) -> Result<[u8; 6]> {
185 if words.len() != 4 {
186 return Err(FourWordError::InvalidInput(format!(
187 "Expected 4 words, got {}",
188 words.len()
189 )));
190 }
191
192 let mut n: u64 = 0;
194 for word in words {
195 let index = DICTIONARY
196 .get_index(word)
197 .ok_or_else(|| FourWordError::InvalidWord(word.to_string()))?;
198 n = (n << 12) | (index as u64);
199 }
200
201 let mut prefix = [0u8; 6];
203 for (i, byte) in prefix.iter_mut().enumerate() {
204 *byte = ((n >> (40 - i * 8)) & 0xFF) as u8;
205 }
206
207 Ok(prefix)
208 }
209
210 pub fn encode_agent(&self, agent_id: &[u8]) -> Result<IdentityWords> {
215 let words = self.encode_hash_prefix(agent_id)?;
216 Ok(IdentityWords::Agent { words })
217 }
218
219 pub fn encode_full(&self, agent_id: &[u8], user_id: &[u8]) -> Result<IdentityWords> {
224 let agent_words = self.encode_hash_prefix(agent_id)?;
225 let user_words = self.encode_hash_prefix(user_id)?;
226 Ok(IdentityWords::Full {
227 agent_words,
228 user_words,
229 })
230 }
231
232 pub fn encode_hex(&self, hex_str: &str) -> Result<IdentityWords> {
236 let bytes = hex::decode(hex_str.trim())
237 .map_err(|e| FourWordError::InvalidInput(format!("Invalid hex string: {e}")))?;
238 self.encode_agent(&bytes)
239 }
240
241 pub fn encode_hex_full(&self, agent_hex: &str, user_hex: &str) -> Result<IdentityWords> {
243 let agent_bytes = hex::decode(agent_hex.trim())
244 .map_err(|e| FourWordError::InvalidInput(format!("Invalid agent hex: {e}")))?;
245 let user_bytes = hex::decode(user_hex.trim())
246 .map_err(|e| FourWordError::InvalidInput(format!("Invalid user hex: {e}")))?;
247 self.encode_full(&agent_bytes, &user_bytes)
248 }
249
250 pub fn parse(&self, input: &str) -> Result<IdentityWords> {
256 if input.contains('@') {
257 let parts: Vec<&str> = input.split('@').collect();
259 if parts.len() != 2 {
260 return Err(FourWordError::InvalidInput(
261 "Full identity must have exactly one '@' separator".to_string(),
262 ));
263 }
264
265 let agent_words: Vec<&str> = parts[0].split_whitespace().collect();
266 let user_words: Vec<&str> = parts[1].split_whitespace().collect();
267
268 if agent_words.len() != 4 {
269 return Err(FourWordError::InvalidInput(format!(
270 "Agent part must have 4 words, got {}",
271 agent_words.len()
272 )));
273 }
274 if user_words.len() != 4 {
275 return Err(FourWordError::InvalidInput(format!(
276 "User part must have 4 words, got {}",
277 user_words.len()
278 )));
279 }
280
281 for word in agent_words.iter().chain(user_words.iter()) {
283 if DICTIONARY.get_index(word).is_none() {
284 return Err(FourWordError::InvalidWord(word.to_string()));
285 }
286 }
287
288 Ok(IdentityWords::Full {
289 agent_words: [
290 agent_words[0].to_lowercase(),
291 agent_words[1].to_lowercase(),
292 agent_words[2].to_lowercase(),
293 agent_words[3].to_lowercase(),
294 ],
295 user_words: [
296 user_words[0].to_lowercase(),
297 user_words[1].to_lowercase(),
298 user_words[2].to_lowercase(),
299 user_words[3].to_lowercase(),
300 ],
301 })
302 } else {
303 let words: Vec<&str> = input.split_whitespace().collect();
305 if words.len() != 4 {
306 return Err(FourWordError::InvalidInput(format!(
307 "Agent identity must have 4 words, got {}",
308 words.len()
309 )));
310 }
311
312 for word in &words {
314 if DICTIONARY.get_index(word).is_none() {
315 return Err(FourWordError::InvalidWord(word.to_string()));
316 }
317 }
318
319 Ok(IdentityWords::Agent {
320 words: [
321 words[0].to_lowercase(),
322 words[1].to_lowercase(),
323 words[2].to_lowercase(),
324 words[3].to_lowercase(),
325 ],
326 })
327 }
328 }
329
330 pub fn matches(&self, hash: &[u8], words: &str) -> Result<bool> {
336 let prefix = self.decode_to_prefix(words)?;
337 if hash.len() < 6 {
338 return Ok(false);
339 }
340 Ok(hash[..6] == prefix[..])
341 }
342}
343
344impl Default for IdentityEncoder {
345 fn default() -> Self {
346 Self::new()
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 const BEN_AGENT_ID: &str = "dd6530452610619d468e4e82be82107e86384365c58efa6e3018d7762c7368da";
356 const DAVID_VPS_AGENT_ID: &str =
357 "da2233d6ba2f95696e5f5ba3bc4db193be1aa53d7ce1c048a8e8a67639337b75";
358 const THIRD_AGENT_ID: &str = "3e729de0469a594d1e042a672b29adde388e34aed2ced1e4c244a87f03053770";
359
360 #[test]
361 fn test_encode_agent_id() {
362 let encoder = IdentityEncoder::new();
363 let bytes = hex::decode(BEN_AGENT_ID).unwrap();
364 let identity = encoder.encode_agent(&bytes).unwrap();
365
366 assert!(matches!(identity, IdentityWords::Agent { .. }));
367 assert_eq!(identity.word_count(), 4);
368
369 let display = identity.to_string();
370 let words: Vec<&str> = display.split_whitespace().collect();
371 assert_eq!(words.len(), 4);
372
373 for word in &words {
375 assert!(
376 DICTIONARY.get_index(word).is_some(),
377 "Word '{}' not in dictionary",
378 word
379 );
380 }
381
382 println!("Ben's agent: {}", identity);
383 }
384
385 #[test]
386 fn test_encode_all_network_agents() {
387 let encoder = IdentityEncoder::new();
388
389 let agents = [
390 ("Ben", BEN_AGENT_ID),
391 ("David VPS", DAVID_VPS_AGENT_ID),
392 ("Third", THIRD_AGENT_ID),
393 ];
394
395 let mut seen = std::collections::HashSet::new();
396 for (name, hex_id) in &agents {
397 let identity = encoder.encode_hex(hex_id).unwrap();
398 let display = identity.to_string();
399 println!("{}: {} -> {}", name, &hex_id[..16], display);
400
401 assert!(
403 seen.insert(display.clone()),
404 "Collision detected for {}",
405 name
406 );
407 }
408 }
409
410 #[test]
411 fn test_round_trip_prefix() {
412 let encoder = IdentityEncoder::new();
413 let bytes = hex::decode(BEN_AGENT_ID).unwrap();
414
415 let identity = encoder.encode_agent(&bytes).unwrap();
416 let prefix = encoder.decode_to_prefix(&identity.to_string()).unwrap();
417
418 assert_eq!(&bytes[..6], &prefix[..]);
420 }
421
422 #[test]
423 fn test_round_trip_all_agents() {
424 let encoder = IdentityEncoder::new();
425
426 for hex_id in [BEN_AGENT_ID, DAVID_VPS_AGENT_ID, THIRD_AGENT_ID] {
427 let bytes = hex::decode(hex_id).unwrap();
428 let identity = encoder.encode_agent(&bytes).unwrap();
429 let prefix = encoder.decode_to_prefix(&identity.to_string()).unwrap();
430 assert_eq!(
431 &bytes[..6],
432 &prefix[..],
433 "Round-trip failed for {}",
434 &hex_id[..16]
435 );
436 }
437 }
438
439 #[test]
440 fn test_full_identity() {
441 let encoder = IdentityEncoder::new();
442 let full = encoder
443 .encode_hex_full(BEN_AGENT_ID, THIRD_AGENT_ID)
444 .unwrap();
445
446 assert!(full.is_full());
447 assert_eq!(full.word_count(), 8);
448
449 let display = full.to_string();
450 assert!(display.contains(" @ "), "Full identity must contain ' @ '");
451
452 let parts: Vec<&str> = display.split(" @ ").collect();
453 assert_eq!(parts.len(), 2);
454 assert_eq!(parts[0].split_whitespace().count(), 4);
455 assert_eq!(parts[1].split_whitespace().count(), 4);
456
457 println!("Full identity: {}", full);
458 }
459
460 #[test]
461 fn test_parse_agent_identity() {
462 let encoder = IdentityEncoder::new();
463 let bytes = hex::decode(BEN_AGENT_ID).unwrap();
464
465 let identity = encoder.encode_agent(&bytes).unwrap();
467 let display = identity.to_string();
468 let parsed = encoder.parse(&display).unwrap();
469
470 assert_eq!(identity, parsed);
471 }
472
473 #[test]
474 fn test_parse_full_identity() {
475 let encoder = IdentityEncoder::new();
476 let full = encoder
477 .encode_hex_full(BEN_AGENT_ID, THIRD_AGENT_ID)
478 .unwrap();
479
480 let display = full.to_string();
481 let parsed = encoder.parse(&display).unwrap();
482
483 assert_eq!(full, parsed);
484 }
485
486 #[test]
487 fn test_matches() {
488 let encoder = IdentityEncoder::new();
489 let bytes = hex::decode(BEN_AGENT_ID).unwrap();
490
491 let identity = encoder.encode_agent(&bytes).unwrap();
492 let display = identity.to_string();
493
494 assert!(encoder.matches(&bytes, &display).unwrap());
496
497 let other_bytes = hex::decode(DAVID_VPS_AGENT_ID).unwrap();
499 assert!(!encoder.matches(&other_bytes, &display).unwrap());
500 }
501
502 #[test]
503 fn test_different_agents_different_words() {
504 let encoder = IdentityEncoder::new();
505
506 let ben = encoder.encode_hex(BEN_AGENT_ID).unwrap().to_string();
507 let david = encoder.encode_hex(DAVID_VPS_AGENT_ID).unwrap().to_string();
508 let third = encoder.encode_hex(THIRD_AGENT_ID).unwrap().to_string();
509
510 assert_ne!(ben, david);
511 assert_ne!(ben, third);
512 assert_ne!(david, third);
513 }
514
515 #[test]
516 fn test_deterministic() {
517 let encoder = IdentityEncoder::new();
518
519 let a = encoder.encode_hex(BEN_AGENT_ID).unwrap().to_string();
521 let b = encoder.encode_hex(BEN_AGENT_ID).unwrap().to_string();
522 assert_eq!(a, b);
523 }
524
525 #[test]
526 fn test_family_name_pattern() {
527 let encoder = IdentityEncoder::new();
528
529 let full1 = encoder
532 .encode_hex_full(BEN_AGENT_ID, THIRD_AGENT_ID)
533 .unwrap();
534 let full2 = encoder
535 .encode_hex_full(DAVID_VPS_AGENT_ID, THIRD_AGENT_ID)
536 .unwrap();
537
538 assert_ne!(full1.agent_words(), full2.agent_words());
540
541 assert_eq!(full1.user_words(), full2.user_words());
543
544 println!("Agent 1: {}", full1);
545 println!("Agent 2: {}", full2);
546 println!(
547 "Same family name: {}",
548 full1.user_words().unwrap().join(" ")
549 );
550 }
551
552 #[test]
553 fn test_short_hash_rejected() {
554 let encoder = IdentityEncoder::new();
555 let short = vec![0u8; 5]; assert!(encoder.encode_agent(&short).is_err());
557 }
558
559 #[test]
560 fn test_invalid_word_rejected() {
561 let encoder = IdentityEncoder::new();
562 assert!(encoder.parse("not real words here").is_err());
563 }
564
565 #[test]
566 fn test_wrong_word_count_rejected() {
567 let encoder = IdentityEncoder::new();
568 let word = DICTIONARY.get_word(0).unwrap();
570 let three_words = format!("{} {} {}", word, word, word);
571 assert!(encoder.parse(&three_words).is_err());
572 }
573}