feat: add cli app

This commit is contained in:
Kristofers Solo 2025-10-06 11:49:27 +03:00
parent 20ff162d71
commit 93696b5d68
Signed by: kristoferssolo
GPG Key ID: 74FF8144483D82C8
7 changed files with 485 additions and 13 deletions

235
Cargo.lock generated
View File

@ -11,6 +11,56 @@ dependencies = [
"memchr", "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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.3" version = "1.0.3"
@ -23,6 +73,61 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" 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]] [[package]]
name = "des-lib" name = "des-lib"
version = "0.1.0" version = "0.1.0"
@ -105,6 +210,12 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.11.4" version = "2.11.4"
@ -115,6 +226,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.176" version = "0.2.176"
@ -127,6 +244,12 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -315,6 +438,12 @@ version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.106" version = "2.0.106"
@ -326,6 +455,26 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.2" version = "0.7.2"
@ -362,6 +511,12 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.7+wasi-0.2.4" version = "0.14.7+wasi-0.2.4"
@ -380,6 +535,86 @@ dependencies = [
"wit-bindgen", "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]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.13" version = "0.7.13"

View File

@ -1,11 +1,15 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["des-lib"] members = ["des", "des-lib"]
[workspace.dependencies] [workspace.dependencies]
des-lib = { path = "des-lib" }
claims = "0.8" claims = "0.8"
clap = { version = "4.5", features = ["derive"] }
rand = "0.9" rand = "0.9"
rstest = "0.26" rstest = "0.26"
thiserror = "2"
[workspace.lints.clippy] [workspace.lints.clippy]
pedantic = "warn" pedantic = "warn"

View File

@ -10,18 +10,6 @@ fn des_instance() -> Des {
Des::new(TEST_KEY) 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] #[rstest]
#[case(TEST_PLAINTEXT, TEST_CIPHERTEXT, TEST_KEY)] #[case(TEST_PLAINTEXT, TEST_CIPHERTEXT, TEST_KEY)]
fn encrypt_decrypt_roundtrip( fn encrypt_decrypt_roundtrip(

15
des/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "des"
version = "0.1.0"
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
edition = "2024"
[dependencies]
clap.workspace = true
des-lib.workspace = true
thiserror.workspace = true
[dev-dependencies]
[lints]
workspace = true

208
des/src/args.rs Normal file
View File

@ -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<OutputFormat>,
},
}
#[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<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 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::<u64>().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<u64, ValueError> {
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)
}
}

21
des/src/main.rs Normal file
View File

@ -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}");
}
}
}

1
key Normal file
View File

@ -0,0 +1 @@
0x85E813540F0AB405