refactor(cipher-core): extract shared block parsing logic

Add generic BlockInt trait and parse_block_int<T>() 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
This commit is contained in:
Kristofers Solo 2025-12-30 23:55:52 +02:00
parent 656e112d9f
commit 451986d702
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
5 changed files with 120 additions and 112 deletions

View File

@ -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<Self, Self::Err> {
Ok(Self(parse_string_to_u128(s)?))
Ok(Self(parse_block_int(s)?))
}
}
fn parse_string_to_u128(s: &str) -> Result<u128, BlockError> {
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<u128, BlockError> {
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<u128, BlockError> {
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)

View File

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

View File

@ -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},
};

112
cipher-core/src/parsing.rs Normal file
View File

@ -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<Self, ParseIntError>;
/// 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, ParseIntError> {
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, ParseIntError> {
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<T: BlockInt>(s: &str) -> Result<T, BlockError> {
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::<T>(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::<T>(bin_str, 2);
}
parse_ascii::<T>(trimmed)
}
fn parse_radix<T: BlockInt>(s: &str, radix: u32) -> Result<T, BlockError> {
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<T: BlockInt>(s: &str) -> Result<T, BlockError> {
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::<T>(),
"String contains non-ASCII characters",
));
}
Ok(T::from_be_bytes_padded(s.as_bytes()))
}

View File

@ -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<Self, Self::Err> {
Ok(Self(parse_string_to_u64(s)?))
Ok(Self(parse_block_int(s)?))
}
}
fn parse_string_to_u64(s: &str) -> Result<u64, BlockError> {
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<u64, BlockError> {
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<u64, BlockError> {
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)