commit 3cf5b5e6522cc867bbb275be4d2a3b8b379a3981 Author: mehbark Date: Wed Apr 22 17:56:49 2026 -0400 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdc0075 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +ciphertext.txt diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1131c83 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "substitution-helper" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..406fe44 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "substitution-helper" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..541b5f1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,103 @@ +use std::{ + collections::HashMap, + env, + io::{self, Read, Write}, + process, +}; + +fn main() { + let mut subst = HashMap::new(); + for pair in env::args().skip(1) { + let &[from, to] = pair.as_bytes() else { + eprintln!("Usage: substitution-helper AB CD EF ..."); + process::exit(1); + }; + subst.insert(from.to_ascii_uppercase(), to.to_ascii_uppercase()); + } + + let mut inp = String::new(); + io::stdin().read_to_string(&mut inp).unwrap(); + let mut out = io::stdout().lock(); + + let mut byte_frequencies: HashMap<[char; 1], usize> = HashMap::new(); + for c in inp.chars().filter(char::is_ascii_alphabetic) { + *byte_frequencies + .entry([c.to_ascii_uppercase()]) + .or_default() += 1; + } + + println!("Letters:"); + print_freqs(byte_frequencies, &subst); + println!(); + + let mut digram_freqs: HashMap<[char; 2], usize> = HashMap::new(); + for slice in inp.as_bytes().windows(2) { + let [a, b] = slice else { + continue; + }; + if !(a.is_ascii_alphabetic() && b.is_ascii_alphabetic()) { + continue; + } + *digram_freqs.entry([*a as char, *b as char]).or_default() += 1; + } + + println!("Digrams:"); + print_freqs(digram_freqs, &subst); + println!(); + + let mut trigram_freqs: HashMap<[char; 3], usize> = HashMap::new(); + for slice in inp.as_bytes().windows(3) { + let [a, b, c] = slice else { + continue; + }; + if !(a.is_ascii_alphabetic() && b.is_ascii_alphabetic() && c.is_ascii_alphabetic()) { + continue; + } + *trigram_freqs + .entry([*a as char, *b as char, *c as char]) + .or_default() += 1; + } + + println!("Trigrams:"); + print_freqs(trigram_freqs, &subst); + println!(); + + for byte in inp.bytes() { + let new_byte = subst + .get(&byte.to_ascii_uppercase()) + .copied() + .unwrap_or(byte.to_ascii_lowercase()); + // .unwrap_or(b'.'); + out.write_all(&[new_byte]).unwrap(); + } +} + +fn print_freqs(freqs: HashMap<[char; N], usize>, subst: &HashMap) { + let total_freqs: usize = freqs.values().sum(); + + let mut freqs: Vec<_> = freqs + .into_iter() + .filter(|(_, n)| *n > 1 && (*n as f64 / total_freqs as f64) > 0.001) + .collect(); + freqs.sort_by_key(|(f, count)| (*count, *f)); + + for (c, count) in freqs.iter().rev() { + println!( + "{:>3}: {:4.1}% ({count:4})", + c.iter() + .map(|c| { + let Ok(b): Result = (*c).try_into() else { + panic!() + }; + + if let Some(new) = subst.get(&b) { + (*new as char).to_ascii_uppercase() + } else { + (b as char).to_ascii_lowercase() + } + }) + .collect::(), + (*count as f32 / total_freqs as f32) * 100. + ); + } +}