mirror of
https://github.com/kristoferssolo/cipher-workshop.git
synced 2025-12-20 11:04:38 +00:00
feat(cli): make flat command structure
This commit is contained in:
parent
54ddcab377
commit
66b440adf6
@ -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
|
||||
|
||||
225
cli/src/args.rs
225
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<OutputFormat>,
|
||||
}
|
||||
|
||||
#[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<OutputFormat>,
|
||||
},
|
||||
#[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<Value> for u64 {
|
||||
fn from(value: Value) -> Self {
|
||||
value.as_64()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Value {
|
||||
type Err = ValueError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(num) = s.parse::<u64>() {
|
||||
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<u64, ValueError> {
|
||||
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<u64, ValueError> {
|
||||
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<u64, ValueError> {
|
||||
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,
|
||||
}
|
||||
|
||||
14
cli/src/cipher.rs
Normal file
14
cli/src/cipher.rs
Normal file
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
26
cli/src/error.rs
Normal file
26
cli/src/error.rs
Normal file
@ -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),
|
||||
}
|
||||
@ -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}"),
|
||||
|
||||
14
cli/src/output.rs
Normal file
14
cli/src/output.rs
Normal file
@ -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,
|
||||
}
|
||||
131
cli/src/value.rs
Normal file
131
cli/src/value.rs
Normal file
@ -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<Value> for u64 {
|
||||
fn from(value: Value) -> Self {
|
||||
value.as_64()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Value {
|
||||
type Err = ValueError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(num) = s.parse::<u64>() {
|
||||
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<u64, ValueError> {
|
||||
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<u64, ValueError> {
|
||||
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<u64, ValueError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
|
||||
@ -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<u64> for Key {
|
||||
fn from(key: u64) -> Self {
|
||||
Self(key.to_be_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Key> for [u8; 8] {
|
||||
fn from(key: Key) -> Self {
|
||||
key.0
|
||||
@ -43,12 +58,6 @@ impl AsRef<[u8]> for Key {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> 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])")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user