From 93696b5d68cf13648dfc61599c907ef60f6a326b Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 6 Oct 2025 11:49:27 +0300 Subject: [PATCH] feat: add cli app --- Cargo.lock | 235 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 +- des-lib/tests/des.rs | 12 --- des/Cargo.toml | 15 +++ des/src/args.rs | 208 ++++++++++++++++++++++++++++++++++++++ des/src/main.rs | 21 ++++ key | 1 + 7 files changed, 485 insertions(+), 13 deletions(-) create mode 100644 des/Cargo.toml create mode 100644 des/src/args.rs create mode 100644 des/src/main.rs create mode 100644 key diff --git a/Cargo.lock b/Cargo.lock index 9ae8af3..da80ba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -23,6 +73,61 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "des" +version = "0.1.0" +dependencies = [ + "clap", + "des-lib", + "thiserror", +] + [[package]] name = "des-lib" version = "0.1.0" @@ -105,6 +210,12 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indexmap" version = "2.11.4" @@ -115,6 +226,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "libc" version = "0.2.176" @@ -127,6 +244,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -315,6 +438,12 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.106" @@ -326,6 +455,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml_datetime" version = "0.7.2" @@ -362,6 +511,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasi" version = "0.14.7+wasi-0.2.4" @@ -380,6 +535,86 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.13" diff --git a/Cargo.toml b/Cargo.toml index 40caec1..993b2df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,15 @@ [workspace] resolver = "2" -members = ["des-lib"] +members = ["des", "des-lib"] [workspace.dependencies] +des-lib = { path = "des-lib" } + claims = "0.8" +clap = { version = "4.5", features = ["derive"] } rand = "0.9" rstest = "0.26" +thiserror = "2" [workspace.lints.clippy] pedantic = "warn" diff --git a/des-lib/tests/des.rs b/des-lib/tests/des.rs index 9d4536e..d26839b 100644 --- a/des-lib/tests/des.rs +++ b/des-lib/tests/des.rs @@ -10,18 +10,6 @@ fn des_instance() -> Des { Des::new(TEST_KEY) } -#[test] -fn test_ecb_mode_equivalence() { - // If you implement ECB mode, test it matches single block - let key = 0x1334_5779_9BBC_DFF1; - let des = Des::new(key); - let plain = 0x0123_4567_89AB_CDEF; - - let _single_block = des.encrypt(plain); - // let ecb_result = encrypt_ecb(&[plain]); - // assert_eq!(single_block, ecb_result[0]); -} - #[rstest] #[case(TEST_PLAINTEXT, TEST_CIPHERTEXT, TEST_KEY)] fn encrypt_decrypt_roundtrip( diff --git a/des/Cargo.toml b/des/Cargo.toml new file mode 100644 index 0000000..20b9a00 --- /dev/null +++ b/des/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "des" +version = "0.1.0" +authors = ["Kristofers Solo "] +edition = "2024" + +[dependencies] +clap.workspace = true +des-lib.workspace = true +thiserror.workspace = true + +[dev-dependencies] + +[lints] +workspace = true diff --git a/des/src/args.rs b/des/src/args.rs new file mode 100644 index 0000000..e5118fc --- /dev/null +++ b/des/src/args.rs @@ -0,0 +1,208 @@ +use clap::{Parser, Subcommand, ValueEnum}; +use std::{ + fmt::{Display, LowerHex, UpperHex}, + fs::read_to_string, + num::IntErrorKind, + 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: must be exactly 8 ASCII characters")] + InvalidByteString, + + #[error("String-to-u64 conversion error: {0}")] + ConversionError(String), +} + +#[derive(Debug, Clone, Parser)] +#[command(version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + pub operation: Operation, + + /// Key used to encrypt/decrypt data (64-bit number, string, or path to file) + #[arg(short = 'k', long, value_parser = Value::from_str, required = true)] + pub 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)] + pub text: Value, +} + +#[derive(Debug, Clone, Subcommand, Default)] +pub enum Operation { + /// Encrypt data + #[default] + Encrypt, + /// Decrypt data + Decrypt { + /// Output format for decrypted data + #[arg(short = 'f', long, value_enum)] + output_format: Option, + }, +} + +#[derive(Debug, Clone, Default, ValueEnum)] +pub enum OutputFormat { + /// Binary output + Binary, + /// Octal output (fixed typo) + Octal, + /// Decimal output + Decimal, + /// Hexadecimal output + #[default] + Hex, + /// Text output (ASCII) + Text, +} + +#[derive(Debug, Clone, Copy)] +pub struct Value(u64); + +impl Value { + #[inline] + #[must_use] + pub fn as_64(self) -> u64 { + self.0 + } +} + +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 trimmed.starts_with("0x") || trimmed.starts_with("0X") { + let hex_str = &trimmed[2..].trim_start_matches('0'); + if hex_str.is_empty() { + return Ok(0); // 0x000 -> + } + return u64::from_str_radix(hex_str, 16) + .map_err(|e| ValueError::InvalidFormat(format!("Hex parsing failed: {e}"))); + } + + // Binary with 0b/0B prefix + if trimmed.starts_with("0b") || trimmed.starts_with("0B") { + let bin_str = &trimmed[2..].trim_start_matches('0'); + if bin_str.is_empty() { + return Ok(0); // 0b000 -> 0 + } + if !bin_str.chars().all(|ch| ch == '0' || ch == '1') { + return Err(ValueError::InvalidFormat( + "Binary string contains invalid characters".into(), + )); + } + return u64::from_str_radix(bin_str, 2) + .map_err(|e| ValueError::InvalidFormat(format!("Binary parsing failed: {e}"))); + } + + // 8-character ASCII string conversion to u64 + if trimmed.len() == 8 { + return ascii_string_to_u64(trimmed); + } + + // Regular decimal parsing + trimmed.parse::().map_err(|e| { + ValueError::InvalidFormat(match e.kind() { + IntErrorKind::InvalidDigit => "contains invalid digits".into(), + IntErrorKind::PosOverflow => "number too large for u64".into(), + IntErrorKind::NegOverflow => "negative numbers not allowed".into(), + IntErrorKind::Empty => "empty string".into(), + IntErrorKind::Zero => "invalid zero".into(), + _ => format!("parsing error: {e}"), + }) + }) +} + +fn ascii_string_to_u64(s: &str) -> Result { + if s.len() != 8 { + return Err(ValueError::InvalidByteString); + } + + // Ensure all characters are valid ASCII (0-127) + if !s.bytes().all(|b| b <= 127) { + 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_le_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/main.rs b/des/src/main.rs new file mode 100644 index 0000000..61705ab --- /dev/null +++ b/des/src/main.rs @@ -0,0 +1,21 @@ +mod args; + +use crate::args::{Args, Operation}; +use clap::Parser; +use des_lib::Des; + +fn main() { + let args = Args::parse(); + let des = Des::new(args.key.as_64()); + + match args.operation { + Operation::Encrypt => { + let ciphertext = des.encrypt(args.text.as_64()); + println!("{ciphertext:016X}"); + } + Operation::Decrypt { output_format } => { + let plaintext = des.decrypt(args.text.as_64()); + println!("{plaintext:016X}"); + } + } +} diff --git a/key b/key new file mode 100644 index 0000000..883d322 --- /dev/null +++ b/key @@ -0,0 +1 @@ +0x85E813540F0AB405