use crate::core::{Error, Result};
pub struct SafeHex;
impl SafeHex {
pub fn is_hex_char(c: char) -> bool {
c.is_ascii_hexdigit()
}
pub fn hex_char_to_nibble(c: char) -> Option<u8> {
match c {
'0'..='9' => Some(c as u8 - b'0'),
'a'..='f' => Some(c as u8 - b'a' + 10),
'A'..='F' => Some(c as u8 - b'A' + 10),
_ => None,
}
}
pub fn nibble_to_hex_char(n: u8) -> char {
if n < 10 {
(b'0' + n) as char
} else {
(b'a' + n - 10) as char
}
}
pub fn nibble_to_hex_char_upper(n: u8) -> char {
if n < 10 {
(b'0' + n) as char
} else {
(b'A' + n - 10) as char
}
}
pub fn encode(bytes: &[u8]) -> String {
let mut result = String::with_capacity(bytes.len() * 2);
for &b in bytes {
result.push(Self::nibble_to_hex_char(b >> 4));
result.push(Self::nibble_to_hex_char(b & 0x0F));
}
result
}
pub fn encode_upper(bytes: &[u8]) -> String {
let mut result = String::with_capacity(bytes.len() * 2);
for &b in bytes {
result.push(Self::nibble_to_hex_char_upper(b >> 4));
result.push(Self::nibble_to_hex_char_upper(b & 0x0F));
}
result
}
pub fn decode(hex: &str) -> Result<Vec<u8>> {
if hex.len() % 2 != 0 {
return Err(Error::InvalidFormat("Hex string has odd length".into()));
}
let mut result = Vec::with_capacity(hex.len() / 2);
let chars: Vec<char> = hex.chars().collect();
for i in (0..chars.len()).step_by(2) {
let high = Self::hex_char_to_nibble(chars[i])
.ok_or_else(|| Error::InvalidFormat("Invalid hex character".into()))?;
let low = Self::hex_char_to_nibble(chars[i + 1])
.ok_or_else(|| Error::InvalidFormat("Invalid hex character".into()))?;
result.push((high << 4) | low);
}
Ok(result)
}
pub fn is_valid(s: &str) -> bool {
s.chars().all(Self::is_hex_char)
}
pub fn is_valid_bytes(s: &str) -> bool {
s.len() % 2 == 0 && Self::is_valid(s)
}
pub fn format_spaced(hex: &str) -> Result<String> {
if hex.len() % 2 != 0 {
return Err(Error::InvalidFormat("Hex string has odd length".into()));
}
if hex.is_empty() {
return Ok(String::new());
}
let chars: Vec<char> = hex.chars().collect();
let mut result = String::with_capacity(hex.len() + hex.len() / 2 - 1);
for (i, chunk) in chars.chunks(2).enumerate() {
if i > 0 {
result.push(' ');
}
result.push(chunk[0]);
result.push(chunk[1]);
}
Ok(result)
}
pub fn format_colons(hex: &str) -> Result<String> {
if hex.len() % 2 != 0 {
return Err(Error::InvalidFormat("Hex string has odd length".into()));
}
if hex.is_empty() {
return Ok(String::new());
}
let chars: Vec<char> = hex.chars().collect();
let mut result = String::with_capacity(hex.len() + hex.len() / 2 - 1);
for (i, chunk) in chars.chunks(2).enumerate() {
if i > 0 {
result.push(':');
}
result.push(chunk[0]);
result.push(chunk[1]);
}
Ok(result)
}
pub fn constant_time_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let a_lower: Vec<char> = a.to_lowercase().chars().collect();
let b_lower: Vec<char> = b.to_lowercase().chars().collect();
let mut diff = 0u8;
for (ca, cb) in a_lower.iter().zip(b_lower.iter()) {
diff |= (*ca as u8) ^ (*cb as u8);
}
diff == 0
}
pub fn int_to_hex(value: u64, min_width: usize) -> String {
let hex = format!("{:x}", value);
if hex.len() >= min_width {
hex
} else {
format!("{:0>width$}", hex, width = min_width)
}
}
pub fn hex_to_int(hex: &str) -> Result<u64> {
u64::from_str_radix(hex, 16)
.map_err(|_| Error::InvalidFormat("Invalid hex number".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode() {
assert_eq!(SafeHex::encode(&[0xFF, 0x00, 0xAB]), "ff00ab");
assert_eq!(SafeHex::encode_upper(&[0xFF, 0x00, 0xAB]), "FF00AB");
}
#[test]
fn test_decode() {
assert_eq!(SafeHex::decode("ff00ab").unwrap(), vec![0xFF, 0x00, 0xAB]);
}
#[test]
fn test_validation() {
assert!(SafeHex::is_valid("abcdef0123456789"));
assert!(!SafeHex::is_valid("xyz"));
assert!(SafeHex::is_valid_bytes("aabb"));
assert!(!SafeHex::is_valid_bytes("aab"));
}
#[test]
fn test_format_spaced() {
assert_eq!(SafeHex::format_spaced("aabbcc").unwrap(), "aa bb cc");
}
#[test]
fn test_constant_time_eq() {
assert!(SafeHex::constant_time_eq("aabb", "AABB"));
assert!(!SafeHex::constant_time_eq("aabb", "aab0"));
}
#[test]
fn test_int_to_hex() {
assert_eq!(SafeHex::int_to_hex(255, 2), "ff");
assert_eq!(SafeHex::int_to_hex(255, 4), "00ff");
}
}