Skip to main content

happy_cracking/crypto/
morse.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3use std::collections::HashMap;
4use std::sync::LazyLock;
5
6// Performance: direct ASCII-indexed lookup table avoids HashMap hashing overhead
7// for the encode hot path. O(1) array index vs O(1) amortized but with hash
8// computation, cache-unfriendly bucket chasing, and key comparison.
9static ENCODE_LUT: LazyLock<[Option<&'static str>; 128]> = LazyLock::new(|| {
10    let mut table: [Option<&'static str>; 128] = [None; 128];
11    for &(ch, morse) in MORSE_TABLE {
12        let idx = ch as usize;
13        if idx < 128 {
14            table[idx] = Some(morse);
15        }
16    }
17    table
18});
19
20#[derive(Subcommand)]
21pub enum MorseAction {
22    #[command(about = "Encode text to Morse code")]
23    Encode {
24        #[arg(help = "Input text")]
25        input: String,
26    },
27    #[command(about = "Decode Morse code to text")]
28    Decode {
29        #[arg(help = "Morse code (use . and -, separate letters with space, words with /)")]
30        input: String,
31    },
32}
33
34pub fn run(action: MorseAction) -> Result<()> {
35    match action {
36        MorseAction::Encode { input } => {
37            println!("{}", encode(&input));
38        }
39        MorseAction::Decode { input } => {
40            println!("{}", decode(&input)?);
41        }
42    }
43    Ok(())
44}
45
46const MORSE_TABLE: &[(char, &str)] = &[
47    ('A', ".-"),
48    ('B', "-..."),
49    ('C', "-.-."),
50    ('D', "-.."),
51    ('E', "."),
52    ('F', "..-."),
53    ('G', "--."),
54    ('H', "...."),
55    ('I', ".."),
56    ('J', ".---"),
57    ('K', "-.-"),
58    ('L', ".-.."),
59    ('M', "--"),
60    ('N', "-."),
61    ('O', "---"),
62    ('P', ".--."),
63    ('Q', "--.-"),
64    ('R', ".-."),
65    ('S', "..."),
66    ('T', "-"),
67    ('U', "..-"),
68    ('V', "...-"),
69    ('W', ".--"),
70    ('X', "-..-"),
71    ('Y', "-.--"),
72    ('Z', "--.."),
73    ('0', "-----"),
74    ('1', ".----"),
75    ('2', "..---"),
76    ('3', "...--"),
77    ('4', "....-"),
78    ('5', "....."),
79    ('6', "-...."),
80    ('7', "--..."),
81    ('8', "---.."),
82    ('9', "----."),
83    ('.', ".-.-.-"),
84    (',', "--..--"),
85    ('?', "..--.."),
86    ('!', "-.-.--"),
87    ('/', "-..-."),
88    ('(', "-.--."),
89    (')', "-.--.-"),
90    ('&', ".-..."),
91    (':', "---..."),
92    (';', "-.-.-."),
93    ('=', "-...-"),
94    ('+', ".-.-."),
95    ('-', "-....-"),
96    ('_', "..--.-"),
97    ('"', ".-..-."),
98    ('\'', ".----."),
99    ('$', "...-..-"),
100    ('@', ".--.-."),
101];
102
103static MORSE_TO_CHAR: LazyLock<HashMap<&'static str, char>> =
104    LazyLock::new(|| MORSE_TABLE.iter().map(|&(c, m)| (m, c)).collect());
105
106pub fn encode(input: &str) -> String {
107    let lut = &*ENCODE_LUT;
108    let mut result = String::with_capacity(input.len() * 4);
109    let mut first_word = true;
110
111    for word in input.split_whitespace() {
112        if !first_word {
113            result.push_str(" / ");
114        }
115        first_word = false;
116        let mut first_char = true;
117        for c in word.chars() {
118            let upper = if c.is_ascii_lowercase() {
119                (c as u8 - b'a' + b'A') as char
120            } else {
121                c
122            };
123            let idx = upper as usize;
124            if let Some(morse) = if idx < 128 { lut[idx] } else { None } {
125                if !first_char {
126                    result.push(' ');
127                }
128                first_char = false;
129                result.push_str(morse);
130            }
131        }
132    }
133
134    result
135}
136
137pub fn decode(input: &str) -> Result<String> {
138    input
139        .split(" / ")
140        .map(|word| {
141            word.split_whitespace()
142                .map(|morse| {
143                    MORSE_TO_CHAR
144                        .get(morse)
145                        .copied()
146                        .with_context(|| format!("Unknown Morse code: {}", morse))
147                })
148                .collect::<Result<String>>()
149        })
150        .collect::<Result<Vec<_>>>()
151        .map(|words| words.join(" "))
152}