From 451986d7021984aeb690e6f232e0107ebd17fa8d Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Tue, 30 Dec 2025 23:55:52 +0200 Subject: [PATCH] refactor(cipher-core): extract shared block parsing logic Add generic BlockInt trait and parse_block_int() function to cipher-core, eliminating duplicate parsing code in aes and des crates. - BlockInt trait abstracts over u64/u128 integer types - Supports hex (0x), binary (0b), and ASCII string formats - Improved BlockError::InvalidByteStringLength with max/actual fields --- aes/src/block/block128.rs | 57 +------------------ cipher-core/src/error.rs | 4 +- cipher-core/src/lib.rs | 2 + cipher-core/src/parsing.rs | 112 +++++++++++++++++++++++++++++++++++++ des/src/block/block64.rs | 57 +------------------ 5 files changed, 120 insertions(+), 112 deletions(-) create mode 100644 cipher-core/src/parsing.rs diff --git a/aes/src/block/block128.rs b/aes/src/block/block128.rs index eee3c9f..3a43fd2 100644 --- a/aes/src/block/block128.rs +++ b/aes/src/block/block128.rs @@ -2,7 +2,7 @@ use crate::{ block::{Block32, secret_block}, sbox::SboxLookup, }; -use cipher_core::{BlockError, InputBlock}; +use cipher_core::{parse_block_int, BlockError, InputBlock}; use std::{ ops::BitXor, slice::{from_raw_parts, from_raw_parts_mut}, @@ -68,63 +68,10 @@ impl Block128 { impl FromStr for Block128 { type Err = BlockError; fn from_str(s: &str) -> Result { - Ok(Self(parse_string_to_u128(s)?)) + Ok(Self(parse_block_int(s)?)) } } -fn parse_string_to_u128(s: &str) -> Result { - let trimmed = s.trim(); - - if trimmed.is_empty() { - return Err(BlockError::EmptyBlock); - } - - // 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); - } - // 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); - } - - ascii_string_to_u128(trimmed) -} - -fn parse_radix(s: &str, radix: u32) -> Result { - let trimmed = s.trim_start_matches('0'); - if trimmed.is_empty() { - return Ok(0); - } - - u128::from_str_radix(trimmed, radix).map_err(BlockError::from) -} - -fn ascii_string_to_u128(s: &str) -> Result { - if s.len() > 16 { - return Err(BlockError::InvalidByteStringLength(s.len())); - } - - if !s.is_ascii() { - return Err(BlockError::conversion_error( - "u64", - "String contains non-ASCII characters", - )); - } - - let mut bytes = [0u8; 16]; - let offset = 16 - s.len(); - bytes[offset..].copy_from_slice(s.as_bytes()); - - Ok(u128::from_be_bytes(bytes)) -} - impl From<[u8; 16]> for Block128 { fn from(bytes: [u8; 16]) -> Self { Self::from_be_bytes(bytes) diff --git a/cipher-core/src/error.rs b/cipher-core/src/error.rs index 84e8031..8595c8e 100644 --- a/cipher-core/src/error.rs +++ b/cipher-core/src/error.rs @@ -44,8 +44,8 @@ pub enum BlockError { ParseError(#[from] ParseIntError), /// Byte size length - #[error("Invalid byte string length: expected no more than 8, found {0}")] - InvalidByteStringLength(usize), + #[error("Invalid byte string length: expected no more than {max}, found {actual}")] + InvalidByteStringLength { max: usize, actual: usize }, /// String to int conversion error #[error("String-to-{typ} conversion error: {err}")] diff --git a/cipher-core/src/lib.rs b/cipher-core/src/lib.rs index c17713a..d04f1dc 100644 --- a/cipher-core/src/lib.rs +++ b/cipher-core/src/lib.rs @@ -1,9 +1,11 @@ mod error; +mod parsing; mod traits; mod types; pub use { error::{BlockError, CipherError, CipherResult}, + parsing::{parse_block_int, BlockInt}, traits::{BlockCipher, BlockParser, InputBlock}, types::{CipherAction, Output}, }; diff --git a/cipher-core/src/parsing.rs b/cipher-core/src/parsing.rs new file mode 100644 index 0000000..4693af2 --- /dev/null +++ b/cipher-core/src/parsing.rs @@ -0,0 +1,112 @@ +use crate::BlockError; +use std::{any::type_name, num::ParseIntError}; + +/// Trait for integer types that can be parsed from block string formats. +/// +/// Implemented for `u64` and `u128` to support DES (64-bit) and AES (128-bit) block parsing. +pub trait BlockInt: Sized + Copy { + /// Number of bytes this integer type represents. + const BYTE_SIZE: usize; + + /// Parse from string with given radix. + /// + /// # Errors + /// Returns `ParseIntError` if the string contains invalid digits for the radix. + fn from_str_radix(s: &str, radix: u32) -> Result; + + /// Construct from big-endian bytes (zero-padded on the left). + fn from_be_bytes_padded(bytes: &[u8]) -> Self; +} + +impl BlockInt for u64 { + const BYTE_SIZE: usize = 8; + + fn from_str_radix(s: &str, radix: u32) -> Result { + Self::from_str_radix(s, radix) + } + + fn from_be_bytes_padded(bytes: &[u8]) -> Self { + let mut arr = [0u8; 8]; + let offset = 8 - bytes.len(); + arr[offset..].copy_from_slice(bytes); + Self::from_be_bytes(arr) + } +} + +impl BlockInt for u128 { + const BYTE_SIZE: usize = 16; + + fn from_str_radix(s: &str, radix: u32) -> Result { + Self::from_str_radix(s, radix) + } + + fn from_be_bytes_padded(bytes: &[u8]) -> Self { + let mut arr = [0u8; 16]; + let offset = 16 - bytes.len(); + arr[offset..].copy_from_slice(bytes); + Self::from_be_bytes(arr) + } +} + +/// Parse a string into a block integer, supporting hex (0x), binary (0b), and ASCII formats. +/// +/// # Formats +/// - `0x...` or `0X...`: Hexadecimal +/// - `0b...` or `0B...`: Binary +/// - Otherwise: ASCII string (right-aligned, zero-padded) +/// +/// # Errors +/// Returns `BlockError` if the string is empty, contains invalid characters, +/// or exceeds the maximum byte length for the target type. +pub fn parse_block_int(s: &str) -> Result { + let trimmed = s.trim(); + + if trimmed.is_empty() { + return Err(BlockError::EmptyBlock); + } + + // 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); + } + + // 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); + } + + parse_ascii::(trimmed) +} + +fn parse_radix(s: &str, radix: u32) -> Result { + let trimmed = s.trim_start_matches('0'); + if trimmed.is_empty() { + return Ok(T::from_be_bytes_padded(&[])); + } + + T::from_str_radix(trimmed, radix).map_err(BlockError::from) +} + +fn parse_ascii(s: &str) -> Result { + if s.len() > T::BYTE_SIZE { + return Err(BlockError::InvalidByteStringLength { + max: T::BYTE_SIZE, + actual: s.len(), + }); + } + + if !s.is_ascii() { + return Err(BlockError::conversion_error( + type_name::(), + "String contains non-ASCII characters", + )); + } + + Ok(T::from_be_bytes_padded(s.as_bytes())) +} diff --git a/des/src/block/block64.rs b/des/src/block/block64.rs index 0744cad..5bfedf8 100644 --- a/des/src/block/block64.rs +++ b/des/src/block/block64.rs @@ -1,5 +1,5 @@ use crate::block::{lr::LR, secret_block}; -use cipher_core::{BlockError, InputBlock}; +use cipher_core::{parse_block_int, BlockError, InputBlock}; use std::{ slice::{from_raw_parts, from_raw_parts_mut}, str::FromStr, @@ -48,63 +48,10 @@ impl Block64 { impl FromStr for Block64 { type Err = BlockError; fn from_str(s: &str) -> Result { - Ok(Self(parse_string_to_u64(s)?)) + Ok(Self(parse_block_int(s)?)) } } -fn parse_string_to_u64(s: &str) -> Result { - let trimmed = s.trim(); - - if trimmed.is_empty() { - return Err(BlockError::EmptyBlock); - } - - // 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); - } - // 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); - } - - ascii_string_to_u64(trimmed) -} - -fn parse_radix(s: &str, radix: u32) -> Result { - let trimmed = s.trim_start_matches('0'); - if trimmed.is_empty() { - return Ok(0); - } - - u64::from_str_radix(trimmed, radix).map_err(BlockError::from) -} - -fn ascii_string_to_u64(s: &str) -> Result { - if s.len() > 8 { - return Err(BlockError::InvalidByteStringLength(s.len())); - } - - if !s.is_ascii() { - return Err(BlockError::conversion_error( - "u64", - "String contains non-ASCII characters", - )); - } - - let mut bytes = [0u8; 8]; - let offset = 8 - s.len(); - bytes[offset..].copy_from_slice(s.as_bytes()); - - Ok(u64::from_be_bytes(bytes)) -} - impl From<[u8; 8]> for Block64 { fn from(bytes: [u8; 8]) -> Self { Self::from_be_bytes(bytes)