From 7e16b04e6e94045a9209a3c81868873813719f9f Mon Sep 17 00:00:00 2001 From: indomitableSwan <5496520+indomitableSwan@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:13:57 -0400 Subject: [PATCH] Clean up error handling, part one (#35) * refactor using thiserror crate * Created an opaque error type to hide internal error representation details * Collected internal errors under one private enum `ErrorRepr` for future use * rewrite error messages and attach context to public types for greater clarity * Internal code cleanup where possible (simplifying code, reorganization for better readability, making more idiomatic) * reduce code duplication in demo by adding trait bounds to the parser helper `process_input` requiring the output type's `FromStr` Error to implement `Display`. This also allows the demo writer to pass through errors from the crypto library transparently instead of rewriting similar content. * Add tests: *Writing failing test for encryption/decryption, caused by lack of validation in RingElement construction. Relates to #1. This test only works within the library bc `RingElement` is private. * add test that demonstrates expected, but potentially surprising behavior reading RingElements from i8s --- Makefile.toml | 15 +-- classical_crypto/Cargo.toml | 1 + classical_crypto/src/errors.rs | 49 +++++++ classical_crypto/src/lib.rs | 150 ++++++++++++--------- classical_crypto/src/shift.rs | 78 +++++++++-- classical_crypto/tests/integration_test.rs | 18 +++ demo/src/crypto_functionality.rs | 40 +++--- demo/src/lib.rs | 19 ++- demo/src/menu.rs | 7 + 9 files changed, 258 insertions(+), 119 deletions(-) create mode 100644 classical_crypto/src/errors.rs diff --git a/Makefile.toml b/Makefile.toml index 58179fa..0a9b069 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,5 +1,5 @@ -# This is a makefile that can be run using `cargo make`. -# See https://sagiegurari.github.io/cargo-make/ for documentation. +# This is a makefile that can be# See https://sagiegurari.github.io/cargo-make/ for documentation. +# run using `cargo make`. # There is a lot more functionality than what is used here, including # testing multiple platforms, running benchmarks, security checks, etc. @@ -18,14 +18,10 @@ args = ["clippy", "--all-targets", "--workspace"] [tasks.build] dependencies = ["clean"] -# Since examples are not specifed as test code, you have to pass -# the option "--examples" to make sure that the dev flow builds -# and runs all the associated examples for the crate. This helps -# catch problems early that might otherwise impact projects -# dependent on the library. -[tasks.examples] +# Make sure every to test everything +[tasks.test] command = "cargo" -args = ["test", "--examples", "--workspace"] +args = ["test", "--workspace", "--all-targets"] # Potentially useful options to add include: # - "--document-private-items", since this is a tutorial-style library. @@ -74,6 +70,5 @@ dependencies = [ "clippy", "build", "test", - "examples", ] diff --git a/classical_crypto/Cargo.toml b/classical_crypto/Cargo.toml index 75de9ed..c90cb33 100644 --- a/classical_crypto/Cargo.toml +++ b/classical_crypto/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] rand = "0.8" +thiserror = "1" [dev-dependencies] rand_chacha = "0.3.1" \ No newline at end of file diff --git a/classical_crypto/src/errors.rs b/classical_crypto/src/errors.rs new file mode 100644 index 0000000..08300aa --- /dev/null +++ b/classical_crypto/src/errors.rs @@ -0,0 +1,49 @@ +//! Contains custom error types. +use thiserror::Error; + +/// An opaque error type that hides the implementation details of internal +/// errors. +// This is a technique that is easy to use with the `thiserror` crate. The attribute +// `error(transparent)` forwards the source and display methods straight through to the underlying +// internal error representations. +#[derive(Error, Debug, PartialEq)] +#[error(transparent)] +pub struct InternalError(#[from] ErrorRepr); + +/// Internal errors. +#[derive(Clone, Debug, PartialEq, Error)] +pub(super) enum ErrorRepr { + /// Thrown when a conversion between the Latin + /// Alphabet and the ring of integers modulo [`RingElement::MODULUS`] fails. + /// + /// This error should only be thrown if: + /// - There is a mistake in the definition of the constant + /// [`RingElement::ALPH_ENCODING`]; + /// - The input was not a lowercase letter from the Latin Alphabet. + #[error("Failed to encode the following characters as ring elements: {0}")] + RingElementEncodingError(String), +} + +// TODO: Are these usable for other ciphers? +/// An error type that indicates a failure to parse a string. +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum EncodingError { + /// Error thrown when parsing a string as a message. This error is thrown + /// when the string included one or more characters that are not + /// lowercase letters from the Latin Alphabet. + #[error("Invalid Message. {0}")] + InvalidMessage(InternalError), + /// Error thrown when parsing a string as a ciphertext. This error is thrown + /// when the string included one or more characters that are not letters + /// from the Latin Alphabet. We allow for strings containing both + /// capitalized and lowercase letters when parsing as string as a + /// ciphertext. + #[error("Invalid Ciphertext. {0}")] + InvalidCiphertext(InternalError), + /// Error thrown when parsing a string as a key. This error is thrown when + /// the string does not represent a number in the appropriate + /// range. e.g., for the Latin Shift Cipher, keys are in the range 0 to 25, + /// inclusive. + #[error("Input \"{0}\" does not represent a valid key")] + InvalidKey(String), +} diff --git a/classical_crypto/src/lib.rs b/classical_crypto/src/lib.rs index babe894..f583001 100644 --- a/classical_crypto/src/lib.rs +++ b/classical_crypto/src/lib.rs @@ -36,8 +36,11 @@ use std::{ str::FromStr, }; +pub mod errors; pub mod shift; +use crate::errors::{EncodingError, ErrorRepr}; + /// This trait represents a deterministic cipher. pub trait CipherTrait { /// The message space (plaintext space) of the cipher. @@ -49,21 +52,11 @@ pub trait CipherTrait { /// The keyspace of the cipher, which must implement the [`KeyTrait`] trait. type Key: KeyTrait; - // TODO: not implemented yet - /// The error type returned by [`CipherTrait::encrypt`]. - type EncryptionError; - - // TODO: not implemented yet - /// The error type returned by [`CipherTrait::decrypt`]. - type DecryptionError; - - // TODO: Return a Result instead /// The encryption function of the cipher. /// Invariant: For each key `k` in the keyspace, we have decrypt(encrypt(m, /// k), k) = m for every message `m` in the message space. fn encrypt(msg: &Self::Message, key: &Self::Key) -> Self::Ciphertext; - // TODO: Return a Result instead /// The decryption function of the cipher. /// Invariant: For each key `k` in the keyspace, we have decrypt(encrypt(m, /// k), k) = m for every message `m` in the message space. @@ -106,16 +99,6 @@ trait Ring: #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] struct RingElement(i8); -/// A custom error type that is thrown when a conversion between the Latin -/// Alphabet and the ring of integers modulo [`RingElement::MODULUS`] fails. -/// -/// This error should only be thrown if: -/// - There is a mistake in the definition of the constant -/// [`RingElement::ALPH_ENCODING`]; -/// - The input was not a lowercase letter from the Latin Alphabet. -#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] -struct RingElementEncodingError; - impl RingElement { /// The default alphabet encoding for the Latin Shift Cipher. const ALPH_ENCODING: [(char, i8); 26] = [ @@ -177,23 +160,23 @@ impl RingElement { } impl AlphabetEncoding for RingElement { - type Error = RingElementEncodingError; + type Error = ErrorRepr; /// Convert from a character. /// /// # Errors - /// This method will return a custom pub(crate) error if the constant + /// This method will return a custom internal error if the constant /// [`RingElement::ALPH_ENCODING`] does not specify a mapping to the ring of /// integers for the given input. This happens if the input is not from the /// lowercase Latin Alphabet. For crate users, this error type will get /// "lifted" to the public error type [`EncodingError`] by the caller, e.g., /// when parsing a [`Message`] from a string. - fn from_char(ltr: char) -> Result { + fn from_char(ltr: char) -> Result { // This constructor uses the encoding defined in `RingElement::ALPH_ENCODING`. RingElement::ALPH_ENCODING .into_iter() .find_map(|(x, y)| if x == ltr { Some(RingElement(y)) } else { None }) - .ok_or(RingElementEncodingError) + .ok_or(ErrorRepr::RingElementEncodingError(ltr.to_string())) } /// Convert from a ring element to a character. @@ -295,21 +278,6 @@ impl Message { } } -/// An error type that indicates a failure to parse a string. -/// -/// This is likely because the string violates one of the constraints -/// for the desired value type. That is: -/// -/// - For messages: The string included one or more characters that are not -/// lowercase letters from the Latin Alphabet. -/// - For ciphertexts: The string included one or more characters that are not -/// letters from the Latin Alphabet. We allow for strings containing both -/// capitalized and lowercase letters when parsing as string as a ciphertext. -/// - For key values: The string does not represent a number in the appropriate -/// range. For the Latin Alphabet, this range is 0 to 25, inclusive. -#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] -pub struct EncodingError; - /// Parse a message from a string. /// /// # Errors @@ -320,9 +288,10 @@ impl FromStr for Message { type Err = EncodingError; fn from_str(s: &str) -> Result { - s.chars() - .map(|i| RingElement::from_char(i).or(Err(EncodingError))) - .collect() + match from_str(s) { + Ok(msg) => Ok(Message(msg)), + Err(e) => Err(EncodingError::InvalidMessage(e.into())), + } } } @@ -333,17 +302,10 @@ impl fmt::Display for Message { write!(f, "{txt}") } } -// Question: Can I do something generic here that covers both Message and -// Ciphertext? + impl FromIterator for Message { fn from_iter>(iter: I) -> Self { - let mut c = Vec::new(); - - for i in iter { - c.push(i); - } - - Message(c) + Message(iter.into_iter().collect()) } } @@ -360,10 +322,10 @@ impl FromStr for Ciphertext { type Err = EncodingError; fn from_str(s: &str) -> Result { - s.to_lowercase() - .chars() - .map(|i| RingElement::from_char(i).or(Err(EncodingError))) - .collect() + match from_str(&s.to_lowercase()) { + Ok(msg) => Ok(Ciphertext(msg)), + Err(e) => Err(EncodingError::InvalidCiphertext(e.into())), + } } } @@ -379,13 +341,34 @@ impl fmt::Display for Ciphertext { impl FromIterator for Ciphertext { fn from_iter>(iter: I) -> Self { - let mut c = Vec::new(); - - for i in iter { - c.push(i); - } + Ciphertext(iter.into_iter().collect()) + } +} - Ciphertext(c) +// Parse a string as a `Vec` +// We cannot implement `FromStr` for `Vec` (since both `FromStr` +// and `Vec` are external to our crate), but we need similar functionality in +// order to avoid code duplication when converting from Strings to Wrapper types +// around `Vec`` +fn from_str(s: &str) -> Result, ErrorRepr> { + let (msg, errors): (Vec<_>, Vec<_>) = s + .chars() + .map(|i: char| RingElement::from_char(i)) + .partition(Result::is_ok); + + let msg: Vec = msg.into_iter().map(|i| i.unwrap()).collect(); + + let errors: String = errors + .into_iter() + .map(|i| i.unwrap_err()) + .map(|ErrorRepr::RingElementEncodingError(i)| i) + .filter(|i| i != " ") + .collect(); + + if errors.is_empty() && !msg.is_empty() { + Ok(msg) + } else { + Err(ErrorRepr::RingElementEncodingError(errors)) } } @@ -406,7 +389,7 @@ mod tests { // Encrypted "wewillmeetatmidnight" message with key=11, from Example 1.1, // Stinson 3rd Edition, Example 2.1 Stinson 4th Edition - thread_local! (static CIPH0: Ciphertext = Ciphertext(vec![RingElement(7), RingElement(15), + thread_local! (static CIPH0: Ciphertext = Ciphertext(vec![RingElement(7), RingElement(15), RingElement(7), RingElement(19), RingElement(22), RingElement(22), RingElement(23), RingElement(15), RingElement(15), RingElement(4), RingElement(11), RingElement(4), @@ -467,9 +450,20 @@ mod tests { } #[test] - fn ring_elmt_encoding_error() { - assert_eq!(RingElement::from_char('_'), Err(RingElementEncodingError)); - assert_eq!(RingElement::from_char('A'), Err(RingElementEncodingError)); + fn ring_elmt_encoding_errors() { + assert_eq!( + RingElement::from_char('_'), + Err(ErrorRepr::RingElementEncodingError('_'.to_string())) + ); + assert_eq!( + RingElement::from_char('A'), + Err(ErrorRepr::RingElementEncodingError('A'.to_string())) + ); + + assert_eq!( + from_str("asd;lkasdfEnk0").unwrap_err(), + ErrorRepr::RingElementEncodingError(";E0".to_string()) + ) } #[test] @@ -477,6 +471,10 @@ mod tests { expected = "Could not map to `char`: The definition of `RingElement::ALPH_ENCODING` must have an error or there is an invalid `RingElement`." )] fn ring_elmt_encoding_panic() { + // Sometimes you google to find out how to prevent things like backtraces + // appearing in your output for tests that should panic + let f = |_: &std::panic::PanicInfo| {}; + std::panic::set_hook(Box::new(f)); let _fail = RingElement(26).to_char(); } @@ -506,7 +504,20 @@ mod tests { #[test] // Malformed message errors. fn msg_encoding_error() { - assert_eq!(Message::new("we will meet at midnight"), Err(EncodingError)) + println!("{}", Message::new("what; on earh#A").unwrap_err()); + assert_eq!( + Message::new("we~ will Meet at midnight;"), + Err(EncodingError::InvalidMessage( + ErrorRepr::RingElementEncodingError("~M;".to_string()).into() + )) + ); + + assert_eq!( + Message::new(""), + Err(EncodingError::InvalidMessage( + ErrorRepr::RingElementEncodingError("".to_string()).into() + )) + ) } #[test] @@ -540,6 +551,11 @@ mod tests { #[test] fn ciphertxt_encoding_error() { - assert_eq!(Ciphertext::from_str("a;k"), Err(EncodingError)) + assert_eq!( + Ciphertext::from_str("a;k"), + Err(EncodingError::InvalidCiphertext( + ErrorRepr::RingElementEncodingError(";".to_string()).into() + )) + ) } } diff --git a/classical_crypto/src/shift.rs b/classical_crypto/src/shift.rs index 2830d2e..f0b4b58 100644 --- a/classical_crypto/src/shift.rs +++ b/classical_crypto/src/shift.rs @@ -10,8 +10,11 @@ use rand::{CryptoRng, Rng}; use std::{fmt::Display, str::FromStr}; /// The ciphertext space for the Latin Shift Cipher. -// Notes: -// This is a wrapper type around the library's private representation of a ciphertext using the ring of integers mod 26. We do this because we want to force library users to use types specific to the Latin Shift cipher when using the Latin Shift Cipher, even though other ciphers may also (mathematically and under the hood in the implementation) operate on the same underlying types +// Notes: +// This is a wrapper type around the library's private representation of a ciphertext using the +// ring of integers mod 26. We do this because we want to force library users to use types specific +// to the Latin Shift cipher when using the Latin Shift Cipher, even though other ciphers may also +// (mathematically and under the hood in the implementation) operate on the same underlying types #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct Ciphertext(Ciphtxt); @@ -35,8 +38,12 @@ impl FromIterator for Ciphertext { } /// The message space of the Latin Shift Cipher. -// Notes: -// 1. This is a wrapper type around the library's private representation of a ciphertext using the ring of integers mod 26. We do this because we want to force library users to use types specific to the Latin Shift cipher when using the Latin Shift Cipher, even though other ciphers may also (mathematically and under the hood in the implementation) operate on the same underlying types +// Notes: +// 1. This is a wrapper type around the library's private representation of a ciphertext using the +// ring of integers mod 26. We do this because we want to force library users to use types +// specific to the Latin Shift cipher when using the Latin Shift Cipher, even though other +// ciphers may also (mathematically and under the hood in the implementation) operate on the same +// underlying types // 2. The Rust Book (19.3) offers guidance on using the `Deref` trait in the newtype pattern to automatically implement all methods defined on the inner type for the wrapper type. We do not do this because doing so makes for surprises in the API. Also note that this trick does not give you trait implementations defined on the inner type for the wrapper. See also discussion [`here`](https://rust-unofficial.github.io/patterns/anti_patterns/deref.html) #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct Message(Msg); @@ -138,12 +145,12 @@ impl FromStr for Key { fn from_str(s: &str) -> Result { let key = match i8::from_str(s) { Ok(num) => num, - Err(_) => return Err(EncodingError), + Err(_) => return Err(EncodingError::InvalidKey(s.to_string())), }; match key { x if (0..=25).contains(&x) => Ok(Key::from(RingElement::from_i8(key))), - _ => Err(EncodingError), + _ => Err(EncodingError::InvalidKey(s.to_string())), } } } @@ -164,9 +171,6 @@ impl CipherTrait for ShiftCipher { type Ciphertext = Ciphertext; type Key = Key; - type EncryptionError = EncryptionError; - type DecryptionError = DecryptionError; - /// Encrypt a message. /// /// # Examples @@ -337,6 +341,34 @@ mod tests { ) } + #[test] + #[should_panic( + expected = "Could not map to `char`: The definition of `RingElement::ALPH_ENCODING` must have an error or there is an invalid `RingElement`." + )] + fn unchecked_dec_panic() { + // Sometimes you google to find out how to prevent things like backtraces + // appearing in your output for tests that should panic + let f = |_: &std::panic::PanicInfo| {}; + std::panic::set_hook(Box::new(f)); + let ciph = Ciphertext(Ciphtxt(vec![RingElement(65)])); + + let key = Key(RingElement(0)); + println!("{}", ShiftCipher::decrypt(&ciph, &key)); + } + + #[test] + // Won't panic because appropriate constructor used for RingElement, but result + // may surprise the library developer + fn unchecked_dec_nopanic() { + let ciph = Ciphertext(Ciphtxt(vec![RingElement::from_i8(65)])); + + let key = Key(RingElement(0)); + assert_eq!( + ShiftCipher::decrypt(&ciph, &key), + Message::from_str("n").expect("Test writer should ensure this example does not panic") + ); + } + // Tests with randomly generated keys. #[test] fn enc_dec_random_keys() { @@ -393,4 +425,32 @@ mod tests { msg1 ) } + + #[test] + fn new_key_err() { + assert_eq!( + Key::from_str("65").unwrap_err(), + EncodingError::InvalidKey("65".to_string()) + ); + assert_eq!( + Key::from_str("").unwrap_err(), + EncodingError::InvalidKey("".to_string()) + ); + assert_eq!( + Key::from_str("-5").unwrap_err(), + EncodingError::InvalidKey("-5".to_string()) + ); + assert_eq!( + Key::from_str("26").unwrap_err(), + EncodingError::InvalidKey("26".to_string()) + ); + assert_eq!( + Key::from_str("asdfas").unwrap_err(), + EncodingError::InvalidKey("asdfas".to_string()) + ); + assert_eq!( + Key::from_str("4s").unwrap_err(), + EncodingError::InvalidKey("4s".to_string()) + ); + } } diff --git a/classical_crypto/tests/integration_test.rs b/classical_crypto/tests/integration_test.rs index 502686f..6492375 100644 --- a/classical_crypto/tests/integration_test.rs +++ b/classical_crypto/tests/integration_test.rs @@ -89,3 +89,21 @@ fn short_msg_example() { small_msg_1 ); } + +#[test] +fn new_msg_err() { + assert_eq!( + Message::new("this;crazy;world").unwrap_err().to_string(), + "Invalid Message. Failed to encode the following characters as ring elements: ;;" + ); +} + +#[test] +fn new_ciphtxt_err() { + assert_eq!( + Ciphertext::from_str("this;crazy;world") + .unwrap_err() + .to_string(), + "Invalid Ciphertext. Failed to encode the following characters as ring elements: ;;" + ) +} diff --git a/demo/src/crypto_functionality.rs b/demo/src/crypto_functionality.rs index 46eb4f7..3a1b767 100644 --- a/demo/src/crypto_functionality.rs +++ b/demo/src/crypto_functionality.rs @@ -21,11 +21,12 @@ pub fn make_key() -> Result<(), Box> { println!("\nWe generated your key successfully!."); println!("\nWe shouldn't export your key (or say, save it in logs), but we can!"); - println!("Here it is: {}", ShiftCipher::insecure_key_export(&key)); - println!("\nAre you happy with your key?"); - ConsentMenu::print_menu(); + println!("Here it is: {}\n", ShiftCipher::insecure_key_export(&key)); - let command: ConsentMenu = process_input(ConsentMenu::print_menu)?; + let command: ConsentMenu = process_input(|| { + println!("\nAre you happy with your key?"); + ConsentMenu::print_menu() + })?; match command { ConsentMenu::NoKE => continue, @@ -40,16 +41,13 @@ pub fn make_key() -> Result<(), Box> { /// Takes in a key and a message and encrypts, then prints /// the result. pub fn encrypt() -> Result<(), Box> { - println!("\nPlease enter the message you want to encrypt:"); - - let msg: Message = process_input(|| { - println!("\nWe only accept lowercase letters from the Latin Alphabet, in one of the most awkward \nAPI decisions ever."); - })?; + let msg: Message = + process_input(|| println!("\nPlease enter the message you want to encrypt:"))?; - println!("\nNow, do you have a key that was generated uniformly at random that you remember and \nwould like to use? If yes, please enter your key. Otherwise, please pick a fresh key \nuniformly at random from the ring of integers modulo 26 yourself. \n\nYou won't be as good at this as a computer, but if you understand the cryptosystem \nyou are using (something we cryptographers routinely assume about other people, while \npretending that we aren't assuming this), you will probably not pick a key of 0, \nwhich is equivalent to sending your messages \"in the clear\", i.e., unencrypted. Good \nluck! \n\nGo ahead and enter your key now:"); + println!("\nNow, do you have a key that was generated uniformly at random that you remember and \nwould like to use? If yes, please enter your key. Otherwise, please pick a fresh key \nuniformly at random from the ring of integers modulo 26 yourself. \n\nYou won't be as good at this as a computer, but if you understand the cryptosystem \nyou are using (something we cryptographers routinely assume about other people, while \npretending that we aren't assuming this), you will probably not pick a key of 0, \nwhich is equivalent to sending your messages \"in the clear\", i.e., unencrypted. Good \nluck! \n"); let key: Key = process_input(|| { - println!("{KEY_PROMPT}"); + println!("\nPlease enter a key now. Keys are numbers between 0 and 25 inclusive.") })?; println!("\nYour ciphertext is {}", ShiftCipher::encrypt(&msg, &key)); @@ -61,10 +59,10 @@ pub fn encrypt() -> Result<(), Box> { /// Takes in a ciphertext and attempts to decrypt and /// print result. pub fn decrypt(command: DecryptMenu) -> Result<(), Box> { - println!("\nEnter your ciphertext. Ciphertexts use characters only from the Latin Alphabet:"); - let ciphertxt: Ciphertext = process_input(|| { - println!("\nCiphertext must contain characters from the Latin Alphabet only."); + println!( + "\nEnter your ciphertext. Ciphertexts use characters only from the Latin Alphabet:" + ) })?; // Attempt decryption or stop trying @@ -84,9 +82,8 @@ pub fn decrypt(command: DecryptMenu) -> Result<(), Box> { /// Gets key from stdin and attempts to decrypt. pub fn chosen_key(ciphertxt: &Ciphertext) -> Result<(), Box> { loop { - println!("\nOK. Please enter a key now:"); let key: Key = process_input(|| { - println!("{KEY_PROMPT}"); + println!("\nPlease enter a key now. Keys are numbers between 0 and 25 inclusive.") })?; match try_decrypt(ciphertxt, key) { Ok(_) => break, @@ -104,7 +101,7 @@ pub fn computer_chosen_key(ciphertxt: &Ciphertext) -> Result<(), Box> let key = Key::new(&mut rng); match try_decrypt(ciphertxt, key) { Ok(_) => break, - Err(_) => continue, // TODO: How to handle different errors independently? + Err(_) => continue, } } Ok(()) @@ -116,15 +113,14 @@ pub fn try_decrypt(ciphertxt: &Ciphertext, key: Key) -> Result<(), Box Err("try again".into()), ConsentMenu::YesKE => Ok(()), } } - -const KEY_PROMPT: &str = "\nA key is a number between 0 and 25 inclusive."; diff --git a/demo/src/lib.rs b/demo/src/lib.rs index 9e53977..4e109d0 100644 --- a/demo/src/lib.rs +++ b/demo/src/lib.rs @@ -9,7 +9,7 @@ use crate::io_helper::process_input; use crate::menu::{DecryptMenu, MainMenu, Menu}; /// Presents main menu and runs user selection. -/// +/// /// Prints main menu of user options and matches on user input to do one of: /// - Generate a key; /// - Encrypt a message; @@ -17,9 +17,6 @@ use crate::menu::{DecryptMenu, MainMenu, Menu}; /// - Quit the CLI application. pub fn menu() -> Result<(), Box> { loop { - // Print the main menu - MainMenu::print_menu(); - // Get menu selection from user let command: MainMenu = process_input(MainMenu::print_menu)?; @@ -43,7 +40,7 @@ pub fn menu() -> Result<(), Box> { } /// Presents decryption menu and runs user selection. -/// +/// /// Prints menu of user decryption options and matches on user input to do one /// of: /// - Decrypt using a known key; @@ -57,9 +54,6 @@ pub fn decryption_menu() -> Result> { println!( "If not, don't despair. Just guess! On average, you can expect success using this \nsimple brute force attack method after trying 13 keys chosen uniformly at random." ); - println!("Pick one of the following options:"); - - DecryptMenu::print_menu(); let command: DecryptMenu = process_input(DecryptMenu::print_menu)?; Ok(command) @@ -76,18 +70,21 @@ mod io_helper { pub fn process_input(instr: F) -> Result> where T: FromStr, + ::Err: std::fmt::Display, F: Fn(), { loop { + // Print the instructions + instr(); + let mut input = String::new(); io::stdin().read_line(&mut input)?; let result: T = match input.trim().parse::() { Ok(txt) => txt, - Err(_) => { - instr(); - println!("\nPlease try again:"); + Err(_e) => { + //println!("Error. {}", e); continue; } }; diff --git a/demo/src/menu.rs b/demo/src/menu.rs index d7455b0..5c827bb 100644 --- a/demo/src/menu.rs +++ b/demo/src/menu.rs @@ -1,4 +1,5 @@ //! Menus. +use core::fmt; use std::str::FromStr; /// Represents menu functionality. @@ -183,3 +184,9 @@ pub struct Command<'a> { /// The error returned upon failure to parse a [`Command`] from a string. pub struct CommandError; + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid command") + } +}