use anyhow::{Context, Result, bail};
pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>> {
if !hex.is_ascii() {
bail!("non-ASCII hex string");
}
if !hex.len().is_multiple_of(2) {
bail!("odd-length hex string");
}
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).context("invalid hex digit"))
.collect()
}
pub fn parse_poseidon_hash(hex: &str) -> Result<hemera::Hash> {
let bytes = hex_to_bytes(hex).context("invalid hex hash")?;
if bytes.len() != 32 {
bail!("hash must be 32 bytes (64 hex chars), got {} bytes", bytes.len());
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(hemera::Hash::from_bytes(arr))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_to_bytes_round_trips_lowercase() {
assert_eq!(hex_to_bytes("00ff10").unwrap(), vec![0x00, 0xff, 0x10]);
}
#[test]
fn hex_to_bytes_rejects_odd_length() {
assert!(hex_to_bytes("abc").is_err());
}
#[test]
fn hex_to_bytes_rejects_non_hex_digits() {
assert!(hex_to_bytes("zz").is_err());
}
#[test]
fn hex_to_bytes_rejects_non_ascii_without_panicking() {
let s = format!("{}รฉ{}", "a".repeat(31), "a");
assert_eq!(s.len(), 34);
assert!(hex_to_bytes(&s).is_err());
}
#[test]
fn hex_to_bytes_empty_is_empty() {
assert_eq!(hex_to_bytes("").unwrap(), Vec::<u8>::new());
}
#[test]
fn parse_poseidon_hash_round_trips_through_display() {
let h = hemera::hash(b"radio-cli lib target");
let parsed = parse_poseidon_hash(&h.to_string()).unwrap();
assert_eq!(parsed, h);
}
#[test]
fn parse_poseidon_hash_rejects_wrong_length() {
assert!(parse_poseidon_hash("ab").is_err());
}
#[test]
fn parse_poseidon_hash_rejects_bad_hex() {
assert!(parse_poseidon_hash(&"z".repeat(64)).is_err());
}
}