use std::io::{Read, Write};
use crate::pipeline::traits::{self, Cipher, Error, GeneratedKey};
pub struct RotN;
impl Cipher for RotN {
fn new() -> Self
where
Self: Sized,
{
Self
}
fn generate_key(&self) -> GeneratedKey {
GeneratedKey::None
}
fn encrypt_stream(
&self,
key: &[u8],
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> traits::Result<()> {
let key = extract_n_from_key_or_fail(key)?;
let mut buffer = [0u8; 4096];
while let Ok(n) = reader.read(&mut buffer) {
if n == 0 {
break;
}
for c in &mut buffer[..n] {
*c = rotate(*c, key);
}
writer
.write_all(&buffer[..n])
.map_err(|e| Error::Write(e.to_string()))?;
}
Ok(())
}
fn decrypt_stream(
&self,
key: &[u8],
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> traits::Result<()> {
let key = extract_n_from_key_or_fail(key)?;
let mut buffer = [0u8; 4096];
while let Ok(n) = reader.read(&mut buffer) {
if n == 0 {
break;
}
for c in &mut buffer[..n] {
*c = rotate(*c, -key);
}
writer
.write_all(&buffer[..n])
.map_err(|e| Error::Write(e.to_string()))?;
}
Ok(())
}
}
fn extract_n_from_key_or_fail(key: &[u8]) -> traits::Result<i16> {
if key.len() == 1 {
Ok(i16::from(key[0]))
} else {
Err(Error::Key)
}
}
#[inline]
fn rotate(character: u8, rot: i16) -> u8 {
const ALPHABET_SIZE: i16 = (b'z' - b'a' + 1) as i16;
match character {
c @ b'a'..=b'z' => {
let offset_from_a = i16::from(c - b'a');
let rotated = offset_from_a + rot;
let wrapped = rotated.rem_euclid(ALPHABET_SIZE);
let shifted = wrapped + i16::from(b'a');
u8::try_from(shifted).expect("bound to a-z")
}
c @ b'A'..=b'Z' => {
let offset_from_a = i16::from(c - b'A');
let rotated = offset_from_a + rot;
let wrapped = rotated.rem_euclid(ALPHABET_SIZE);
let shifted = wrapped + i16::from(b'A');
u8::try_from(shifted).expect("bound to A-Z")
}
c => c,
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn rot_encrypt_with_n_is_correct() {
let plaintext = b"attack at dawn";
let encrypted = RotN::new().encrypt(&[5], plaintext).unwrap();
assert_eq!(&encrypted, b"fyyfhp fy ifbs");
}
#[test]
fn rot_decrypt_with_n_is_correct() {
let ciphertext = b"fyyfhp fy ifbs";
let decrypted = RotN::new().decrypt(&[5], ciphertext).unwrap();
assert_eq!(&decrypted, b"attack at dawn");
}
#[test]
fn rot_encrypt_does_not_break_multibyte_chars() {
let plaintext = "hello ü, ñ, ü, 漢 world".as_bytes();
let encrypted = RotN::new().encrypt(&[13], plaintext).unwrap();
dbg!(&encrypted);
assert_eq!(&encrypted, "uryyb ü, ñ, ü, 漢 jbeyq".as_bytes());
}
#[test]
fn rot_decrypt_does_not_break_multibyte_chars() {
let ciphertext = "uryyb ü, ñ, ü, 漢 jbeyq".as_bytes();
let decrypted = RotN::new().decrypt(&[13], ciphertext).unwrap();
dbg!(&decrypted);
assert_eq!(&decrypted, "hello ü, ñ, ü, 漢 world".as_bytes());
}
#[test]
fn rot_empty_input_is_noop() {
let plaintext = b"";
let encrypted = RotN::new().encrypt(&[13], plaintext).unwrap();
assert_eq!(&encrypted, b"");
}
#[test]
fn rot_ignores_non_ascii_letters() {
let plaintext = b"1234!@#$%^&*()_+-=[]{}|;:',.<>?/";
let encrypted = RotN::new().encrypt(&[7], plaintext).unwrap();
assert_eq!(&encrypted, plaintext); }
#[test]
fn rot_preserves_mixed_case_and_nonletters() {
let plaintext = b"Hello, World! 123";
let encrypted = RotN::new().encrypt(&[5], plaintext).unwrap();
assert_eq!(&encrypted, b"Mjqqt, Btwqi! 123");
let decrypted = RotN::new().decrypt(&[5], &encrypted).unwrap();
assert_eq!(&decrypted, plaintext);
}
#[test]
fn rot_round_trip_with_arbitrary_n() {
let plaintext = b"Encrypt this message properly.";
let n = 19;
let encrypted = RotN::new().encrypt(&[n], plaintext).unwrap();
let decrypted = RotN::new().decrypt(&[n], &encrypted).unwrap();
assert_eq!(&decrypted, plaintext);
}
#[test]
fn rot_round_trip_all_possible_keys() {
let plaintext = b"The quick brown fox jumps over the lazy dog!";
for key in 0u8..=255 {
let encrypted = RotN::new().encrypt(&[key], plaintext).unwrap();
let decrypted = RotN::new().decrypt(&[key], &encrypted).unwrap();
assert_eq!(
&decrypted, plaintext,
"Failed for key {key}: round-trip mismatch",
);
}
}
}