-
Notifications
You must be signed in to change notification settings - Fork 520
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
part of #1824
- Loading branch information
Showing
5 changed files
with
107 additions
and
164 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,40 @@ | ||
extern crate itertools; | ||
use itertools::Itertools; | ||
|
||
/// Encrypt the input string using square cryptography | ||
pub fn encrypt(input: &str) -> String { | ||
let prepared = prepare(input); | ||
if prepared.is_empty() { | ||
let mut input: Vec<_> = input | ||
.to_ascii_lowercase() | ||
.chars() | ||
.filter(char::is_ascii_alphanumeric) | ||
.collect(); | ||
if input.is_empty() { | ||
return String::new(); | ||
} | ||
|
||
let (cols, rows) = dimensions(prepared.len()); | ||
|
||
let mut output = String::with_capacity(input.len()); | ||
for chunk_iterator in SquareIndexer::new(rows, cols).chunks(cols).into_iter() { | ||
for ch_idx in chunk_iterator { | ||
if ch_idx < prepared.len() { | ||
output.push(prepared[ch_idx]); | ||
} | ||
} | ||
output.push(' '); | ||
} | ||
|
||
// we know there's one extra space at the end | ||
output.pop(); | ||
|
||
output | ||
} | ||
|
||
/// Construct a vector of characters from the given input. | ||
/// | ||
/// Constrain it to the allowed chars: lowercase ascii letters. | ||
/// We construct a vector here because the length of the input | ||
/// matters when constructing the output, so we need to know | ||
/// how many input chars there are. We could treat it as a stream | ||
/// and just stream twice, but collecting it into a vector works | ||
/// equally well and might be a bit faster. | ||
fn prepare(input: &str) -> Vec<char> { | ||
let mut output = Vec::with_capacity(input.len()); | ||
|
||
output.extend( | ||
input | ||
.chars() | ||
.filter(|&c| c.is_ascii() && !c.is_whitespace() && !c.is_ascii_punctuation()) | ||
.map(|c| c.to_ascii_lowercase()), | ||
); | ||
|
||
// add space padding to the end such that the actual string returned | ||
// forms a perfect rectangle | ||
let (r, c) = dimensions(output.len()); | ||
output.resize(r * c, ' '); | ||
let width = (input.len() as f64).sqrt().ceil() as usize; | ||
let size = width * width; | ||
|
||
output.shrink_to_fit(); | ||
|
||
output | ||
} | ||
|
||
/// Get the dimensions of the appropriate bounding rectangle for this encryption | ||
/// | ||
/// To find `(rows, cols)` such that `cols >= rows && cols - rows <= 1`, we find | ||
/// the least square greater than or equal to the message length. Its square root | ||
/// is the cols. If the message length is a perfect square, `rows` is the same. | ||
/// Otherwise, it is one less. | ||
fn dimensions(length: usize) -> (usize, usize) { | ||
let cols = (length as f64).sqrt().ceil() as usize; | ||
let rows = if cols * cols == length { | ||
cols | ||
// skip last row if already empty | ||
let last_row = if input.len() + width > size { | ||
width | ||
} else { | ||
cols - 1 | ||
width - 1 | ||
}; | ||
(rows, cols) | ||
} | ||
|
||
/// Iterator over the indices of the appropriate chars of the output. | ||
/// | ||
/// For a (2, 3) (r, c) grid, yields (0, 3, 1, 4, 2, 5). | ||
/// Does no bounds checking or space insertion: that's handled elsewhere. | ||
#[derive(Debug)] | ||
struct SquareIndexer { | ||
rows: usize, | ||
cols: usize, | ||
cur_row: usize, | ||
cur_col: usize, | ||
max_value: usize, | ||
} | ||
// padding | ||
input.resize(size, ' '); | ||
|
||
impl SquareIndexer { | ||
fn new(rows: usize, cols: usize) -> SquareIndexer { | ||
SquareIndexer { | ||
rows, | ||
cols, | ||
cur_row: 0, | ||
cur_col: 0, | ||
max_value: rows * cols, | ||
} | ||
} | ||
} | ||
// prevent input from being moved into closure below | ||
let input = &input; | ||
|
||
impl Iterator for SquareIndexer { | ||
type Item = usize; | ||
fn next(&mut self) -> Option<usize> { | ||
let value = self.cur_row + (self.cur_col * self.rows); | ||
let output = if value < self.max_value && self.cur_row < self.rows { | ||
Some(value) | ||
} else { | ||
None | ||
}; | ||
// transpose | ||
let mut res: String = (0..width) | ||
.flat_map(|col| { | ||
(0..last_row) | ||
.map(move |row| input[row * width + col]) | ||
.chain(std::iter::once(' ')) | ||
}) | ||
.collect(); | ||
|
||
// now increment internal state to next value | ||
self.cur_col += 1; | ||
if self.cur_col >= self.cols { | ||
self.cur_col = 0; | ||
self.cur_row += 1; | ||
} | ||
// trailing space separator | ||
res.pop(); | ||
|
||
output | ||
} | ||
res | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
use crypto_square::*; | ||
|
||
{% for test in cases %} | ||
#[test] | ||
#[ignore] | ||
fn {{ test.description | make_ident }}() { | ||
let actual = encrypt({{ test.input.plaintext | json_encode() }}); | ||
let expected = {{ test.expected | json_encode() }}; | ||
assert_eq!(&actual, expected); | ||
} | ||
{% endfor -%} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,34 @@ | ||
# This is an auto-generated file. Regular comments will be removed when this | ||
# file is regenerated. Regenerating will not touch any manually added keys, | ||
# so comments can be added in a "comment" key. | ||
# This is an auto-generated file. | ||
# | ||
# Regenerating this file via `configlet sync` will: | ||
# - Recreate every `description` key/value pair | ||
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications | ||
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) | ||
# - Preserve any other key/value pair | ||
# | ||
# As user-added comments (using the # character) will be removed when this file | ||
# is regenerated, comments can be added via a `comment` key. | ||
|
||
[407c3837-9aa7-4111-ab63-ec54b58e8e9f] | ||
description = "empty plaintext results in an empty ciphertext" | ||
|
||
[aad04a25-b8bb-4304-888b-581bea8e0040] | ||
description = "normalization results in empty plaintext" | ||
|
||
[64131d65-6fd9-4f58-bdd8-4a2370fb481d] | ||
description = "Lowercase" | ||
|
||
[63a4b0ed-1e3c-41ea-a999-f6f26ba447d6] | ||
description = "Remove spaces" | ||
|
||
[1b5348a1-7893-44c1-8197-42d48d18756c] | ||
description = "Remove punctuation" | ||
|
||
[8574a1d3-4a08-4cec-a7c7-de93a164f41a] | ||
description = "9 character plaintext results in 3 chunks of 3 characters" | ||
|
||
[a65d3fa1-9e09-43f9-bcec-7a672aec3eae] | ||
description = "8 character plaintext results in 3 chunks, the last one with a trailing space" | ||
|
||
[fbcb0c6d-4c39-4a31-83f6-c473baa6af80] | ||
description = "54 character plaintext results in 7 chunks, the last two with trailing spaces" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,79 +1,64 @@ | ||
use crypto_square::encrypt; | ||
use crypto_square::*; | ||
|
||
fn test(input: &str, output: &str) { | ||
assert_eq!(&encrypt(input), output); | ||
#[test] | ||
fn empty_plaintext_results_in_an_empty_ciphertext() { | ||
let actual = encrypt(""); | ||
let expected = ""; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
fn empty_input() { | ||
test("", "") | ||
#[ignore] | ||
fn normalization_results_in_empty_plaintext() { | ||
let actual = encrypt("... --- ..."); | ||
let expected = ""; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn encrypt_also_decrypts_square() { | ||
// note that you only get the exact input back if: | ||
// 1. no punctuation | ||
// 2. even spacing | ||
// 3. all lowercase | ||
// 4. square input | ||
let example = "lime anda coco anut"; | ||
assert_eq!(example, &encrypt(&encrypt(example))); | ||
fn lowercase() { | ||
let actual = encrypt("A"); | ||
let expected = "a"; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn example() { | ||
test( | ||
"If man was meant to stay on the ground, god would have given us roots.", | ||
"imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau ", | ||
) | ||
fn remove_spaces() { | ||
let actual = encrypt(" b "); | ||
let expected = "b"; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn empty_last_line() { | ||
test("congratulate", "crl oaa ntt gue") | ||
fn remove_punctuation() { | ||
let actual = encrypt("@1,%!"); | ||
let expected = "1"; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn spaces_are_reorganized() { | ||
test("abet", "ae bt"); | ||
test("a bet", "ae bt"); | ||
test(" a b e t ", "ae bt"); | ||
fn test_9_character_plaintext_results_in_3_chunks_of_3_characters() { | ||
let actual = encrypt("This is fun!"); | ||
let expected = "tsf hiu isn"; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn everything_becomes_lowercase() { | ||
test("caSe", "cs ae"); | ||
test("cAsE", "cs ae"); | ||
test("CASE", "cs ae"); | ||
fn test_8_character_plaintext_results_in_3_chunks_the_last_one_with_a_trailing_space() { | ||
let actual = encrypt("Chill out."); | ||
let expected = "clu hlt io "; | ||
assert_eq!(&actual, expected); | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn long() { | ||
test( | ||
r#" | ||
We choose to go to the moon. | ||
We choose to go to the moon in this decade and do the other things, | ||
not because they are easy, but because they are hard, because that | ||
goal will serve to organize and measure the best of our energies and | ||
skills, because that challenge is one that we are willing to accept, | ||
one we are unwilling to postpone, and one which we intend to win, | ||
and the others, too. | ||
-- John F. Kennedy, 12 September 1962 | ||
"#, | ||
&(String::from("womdbudlmecsgwdwob enooetbsenaotioihe ") | ||
+ "cwotcbeeaeunolnnnr henhaecrsrsealeaf1 ocieucavugetciwnk9 " | ||
+ "ohnosauerithcnhde6 sotteusteehaegitn2 eohhtseotsatptchn " | ||
+ "tsiehetohatwtohee oesrethrenceopwod gtdtyhagbdhanoety " | ||
+ "ooehaetaesaresih1 tgcirygnsklewtne2 ooaneaoitilweptrs " | ||
+ "ttdgerazoleiaoese hoesaeleflnlrnntp etanshwaosgleedot " | ||
+ "mhnoyainubeiuatoe oedtbrldreinnnojm "), | ||
) | ||
fn test_54_character_plaintext_results_in_7_chunks_the_last_two_with_trailing_spaces() { | ||
let actual = encrypt("If man was meant to stay on the ground, god would have given us roots."); | ||
let expected = "imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau "; | ||
assert_eq!(&actual, expected); | ||
} |