happy_cracking/crypto/
tapcode.rs1use anyhow::Result;
2use clap::Subcommand;
3
4#[derive(Subcommand)]
5pub enum TapcodeAction {
6 #[command(about = "Encode text to tap code")]
7 Encode {
8 #[arg(help = "Input text")]
9 input: String,
10 },
11 #[command(about = "Decode tap code to text")]
12 Decode {
13 #[arg(help = "Tap code (dots separated by spaces, words separated by /)")]
14 input: String,
15 },
16}
17
18pub fn run(action: TapcodeAction) -> Result<()> {
19 match action {
20 TapcodeAction::Encode { input } => {
21 println!("{}", encode(&input));
22 }
23 TapcodeAction::Decode { input } => {
24 println!("{}", decode(&input)?);
25 }
26 }
27 Ok(())
28}
29
30const GRID: [char; 25] = [
32 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
33 'U', 'V', 'W', 'X', 'Y', 'Z',
34];
35
36const DOTS: [&str; 6] = ["", ".", "..", "...", "....", "....."];
37
38pub fn encode(input: &str) -> String {
39 let mut out = String::with_capacity(input.len() * 10);
40 let mut first_word = true;
41
42 for word in input.split_whitespace() {
43 if !first_word {
44 out.push_str(" / ");
45 }
46 first_word = false;
47
48 let mut first_char = true;
49 for c in word.bytes() {
50 if !c.is_ascii_alphabetic() {
51 continue;
52 }
53 let mut b = c.to_ascii_uppercase();
54 if b == b'K' {
55 b = b'C';
56 }
57
58 let idx = b - b'A';
59 let (row, col) = match b {
60 b'A'..=b'I' => (idx / 5 + 1, idx % 5 + 1),
61 b'K'..=b'Z' => {
62 let shifted = idx - 1;
63 (shifted / 5 + 1, shifted % 5 + 1)
64 }
65 b'J' => (2, 5),
66 _ => continue,
67 };
68
69 if !first_char {
70 out.push_str(" ");
71 }
72 first_char = false;
73
74 out.push_str(DOTS[row as usize]);
75 out.push(' ');
76 out.push_str(DOTS[col as usize]);
77 }
78 }
79 out
80}
81
82pub fn decode(input: &str) -> Result<String> {
83 if input.trim().is_empty() {
84 return Ok(String::new());
85 }
86
87 let mut result = String::with_capacity(input.len() / 3);
88 let mut first_word = true;
89
90 for word in input.split(" / ") {
91 if !first_word {
92 result.push(' ');
93 }
94 first_word = false;
95
96 for pair in word.split(" ").filter(|s| !s.is_empty()) {
97 let mut parts = pair.split(' ').filter(|s| !s.is_empty());
98 let row_str = parts
99 .next()
100 .ok_or_else(|| anyhow::anyhow!("Invalid tap code pair: {}", pair))?;
101 let col_str = parts
102 .next()
103 .ok_or_else(|| anyhow::anyhow!("Invalid tap code pair: {}", pair))?;
104
105 if parts.next().is_some() {
106 anyhow::bail!("Invalid tap code pair: {}", pair);
107 }
108
109 let row = row_str.len();
110 let col = col_str.len();
111
112 if !(1..=5).contains(&row) || !(1..=5).contains(&col) {
113 anyhow::bail!("Tap code values out of range (1-5): {}", pair);
114 }
115 if !row_str.chars().all(|c| c == '.') || !col_str.chars().all(|c| c == '.') {
116 anyhow::bail!("Invalid tap code characters: {}", pair);
117 }
118
119 let idx = (row - 1) * 5 + (col - 1);
120 result.push(GRID[idx]);
121 }
122 }
123
124 Ok(result)
125}