use log::{debug, warn};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum EncodingError {
#[error("input must be ASCII string")]
NonAsciiInput,
#[error("no valid sentences found")]
NoValidSentences,
}
#[derive(Error, Debug)]
pub enum DecodingError {
#[error("character set cannot be empty")]
EmptyCharacterSet,
#[error("invalid code: {0}")]
InvalidCode(usize),
}
#[derive(Error, Debug)]
pub enum CompareError {
#[error("Value out of range")]
ValueOutOfRange,
#[error("Error encoding cover text: {0}")]
EncodingError(#[from] EncodingError),
#[error("Character '{0}' not found in character set")]
CharacterNotFound(char),
}
pub fn encode(txt: &str) -> Result<Vec<usize>, EncodingError> {
if !txt.is_ascii() {
warn!("Non-ASCII string encountered");
return Err(EncodingError::NonAsciiInput);
}
let encoded: Vec<usize> = txt
.split(|c: char| ['.', '!', '?'].contains(&c))
.map(|s| s.split_whitespace().count())
.filter(|&count| count > 0)
.collect();
if encoded.is_empty() {
warn!("No valid sentences found in the input text");
return Err(EncodingError::NoValidSentences);
}
debug!("Encoded text: {:?}", encoded);
Ok(encoded)
}
pub fn decode(encoded: &[usize], character_set: &str) -> Result<String, DecodingError> {
if character_set.is_empty() {
warn!("Character set is empty");
return Err(DecodingError::EmptyCharacterSet);
}
let charset_len = character_set.len();
let decoded: Result<String, _> = encoded
.iter()
.filter(|&&code| code != 0)
.map(|&code| {
character_set
.chars()
.nth((code - 1) % charset_len)
.ok_or(DecodingError::InvalidCode(code))
})
.collect();
match decoded {
Ok(d) => {
debug!("Decoded string: {}", d);
Ok(d)
}
Err(e) => {
warn!("Decoding error: {:?}", e);
Err(e)
}
}
}
pub fn compare(
secret_message: &str,
cover_text: &str,
character_set: &str,
) -> Result<Vec<isize>, CompareError> {
if secret_message.is_empty() {
return Ok(vec![]);
}
let charset_map: HashMap<char, isize> = character_set
.chars()
.enumerate()
.map(|(i, c)| {
isize::try_from(i).map_or(Err(CompareError::ValueOutOfRange), |val| Ok((c, val + 1)))
})
.collect::<Result<_, _>>()?;
let cover_encoded = encode(cover_text).map_err(CompareError::EncodingError)?;
let secret_positions = secret_message
.chars()
.map(|c| {
charset_map
.get(&c)
.copied()
.ok_or_else(|| CompareError::CharacterNotFound(c))
})
.collect::<Result<Vec<isize>, _>>()?;
let mut changes = vec![0; cover_encoded.len()];
for (i, &pos) in secret_positions.iter().enumerate() {
if i < cover_encoded.len() {
changes[i] = pos
- isize::try_from(cover_encoded[i]).map_err(|_| CompareError::ValueOutOfRange)?;
} else {
changes.push(pos);
}
}
for i in secret_positions.len()..cover_encoded.len() {
changes[i] =
-isize::try_from(cover_encoded[i]).map_err(|_| CompareError::ValueOutOfRange)?;
}
Ok(changes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_valid_input() {
let input = "This is a sentence. This is another.";
let result = encode(input).expect("Failed to encode");
assert_eq!(result, vec![4, 3]);
}
#[test]
fn test_encode_non_ascii_input() {
let input = "This is a sentence with non-ascii char ö.";
match encode(input) {
Ok(encoded) => println!("Encoded text: {encoded:?}"),
Err(EncodingError::NonAsciiInput) => println!("Input text must be ASCII"),
Err(EncodingError::NoValidSentences) => println!("No valid sentences found"),
}
}
#[test]
fn test_decode_basic() {
let encoded = vec![1, 26, 5]; let character_set = "abcdefghijklmnopqrstuvwxyz";
let result = decode(&encoded, character_set).expect("Failed to decode");
assert_eq!(result, "aze"); }
#[test]
fn test_decode_with_empty_input() {
let encoded = vec![];
let character_set = "abcdefghijklmnopqrstuvwxyz";
let result = decode(&encoded, character_set).expect("Failed to decode");
assert_eq!(result, ""); }
#[test]
fn test_basic_wps_encode() {
let input = "Hello, this is a test.\n Does this work?\n I sure hope so.";
let expected = vec![5, 3, 4];
let result = encode(input).expect("Failed to encode");
assert_eq!(result, expected);
}
#[test]
fn test_incomplete_sentence_encode() {
let input = "Hello. This is a great tool. .. Bad sentence punctuation";
let expected = vec![1, 5, 3];
let result = encode(input).expect("Failed to encode");
assert_eq!(result, expected);
}
#[test]
fn test_normal_decode() {
let hidden_msg = "SECRET";
let cover_text = "\n Hello Bob, I hope you are well and good, I would like to know if you are free tomorrow.\n Hmm, How about a picnic?\n\n At the park?\n\n I would very much look forward to that, but will jane bring her dog, i was just wondering?\n\n Anyway i would like to.\n But how many days until Sara will be making her famous lemon drizzle cake, it was to die for, before.\n ";
let character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let encoded = encode(cover_text).unwrap(); let result = decode(&encoded, character_set).expect("Failed to decode");
assert_eq!(result, hidden_msg);
}
#[test]
fn test_compare_exact_match() {
let secret_message = "HELLO";
let cover_text = "This is a sentence. And another one.";
let character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = compare(secret_message, cover_text, character_set).expect("Failed to compare");
assert_eq!(result, vec![4, 2, 12, 12, 15]);
}
#[test]
fn test_compare_length_mismatch() {
let secret_message = "HELLO";
let cover_text = "This is a sentence. This is another. And yet another.";
let character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = compare(secret_message, cover_text, character_set).expect("Failed to compare");
assert_eq!(result, vec![4, 2, 9, 12, 15]);
}
#[test]
fn test_compare_non_ascii_input() {
let secret_message = "HELLO";
let cover_text = "This sentence contains ö.";
let character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = compare(secret_message, cover_text, character_set);
assert!(result.is_err());
}
#[test]
fn test_compare_empty_input() {
let secret_message = "";
let cover_text = "";
let character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = compare(secret_message, cover_text, character_set).expect("Failed to compare");
assert_eq!(result, Vec::<isize>::new());
}
}