Skip to main content

happy_cracking/crypto/
frequency.rs

1use anyhow::Result;
2use clap::Subcommand;
3use std::collections::HashMap;
4
5#[derive(Subcommand)]
6pub enum FrequencyAction {
7    #[command(about = "Analyze character frequency")]
8    Analyze {
9        #[arg(help = "Input text")]
10        input: String,
11        #[arg(short, long, help = "Show only alphabetic characters")]
12        alpha_only: bool,
13    },
14    #[command(about = "Chi-squared test against English letter frequencies")]
15    ChiSquared {
16        #[arg(help = "Input text")]
17        input: String,
18    },
19    #[command(about = "Calculate Index of Coincidence (IoC)")]
20    Ioc {
21        #[arg(help = "Input text")]
22        input: String,
23    },
24}
25
26pub fn run(action: FrequencyAction) -> Result<()> {
27    match action {
28        FrequencyAction::Analyze { input, alpha_only } => {
29            let result = analyze(&input, alpha_only);
30            print_analysis(&result);
31        }
32        FrequencyAction::ChiSquared { input } => {
33            let score = chi_squared(&input);
34            println!("Chi-squared score: {:.4}", score);
35            println!("(Lower score = closer to English letter distribution)");
36            if score < 50.0 {
37                println!("Likely English text");
38            } else if score < 100.0 {
39                println!("Possibly English text");
40            } else {
41                println!("Unlikely to be English text");
42            }
43        }
44        FrequencyAction::Ioc { input } => {
45            let ioc = index_of_coincidence(&input);
46            println!("Index of Coincidence: {:.6}", ioc);
47            println!("English text ~0.0667, random text ~0.0385");
48            if (ioc - 0.0667).abs() < 0.01 {
49                println!("Consistent with monoalphabetic cipher or English text");
50            } else if (ioc - 0.0385).abs() < 0.01 {
51                println!("Consistent with polyalphabetic cipher or random text");
52            }
53        }
54    }
55    Ok(())
56}
57
58// English letter frequencies (percentage)
59const ENGLISH_FREQ: &[(char, f64)] = &[
60    ('E', 12.7),
61    ('T', 9.1),
62    ('A', 8.2),
63    ('O', 7.5),
64    ('I', 7.0),
65    ('N', 6.7),
66    ('S', 6.3),
67    ('H', 6.1),
68    ('R', 6.0),
69    ('D', 4.3),
70    ('L', 4.0),
71    ('C', 2.8),
72    ('U', 2.8),
73    ('M', 2.4),
74    ('W', 2.4),
75    ('F', 2.2),
76    ('G', 2.0),
77    ('Y', 2.0),
78    ('P', 1.9),
79    ('B', 1.5),
80    ('V', 1.0),
81    ('K', 0.8),
82    ('J', 0.15),
83    ('X', 0.15),
84    ('Q', 0.10),
85    ('Z', 0.07),
86];
87
88#[derive(Debug)]
89pub struct FrequencyResult {
90    pub frequencies: Vec<(char, usize, f64)>, // (char, count, percentage)
91    pub total_chars: usize,
92}
93
94pub fn analyze(input: &str, alpha_only: bool) -> FrequencyResult {
95    let mut frequencies: Vec<(char, usize, f64)> = Vec::new();
96    let mut total = 0usize;
97
98    if alpha_only {
99        // Optimization: Use array instead of HashMap for pure alphabetic analysis
100        let mut counts = [0usize; 26];
101
102        for c in input.chars() {
103            if c.is_ascii_alphabetic() {
104                counts[(c.to_ascii_uppercase() as u8 - b'A') as usize] += 1;
105                total += 1;
106            }
107        }
108
109        for (i, &count) in counts.iter().enumerate() {
110            if count > 0 {
111                let percentage = if total > 0 {
112                    (count as f64 / total as f64) * 100.0
113                } else {
114                    0.0
115                };
116                frequencies.push(((b'A' + i as u8) as char, count, percentage));
117            }
118        }
119    } else {
120        let mut counts: HashMap<char, usize> = HashMap::new();
121        for c in input.chars() {
122            *counts.entry(c).or_insert(0) += 1;
123            total += 1;
124        }
125
126        frequencies = counts
127            .into_iter()
128            .map(|(c, count)| {
129                let percentage = if total > 0 {
130                    (count as f64 / total as f64) * 100.0
131                } else {
132                    0.0
133                };
134                (c, count, percentage)
135            })
136            .collect();
137    }
138
139    // Sort by count descending
140    frequencies.sort_by_key(|b| std::cmp::Reverse(b.1));
141
142    FrequencyResult {
143        frequencies,
144        total_chars: total,
145    }
146}
147
148// Chi-squared test comparing input letter frequencies against English.
149// Lower score means closer to English distribution.
150pub fn chi_squared(input: &str) -> f64 {
151    let mut counts = [0u32; 26];
152    let mut total = 0u32;
153
154    for c in input.chars() {
155        if c.is_ascii_alphabetic() {
156            counts[(c.to_ascii_uppercase() as u8 - b'A') as usize] += 1;
157            total += 1;
158        }
159    }
160
161    if total == 0 {
162        return f64::INFINITY;
163    }
164
165    let total_f = total as f64;
166    ENGLISH_FREQ
167        .iter()
168        .map(|&(ch, expected_pct)| {
169            let observed = counts[(ch as u8 - b'A') as usize] as f64;
170            let expected = expected_pct / 100.0 * total_f;
171            if expected > 0.0 {
172                (observed - expected).powi(2) / expected
173            } else {
174                0.0
175            }
176        })
177        .sum()
178}
179
180// Index of Coincidence (IoC) for the alphabetic characters in input.
181// English text has IoC ~0.0667, random text ~0.0385 (1/26).
182pub fn index_of_coincidence(input: &str) -> f64 {
183    let mut counts = [0u64; 26];
184    let mut total = 0u64;
185
186    for c in input.chars() {
187        if c.is_ascii_alphabetic() {
188            counts[(c.to_ascii_uppercase() as u8 - b'A') as usize] += 1;
189            total += 1;
190        }
191    }
192
193    if total <= 1 {
194        return 0.0;
195    }
196
197    let numerator: u64 = counts.iter().map(|&n| n * n.saturating_sub(1)).sum();
198    numerator as f64 / (total * (total - 1)) as f64
199}
200
201fn print_analysis(result: &FrequencyResult) {
202    println!("Character Frequency Analysis");
203    println!("============================");
204    println!("Total characters: {}", result.total_chars);
205    println!();
206
207    println!("{:<6} {:>6} {:>8}   English %", "Char", "Count", "Freq %");
208    println!("{}", "-".repeat(40));
209
210    for (c, count, percentage) in &result.frequencies {
211        let english_freq = ENGLISH_FREQ
212            .iter()
213            .find(|(ch, _)| *ch == c.to_ascii_uppercase())
214            .map(|(_, f)| format!("{:.1}%", f))
215            .unwrap_or_default();
216
217        let display_char = if *c == ' ' {
218            "SPACE".to_string()
219        } else if *c == '\n' {
220            "\\n".to_string()
221        } else if *c == '\t' {
222            "\\t".to_string()
223        } else {
224            c.to_string()
225        };
226
227        println!(
228            "{:<6} {:>6} {:>7.1}%   {}",
229            display_char, count, percentage, english_freq
230        );
231    }
232}