From 66b440adf6f82be75fc3cba53a833834a2321091 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Fri, 17 Oct 2025 22:53:35 +0300 Subject: [PATCH] feat(cli): make flat command structure --- cipher-core/src/traits.rs | 2 + cli/src/args.rs | 225 +++++--------------------------------- cli/src/cipher.rs | 14 +++ cli/src/error.rs | 26 +++++ cli/src/main.rs | 36 +++--- cli/src/output.rs | 14 +++ cli/src/value.rs | 131 ++++++++++++++++++++++ des/src/des.rs | 4 + des/src/key/des_key.rs | 21 +++- 9 files changed, 258 insertions(+), 215 deletions(-) create mode 100644 cli/src/cipher.rs create mode 100644 cli/src/error.rs create mode 100644 cli/src/output.rs create mode 100644 cli/src/value.rs diff --git a/cipher-core/src/traits.rs b/cipher-core/src/traits.rs index 4c29225..7ad1add 100644 --- a/cipher-core/src/traits.rs +++ b/cipher-core/src/traits.rs @@ -8,6 +8,8 @@ use crate::{CipherAction, CipherError, CipherOutput, CipherResult}; pub trait BlockCipher: Sized { const BLOCK_SIZE: usize; + fn from_key(key: &[u8]) -> Self; + /// Core cipher transformation (must be implemented by concrete types). /// /// # Errors diff --git a/cli/src/args.rs b/cli/src/args.rs index f957910..e90220c 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -1,204 +1,39 @@ -use clap::{Parser, Subcommand, ValueEnum}; -use std::{ - fmt::{Display, LowerHex, UpperHex}, - fs::read_to_string, - path::PathBuf, - str::FromStr, -}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ValueError { - #[error("String contains no content")] - EmptyString, - - #[error("File '{0}' contains no content")] - EmptyFile(PathBuf), - - #[error("Failed to find file '{0}'. File does not exist")] - MissingFile(PathBuf), - - #[error("Failed to read file '{0}'. Cannot read file contents")] - FileReadingError(PathBuf), - - #[error("Invalid number format: {0}")] - InvalidFormat(String), - - #[error("Invalid byte string length: expected no more than 8, found {0}")] - InvalidByteStringLength(usize), - - #[error("String-to-u64 conversion error: {0}")] - ConversionError(String), -} +use crate::{output::OutputFormat, value::Value}; +use clap::{Parser, ValueEnum}; +use std::str::FromStr; #[derive(Debug, Clone, Parser)] #[command(version, about, long_about = None)] pub struct Args { - #[command(subcommand)] - pub operation: Operation, + /// Operation to perform + #[arg(value_name = "OPERATION")] + pub operation: OperationChoice, + + /// Encryption algorithm + #[arg(short, long)] + pub algorithm: AlgorithmChoice, + + /// Key used for encryption/decryption. Can be a string or a path to a file + #[arg(short, long, value_parser = Value::from_str, required = true)] + pub key: Value, + + /// The text to encrypt/decrypt. Can be a string or a path to a file + #[arg(value_name = "TEXT", value_parser = Value::from_str, required = true)] + pub text: Value, + + /// Output format for decrypted data + #[arg(short = 'f', long)] + pub output_format: Option, } -#[derive(Debug, Clone, Subcommand)] -pub enum Operation { - /// Encrypt data - Encrypt { - /// Key used to encrypt/decrypt data (64-bit number, string, or path to file) - #[arg(short, long, value_parser = Value::from_str, required = true)] - key: Value, - - /// The text to encrypt/decrypt data (64-bit number, string, or path to file) - #[arg(value_name = "TEXT", value_parser = Value::from_str, required = true)] - text: Value, - }, - /// Decrypt data - Decrypt { - /// Key used to encrypt/decrypt data (64-bit number, string, or path to file) - #[arg(short, long, value_parser = Value::from_str, required = true)] - key: Value, - - /// The text to encrypt/decrypt data (64-bit number, string, or path to file) - #[arg(value_name = "TEXT", value_parser = Value::from_str, required = true)] - text: Value, - - /// Output format for decrypted data - #[arg(short = 'f', long, value_enum)] - output_format: Option, - }, +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum AlgorithmChoice { + Des, + Aes, } -#[derive(Debug, Clone, Default, ValueEnum)] -pub enum OutputFormat { - /// Binary output - Binary, - /// Octal output (fixed typo) - Octal, - /// Decimal output - #[default] - Hex, - /// Text output (ASCII) - Text, -} - -#[derive(Debug, Clone, Copy)] -pub struct Value(u64); - -impl Value { - #[inline] - #[must_use] - pub const fn as_64(self) -> u64 { - self.0 - } - - #[inline] - #[must_use] - pub const fn to_be_bytes(self) -> [u8; 8] { - self.0.to_be_bytes() - } -} - -impl From for u64 { - fn from(value: Value) -> Self { - value.as_64() - } -} - -impl From for Value { - fn from(value: u64) -> Self { - Self(value) - } -} - -impl FromStr for Value { - type Err = ValueError; - fn from_str(s: &str) -> Result { - if let Ok(num) = s.parse::() { - return Ok(Self(num)); - } - - let path = PathBuf::from(s); - if path.exists() && path.is_file() { - if let Ok(contents) = read_to_string(&path) { - let value = parse_string_to_u64(&contents)?; - return Ok(Self(value)); - } - return Err(ValueError::FileReadingError(path)); - } - - let value = parse_string_to_u64(s)?; - Ok(Self(value)) - } -} - -fn parse_string_to_u64(s: &str) -> Result { - let trimmed = s.trim(); - - if trimmed.is_empty() { - return Err(ValueError::EmptyString); - } - - // Hexadecimal with 0x/0X prefix - if let Some(hex_str) = trimmed - .strip_prefix("0X") - .or_else(|| trimmed.strip_prefix("0x")) - { - return parse_radix(hex_str, 16, "Hex"); - } - - // Binary with 0b/0B prefix - if let Some(bin_str) = trimmed - .strip_prefix("0b") - .or_else(|| trimmed.strip_prefix("0B")) - { - return parse_radix(bin_str, 2, "Binary"); - } - - // 8-character ASCII string conversion to u64 - if trimmed.len() > 8 { - return Err(ValueError::InvalidByteStringLength(trimmed.len())); - } - - ascii_string_to_u64(trimmed) -} - -fn parse_radix(s: &str, radix: u32, name: &str) -> Result { - let trimmed = s.trim_start_matches('0'); - if trimmed.is_empty() { - return Ok(0); - } - - u64::from_str_radix(trimmed, radix) - .map_err(|e| ValueError::InvalidFormat(format!("{name} parsing failed: {e}"))) -} - -fn ascii_string_to_u64(s: &str) -> Result { - if !s.is_ascii() { - return Err(ValueError::ConversionError( - "String contains non-ASCII characters".into(), - )); - } - - let mut bytes = [0; 8]; - for (idx, byte) in s.bytes().enumerate() { - bytes[idx] = byte; - } - - Ok(u64::from_be_bytes(bytes)) -} - -impl Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:0b}", self.0) - } -} - -impl UpperHex for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:016X}", self.0) - } -} - -impl LowerHex for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:016x}", self.0) - } +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum OperationChoice { + Encrypt, + Decrypt, } diff --git a/cli/src/cipher.rs b/cli/src/cipher.rs new file mode 100644 index 0000000..96e3616 --- /dev/null +++ b/cli/src/cipher.rs @@ -0,0 +1,14 @@ +use crate::{args::AlgorithmChoice, value::Value}; +use cipher_core::BlockCipher; +use des::Des; + +impl AlgorithmChoice { + #[must_use] + pub fn get_cipher(&self, key: Value) -> impl BlockCipher { + let key = key.to_be_bytes(); + match self { + Self::Des => Des::from_key(&key), + Self::Aes => todo!("Must implement AES first"), + } + } +} diff --git a/cli/src/error.rs b/cli/src/error.rs new file mode 100644 index 0000000..a8efbfc --- /dev/null +++ b/cli/src/error.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ValueError { + #[error("String contains no content")] + EmptyString, + + #[error("File '{0}' contains no content")] + EmptyFile(PathBuf), + + #[error("Failed to find file '{0}'. File does not exist")] + MissingFile(PathBuf), + + #[error("Failed to read file '{0}'. Cannot read file contents")] + FileReadingError(PathBuf), + + #[error("Invalid number format: {0}")] + InvalidFormat(String), + + #[error("Invalid byte string length: expected no more than 8, found {0}")] + InvalidByteStringLength(usize), + + #[error("String-to-u64 conversion error: {0}")] + ConversionError(String), +} diff --git a/cli/src/main.rs b/cli/src/main.rs index d4bc23e..e812cce 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,26 +1,34 @@ mod args; +mod cipher; +mod error; +mod output; +mod value; -use crate::args::{Args, Operation, OutputFormat}; +use crate::{ + args::{Args, OperationChoice}, + output::OutputFormat, +}; use cipher_core::BlockCipher; use clap::Parser; -use des::Des; fn main() -> anyhow::Result<()> { - let args = Args::parse(); + let Args { + operation, + algorithm, + key, + text, + output_format, + } = Args::parse(); - match args.operation { - Operation::Encrypt { key, text } => { - let des = Des::new(key.as_64()); - let ciphertext = des.encrypt(&text.to_be_bytes())?; + match operation { + OperationChoice::Encrypt => { + let cipher = algorithm.get_cipher(key); + let ciphertext = cipher.encrypt(&text.to_be_bytes())?; println!("{ciphertext:016X}"); } - Operation::Decrypt { - key, - text, - output_format, - } => { - let des = Des::new(key.as_64()); - let plaintext = des.decrypt(&text.to_be_bytes())?; + OperationChoice::Decrypt => { + let cipher = algorithm.get_cipher(key); + let plaintext = cipher.decrypt(&text.to_be_bytes())?; match output_format.unwrap_or_default() { OutputFormat::Binary => println!("{plaintext:064b}"), OutputFormat::Octal => println!("{plaintext:022o}"), diff --git a/cli/src/output.rs b/cli/src/output.rs new file mode 100644 index 0000000..34fd07b --- /dev/null +++ b/cli/src/output.rs @@ -0,0 +1,14 @@ +use clap::ValueEnum; + +#[derive(Debug, Clone, Default, ValueEnum)] +pub enum OutputFormat { + /// Binary output + Binary, + /// Octal output + Octal, + /// Decimal output + #[default] + Hex, + /// Text output (ASCII) + Text, +} diff --git a/cli/src/value.rs b/cli/src/value.rs new file mode 100644 index 0000000..3a15dd3 --- /dev/null +++ b/cli/src/value.rs @@ -0,0 +1,131 @@ +use crate::error::ValueError; +use std::{ + fmt::{Display, LowerHex, UpperHex}, + fs::read_to_string, + path::PathBuf, + str::FromStr, +}; + +#[derive(Debug, Clone, Copy)] +pub struct Value(u64); + +impl Value { + #[inline] + #[must_use] + pub const fn as_64(self) -> u64 { + self.0 + } + + #[inline] + #[must_use] + pub const fn to_be_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } +} + +impl From for u64 { + fn from(value: Value) -> Self { + value.as_64() + } +} + +impl From for Value { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl FromStr for Value { + type Err = ValueError; + fn from_str(s: &str) -> Result { + if let Ok(num) = s.parse::() { + return Ok(Self(num)); + } + + let path = PathBuf::from(s); + if path.exists() && path.is_file() { + if let Ok(contents) = read_to_string(&path) { + let value = parse_string_to_u64(&contents)?; + return Ok(Self(value)); + } + return Err(ValueError::FileReadingError(path)); + } + + let value = parse_string_to_u64(s)?; + Ok(Self(value)) + } +} + +fn parse_string_to_u64(s: &str) -> Result { + let trimmed = s.trim(); + + if trimmed.is_empty() { + return Err(ValueError::EmptyString); + } + + // Hexadecimal with 0x/0X prefix + if let Some(hex_str) = trimmed + .strip_prefix("0X") + .or_else(|| trimmed.strip_prefix("0x")) + { + return parse_radix(hex_str, 16, "Hex"); + } + + // Binary with 0b/0B prefix + if let Some(bin_str) = trimmed + .strip_prefix("0b") + .or_else(|| trimmed.strip_prefix("0B")) + { + return parse_radix(bin_str, 2, "Binary"); + } + + // 8-character ASCII string conversion to u64 + if trimmed.len() > 8 { + return Err(ValueError::InvalidByteStringLength(trimmed.len())); + } + + ascii_string_to_u64(trimmed) +} + +fn parse_radix(s: &str, radix: u32, name: &str) -> Result { + let trimmed = s.trim_start_matches('0'); + if trimmed.is_empty() { + return Ok(0); + } + + u64::from_str_radix(trimmed, radix) + .map_err(|e| ValueError::InvalidFormat(format!("{name} parsing failed: {e}"))) +} + +fn ascii_string_to_u64(s: &str) -> Result { + if !s.is_ascii() { + return Err(ValueError::ConversionError( + "String contains non-ASCII characters".into(), + )); + } + + let mut bytes = [0; 8]; + for (idx, byte) in s.bytes().enumerate() { + bytes[idx] = byte; + } + + Ok(u64::from_be_bytes(bytes)) +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:0b}", self.0) + } +} + +impl UpperHex for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:016X}", self.0) + } +} + +impl LowerHex for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:016x}", self.0) + } +} diff --git a/des/src/des.rs b/des/src/des.rs index 51bf0fb..25ba72f 100644 --- a/des/src/des.rs +++ b/des/src/des.rs @@ -37,6 +37,10 @@ impl Des { impl BlockCipher for Des { const BLOCK_SIZE: usize = 8; + fn from_key(key: &[u8]) -> Self { + Self::new(key) + } + fn transform_impl( &self, block: &[u8], diff --git a/des/src/key/des_key.rs b/des/src/key/des_key.rs index 5743c9c..5402cfa 100644 --- a/des/src/key/des_key.rs +++ b/des/src/key/des_key.rs @@ -31,6 +31,21 @@ impl From<[u8; 8]> for Key { } } +impl From<&[u8]> for Key { + fn from(value: &[u8]) -> Self { + let mut bytes = [0; 8]; + let len = value.len().min(8); + bytes[..len].copy_from_slice(&value[..len]); + Self(bytes) + } +} + +impl From for Key { + fn from(key: u64) -> Self { + Self(key.to_be_bytes()) + } +} + impl From for [u8; 8] { fn from(key: Key) -> Self { key.0 @@ -43,12 +58,6 @@ impl AsRef<[u8]> for Key { } } -impl From for Key { - fn from(key: u64) -> Self { - Self(key.to_be_bytes()) - } -} - impl Debug for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Key([REDACTED])")