Skip to main content

happy_cracking/crypto/
semaphore.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3use std::collections::HashMap;
4use std::sync::LazyLock;
5
6#[derive(Subcommand)]
7pub enum SemaphoreAction {
8    #[command(about = "Encode text to flag semaphore positions")]
9    Encode {
10        #[arg(help = "Input text")]
11        input: String,
12    },
13    #[command(about = "Decode flag semaphore positions to text")]
14    Decode {
15        #[arg(help = "Semaphore encoded string (e.g. 1-2 1-3 1-4)")]
16        input: String,
17    },
18}
19
20pub fn run(action: SemaphoreAction) -> Result<()> {
21    match action {
22        SemaphoreAction::Encode { input } => {
23            println!("{}", encode(&input)?);
24        }
25        SemaphoreAction::Decode { input } => {
26            println!("{}", decode(&input)?);
27        }
28    }
29    Ok(())
30}
31
32// Positions: 1=S, 2=SW, 3=W, 4=NW, 5=N, 6=NE, 7=E, 8=SE
33const SEMAPHORE_MAP: &[(char, &str)] = &[
34    ('A', "1-2"),
35    ('B', "1-3"),
36    ('C', "1-4"),
37    ('D', "1-5"),
38    ('E', "1-6"),
39    ('F', "1-7"),
40    ('G', "1-8"),
41    ('H', "2-3"),
42    ('I', "2-4"),
43    ('J', "5-6"),
44    ('K', "3-1"),
45    ('L', "3-2"),
46    ('M', "3-4"),
47    ('N', "3-5"),
48    ('O', "3-6"),
49    ('P', "3-7"),
50    ('Q', "3-8"),
51    ('R', "4-3"),
52    ('S', "4-5"),
53    ('T', "4-6"),
54    ('U', "4-7"),
55    ('V', "5-1"),
56    ('W', "6-2"),
57    ('X', "6-3"),
58    ('Y', "6-4"),
59    ('Z', "6-5"),
60];
61
62// Performance: [Option<&str>; 26] array indexed by (letter - 'A') replaces
63// HashMap<char, &str> for encoding. Eliminates hashing overhead, bucket chasing,
64// and key comparison — a direct O(1) array index vs amortized O(1) HashMap lookup.
65const ENCODE_LUT: [Option<&str>; 26] = {
66    let mut table: [Option<&str>; 26] = [None; 26];
67    let mut i = 0;
68    while i < SEMAPHORE_MAP.len() {
69        let (ch, code) = SEMAPHORE_MAP[i];
70        table[ch as usize - 'A' as usize] = Some(code);
71        i += 1;
72    }
73    table
74};
75
76static SEMAPHORE_TO_CHAR: LazyLock<HashMap<&'static str, char>> =
77    LazyLock::new(|| SEMAPHORE_MAP.iter().map(|&(c, code)| (code, c)).collect());
78
79// Performance: build output directly into a pre-allocated String instead of
80// collecting into Vec<&str> and joining. This avoids the intermediate Vec
81// allocation and the second pass that join() performs to concatenate.
82pub fn encode(input: &str) -> Result<String> {
83    if input.is_empty() {
84        return Ok(String::new());
85    }
86
87    // Each semaphore code is 3 chars ("X-Y") + 1 space separator
88    let mut result = String::with_capacity(input.len() * 4);
89    let mut has_content = false;
90
91    for b in input.bytes() {
92        let upper = match b {
93            b'a'..=b'z' => b - b'a',
94            b'A'..=b'Z' => b - b'A',
95            _ => continue,
96        };
97        if has_content {
98            result.push(' ');
99        }
100        result.push_str(ENCODE_LUT[upper as usize].ok_or_else(|| {
101            anyhow::anyhow!("No semaphore code for character: {}", upper as char)
102        })?);
103        has_content = true;
104    }
105
106    Ok(result)
107}
108
109pub fn decode(input: &str) -> Result<String> {
110    let input = input.trim();
111    if input.is_empty() {
112        return Ok(String::new());
113    }
114
115    input
116        .split_whitespace()
117        .map(|code| {
118            SEMAPHORE_TO_CHAR
119                .get(code)
120                .copied()
121                .with_context(|| format!("Unknown semaphore code: {}", code))
122        })
123        .collect()
124}