use std::error::Error;
use std::fmt;
const LOWER: &[u8; 16] = b"0123456789abcdef";
pub fn encode(bytes: impl AsRef<[u8]>) -> String {
let bytes = bytes.as_ref();
let mut out = String::with_capacity(bytes.len().saturating_mul(2));
for &byte in bytes {
out.push(char::from(LOWER[usize::from(byte >> 4)]));
out.push(char::from(LOWER[usize::from(byte & 0x0f)]));
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecodeError {
OddLength {
len: usize,
at: usize,
},
NotHexDigit {
at: usize,
byte: u8,
},
OutputLength {
expected: usize,
actual: usize,
},
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::OddLength { len, .. } => {
write!(
f,
"hex input has odd length {len}; every byte needs two characters"
)
}
Self::NotHexDigit { at, byte } => {
write!(f, "byte {byte:#04x} at offset {at} is not a hex digit")
}
Self::OutputLength { expected, actual } => write!(
f,
"hex output has length {actual}; exactly {expected} bytes are required"
),
}
}
}
impl Error for DecodeError {}
fn check_even_length(len: usize) -> Result<(), DecodeError> {
if !len.is_multiple_of(2) {
Err(DecodeError::OddLength { len, at: len })
} else {
Ok(())
}
}
fn decode_pair(pair: &[u8], offset: usize) -> Result<u8, DecodeError> {
let hi = decode_nibble(pair[0]).ok_or(DecodeError::NotHexDigit {
at: offset,
byte: pair[0],
})?;
let lo = decode_nibble(pair[1]).ok_or(DecodeError::NotHexDigit {
at: offset.saturating_add(1),
byte: pair[1],
})?;
Ok((hi << 4) | lo)
}
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
let input = input.as_ref();
check_even_length(input.len())?;
let pair_count = input.len().checked_div(2).unwrap_or(0);
let mut out = Vec::with_capacity(pair_count);
let (pairs, _) = input.as_chunks::<2>();
for (pair_index, pair) in pairs.iter().enumerate() {
let offset = pair_index.saturating_mul(2);
out.push(decode_pair(pair, offset)?);
}
Ok(out)
}
pub fn decode_into(input: impl AsRef<[u8]>, output: &mut [u8]) -> Result<(), DecodeError> {
let input = input.as_ref();
check_even_length(input.len())?;
let expected = input.len().checked_div(2).unwrap_or(0);
if output.len() != expected {
let refusal = Err(DecodeError::OutputLength {
expected,
actual: output.len(),
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "decode_into: returning an error to the caller");
return refusal;
}
let (pairs, _) = input.as_chunks::<2>();
for (pair_index, pair) in pairs.iter().enumerate() {
let offset = pair_index.saturating_mul(2);
decode_pair(pair, offset)?;
}
for (pair_index, (pair, destination)) in pairs.iter().zip(output.iter_mut()).enumerate() {
let offset = pair_index.saturating_mul(2);
*destination = decode_pair(pair, offset)?;
}
Ok(())
}
#[cfg(any(feature = "random", feature = "hash"))]
pub(crate) fn write_lowercase(bytes: &[u8], output: &mut impl fmt::Write) -> fmt::Result {
for &byte in bytes {
output.write_char(char::from(LOWER[usize::from(byte >> 4)]))?;
output.write_char(char::from(LOWER[usize::from(byte & 0x0f)]))?;
}
Ok(())
}
pub(crate) fn decode_nibble(ascii_byte: u8) -> Option<u8> {
match ascii_byte {
b'0'..=b'9' => Some(ascii_byte.saturating_sub(b'0')),
b'a'..=b'f' => Some(ascii_byte.saturating_sub(b'a').saturating_add(10)),
b'A'..=b'F' => Some(ascii_byte.saturating_sub(b'A').saturating_add(10)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_emits_two_lowercase_characters_per_byte() {
assert_eq!(encode([]), "");
assert_eq!(encode([0x00]), "00");
assert_eq!(encode([0x0f]), "0f");
assert_eq!(encode([0xf0]), "f0");
assert_eq!(encode([0xff]), "ff");
assert_eq!(encode(b"hello"), "68656c6c6f");
}
#[test]
fn decode_accepts_uppercase_digits() -> Result<(), DecodeError> {
assert_eq!(decode("48454C4C4F")?, b"HELLO");
assert_eq!(decode("48454c4c6f")?, b"HELLo");
Ok(())
}
#[test]
fn decode_refuses_odd_length() {
assert_eq!(decode("abc"), Err(DecodeError::OddLength { len: 3, at: 3 }));
assert_eq!(decode("a"), Err(DecodeError::OddLength { len: 1, at: 1 }));
}
#[test]
fn decode_names_the_offending_offset() {
assert_eq!(
decode("00zz"),
Err(DecodeError::NotHexDigit { at: 2, byte: b'z' })
);
assert_eq!(
decode("000z"),
Err(DecodeError::NotHexDigit { at: 3, byte: b'z' })
);
assert_eq!(
decode("g0"),
Err(DecodeError::NotHexDigit { at: 0, byte: b'g' })
);
}
#[test]
fn decode_into_validates_exact_length_and_preserves_output_on_failure() {
let mut short = [0xa5; 1];
assert_eq!(
decode_into("aabb", &mut short),
Err(DecodeError::OutputLength {
expected: 2,
actual: 1
}),
"short destination is refused"
);
assert_eq!(short, [0xa5], "short destination remains untouched");
let mut long = [0xa5; 3];
assert_eq!(
decode_into("aabb", &mut long),
Err(DecodeError::OutputLength {
expected: 2,
actual: 3
}),
"long destination is refused"
);
assert_eq!(long, [0xa5; 3], "long destination remains untouched");
let mut invalid = [0xa5; 2];
assert_eq!(
decode_into("aabz", &mut invalid),
Err(DecodeError::NotHexDigit { at: 3, byte: b'z' }),
"invalid digit identifies its source byte"
);
assert_eq!(invalid, [0xa5; 2], "invalid input writes no prefix");
assert_eq!(decode_into("", &mut []), Ok(()), "empty input decodes");
assert_eq!(
decode_into("a", &mut []),
Err(DecodeError::OddLength { len: 1, at: 1 }),
"odd input is rejected before destination validation"
);
let mut uppercase = [0; 2];
assert_eq!(decode_into("Af09", &mut uppercase), Ok(()));
assert_eq!(uppercase, [0xaf, 0x09], "uppercase input is preserved");
}
#[test]
fn roundtrip_holds_for_every_single_byte() -> Result<(), DecodeError> {
for byte_value in 0u8..=255 {
let encoded = encode([byte_value]);
let decoded = decode(&encoded)?;
assert_eq!(decoded, vec![byte_value]);
}
Ok(())
}
#[test]
fn matches_the_braid_cid_encoding() -> Result<(), DecodeError> {
let sample = [
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
0x32, 0x10, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44,
0x55, 0x66, 0x77, 0x88,
];
let expected: String = sample
.iter()
.map(|byte_value| format!("{byte_value:02x}"))
.collect();
assert_eq!(encode(sample), expected);
assert_eq!(decode(&expected)?, sample);
Ok(())
}
}