extern crate hex;
extern crate
is generally not necessary for programs since the 2018 (currently latest) edition of Rust. You should be able to delete this line, provided that edition = "2018"
is in your Cargo.toml
file.
let ciphertext_bytes = hex::decode(ciphertext).unwrap();
If you change .unwrap
to .expect("failed to decode ciphertext")
then there'll be a little more information in the error message. Of course, for a well-made command line program, you'd want to print your own better-formatted error message, instead of a Rust "panic" error.
fn find_xor_key(ciphertext: &Vec<u8>) -> u8{
Accepting &Vec<u8>
is usually overly restrictive because it requires the caller to have constructed a Vec
, whereas if you change the type to &[u8]
they can pass any slice of bytes. This will work without needing to change main
, because Vec
derefs to a byte slice.
The same applies to get_score
's parameter, but you'll need to tweak a couple things to make that work, like giving xored_vec
the explicit type Vec<u8>
and changing byte as char
to *byte as char
.
let ideal_frequencies: HashMap<char, f32> = [
...
].iter().cloned().collect();
This is basically a constant, so it'd be nice if we can compute this hash map only once rather than every time we check a score. We can do that with the once_cell
crate, which will hopefully soon become part of the standard library. Like this:
static IDEAL_FREQUENCIES: Lazy<HashMap<char, f32>> = Lazy::new(|| [
('E', 11.1607),
...
].iter().cloned().collect());
for byte in text.clone(){
Use text.iter()
instead of text.clone()
; it avoids allocating a copy. In general, where both are available, .into_iter()
consumes the structure and iterates over its elements by value (moves them), and .iter()
does not consume the structure but iterates over references to its elements instead.
let char_rep: char = byte as char;
if !char_rep.is_ascii(){
You can just use if !byte.is_ascii() {
here.
let chars: Vec<char> = text.iter().cloned().map(|x|x as char).collect();
let frequencies: HashMap<char, f32> = calculate_frequencies(&chars.into_iter().collect::<String>());
.cloned().map(|x|x as char)
can be written as .map(|x| *x as char)
.
You're collecting into chars
and then immediately iterate to convert it to a String
; that isn't necessary because you can just write
calculate_frequencies(&text.iter().map(|x| *x as char).collect::<String>())
But, there's a bigger point: text
was already an &[u8]
, and if we want a &str
out of that, that's just std::str::from_utf8
.
calculate_frequencies(std::str::from_utf8(text).unwrap())
(.unwrap()
will never fail here because we already checked that every byte is ASCII, so it cannot be invalid UTF-8.)
Your loop over the frequencies has several unnecessary &
s, and also looks up in the hash map twice instead of once; here's both fixed:
for (letter, frequency) in frequencies.iter() {
if let Some(ideal_frequency) = IDEAL_FREQUENCIES.get(letter) {
if (frequency - ideal_frequency).abs() <= 10.0 {
score += 1;
}
}
}
fn calculate_frequencies(text: &str) ->HashMap<char, f32>{
If we want to optimize for performance, which a real code-breaking tool would want to do, then this function should be accepting &[u8]
, since the only thing it does with the str
is split back to characters, and we know all the characters are ASCII at this point. But for more general idiomatic programming, taking &str
is fine.
let mut frequencies: HashMap<char, f32> = HashMap::new();
let mut letter_counts:HashMap<char, i32> = HashMap::new();
You can leave out the type declarations here and they will be inferred.
let text_upper = text.to_ascii_uppercase();
for c in text_upper.chars(){
This allocates a String
of uppercase unnecessarily; we might as well do it per-character instead:
for c in text.chars() {
let c = c.to_ascii_uppercase();
(Note, however, that this strategy does not work for Unicode-aware uppercasing, where one char
may occasionally become several.)
This is not a matter of Rust idiom at all, but I notice that frequencies
is identical to letter_counts
except for the values getting scaled. So, we can reuse a single HashMap
instead of duplicating it:
fn calculate_frequencies(text: &str) ->HashMap<char, f32>{
let count_to_frequency_scale = 100.0 / text.len() as f32;
let mut counts = HashMap::new();
for c in text.chars() {
*counts.entry(c.to_ascii_uppercase()).or_insert(0.0) += 1.0;
}
// Rescale counts to frequencies
for count in counts.values_mut() {
*count *= count_to_frequency_scale;
}
counts
}
Note: Using f32
instead of i32
for counting does not introduce any floating-point error, for reasonable string lengths: f32
exactly represents all integers up to 224 - 1 = 16,777,216. (However, there would be accumulated error if we tried to apply count_to_frequency_scale
while counting instead of after.)
If it didn't happen to be possible to reuse the same map, I'd be suggesting using iterators to do the transformation instead:
letter_counts
.into_iter()
.map(|(key, count)| (key, count as f32 * count_to_frequency_scale))
.collect()
Here's your code with all of my suggestions, and also formatted with rustfmt
. I haven't tested it because I was working in the Rust Playground which doesn't include the hex
crate in its available libraries.
extern crate hex;
use once_cell::sync::Lazy;
use std::collections::HashMap;
#[allow(dead_code)]
fn main() {
let ciphertext = "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736";
let ciphertext_bytes = hex::decode(ciphertext).unwrap();
let xor_key = find_xor_key(&ciphertext_bytes);
println!(
"{}",
ciphertext_bytes
.iter()
.map(|x| (x ^ xor_key) as char)
.collect::<String>()
);
}
fn find_xor_key(ciphertext: &[u8]) -> u8 {
let mut max_score = 0;
let mut best_key = 0;
for xor_key in 1..255 {
let xored_vec: Vec<u8> = ciphertext.iter().map(|x| x ^ xor_key).collect();
let score = get_score(&xored_vec);
if score > max_score {
max_score = score;
best_key = xor_key;
}
}
println!("{}", max_score);
best_key
}
fn get_score(text: &[u8]) -> u8 {
for byte in text.iter() {
if !byte.is_ascii() {
return 0;
}
}
let mut score = 1;
let frequencies = calculate_frequencies(std::str::from_utf8(text).unwrap());
for (letter, frequency) in frequencies.iter() {
if let Some(ideal_frequency) = IDEAL_FREQUENCIES.get(letter) {
if (frequency - ideal_frequency).abs() <= 10.0 {
score += 1;
}
}
}
score
}
fn calculate_frequencies(text: &str) -> HashMap<char, f32> {
let count_to_frequency_scale = 100.0 / text.len() as f32;
let mut counts = HashMap::new();
for c in text.chars() {
*counts.entry(c.to_ascii_uppercase()).or_insert(0.0) += 1.0;
}
// Rescale counts to frequencies
for count in counts.values_mut() {
*count *= count_to_frequency_scale;
}
counts
}
static IDEAL_FREQUENCIES: Lazy<HashMap<char, f32>> = Lazy::new(|| {
[
('E', 11.1607),
('M', 3.0129),
('A', 8.4966),
('H', 3.0034),
('R', 7.5809),
('G', 2.4705),
('I', 7.5448),
('B', 2.0720),
('O', 7.1635),
('F', 1.8121),
('T', 6.9509),
('Y', 1.7779),
('N', 6.6544),
('W', 1.2899),
('S', 5.7351),
('K', 1.1016),
('L', 5.4893),
('V', 1.0074),
('C', 4.5388),
('X', 0.2902),
('U', 3.6308),
('Z', 0.2722),
('D', 3.3844),
('J', 0.1965),
('P', 3.1671),
('Q', 0.1962),
]
.iter()
.cloned()
.collect()
});
```