use qubit_codec::ValueDecoder;
use qubit_codec::ValueEncoder;
use crate::MiscCodecError;
use crate::MiscCodecResult;
use crate::hex_codec_config::HexCodecConfig;
const LOWER_HEX_DIGITS: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
const UPPER_HEX_DIGITS: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
];
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HexCodec {
config: HexCodecConfig,
}
impl HexCodec {
#[inline]
pub fn new() -> Self {
Self::from_config(HexCodecConfig::new())
}
#[inline]
pub fn upper() -> Self {
Self::from_config(HexCodecConfig::new().with_uppercase(true))
}
#[inline]
pub fn from_config(config: HexCodecConfig) -> Self {
Self { config }
}
#[must_use = "inspect the configuration or retain the reference for later use"]
#[inline]
pub fn config(&self) -> &HexCodecConfig {
&self.config
}
#[inline]
pub fn with_uppercase(mut self, uppercase: bool) -> Self {
self.config = self.config.with_uppercase(uppercase);
self
}
#[inline]
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.config = self.config.with_prefix(prefix);
self
}
#[inline]
pub fn with_byte_prefix(mut self, prefix: impl Into<String>) -> Self {
self.config = self.config.with_byte_prefix(prefix);
self
}
#[inline]
pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
self.config = self.config.with_separator(separator);
self
}
#[inline]
pub fn with_ignored_ascii_whitespace(mut self, ignore: bool) -> Self {
self.config = self.config.with_ignored_ascii_whitespace(ignore);
self
}
#[inline]
pub fn with_ignore_prefix_case(mut self, ignore: bool) -> Self {
self.config = self.config.with_ignore_prefix_case(ignore);
self
}
#[inline]
#[must_use]
pub fn encode(&self, bytes: &[u8]) -> String {
let separator_len = self.config.separator.as_ref().map_or(0, String::len);
let prefix_len = self.config.prefix.as_ref().map_or(0, String::len);
let byte_prefix_len = self.config.byte_prefix.as_ref().map_or(0, String::len);
let capacity = prefix_len.saturating_add(
bytes
.len()
.saturating_mul(byte_prefix_len.saturating_add(2))
.saturating_add(bytes.len().saturating_sub(1).saturating_mul(separator_len)),
);
let mut output = String::with_capacity(capacity);
self.encode_into(bytes, &mut output);
output
}
#[inline]
pub fn encode_into(&self, bytes: &[u8], output: &mut String) {
if let Some(prefix) = &self.config.prefix {
output.push_str(prefix);
}
for (index, byte) in bytes.iter().enumerate() {
if index > 0
&& let Some(separator) = &self.config.separator
{
output.push_str(separator);
}
if let Some(byte_prefix) = &self.config.byte_prefix {
output.push_str(byte_prefix);
}
push_hex_byte(*byte, self.config.uppercase, output);
}
}
#[inline]
pub fn decode(&self, text: &str) -> MiscCodecResult<Vec<u8>> {
let mut output = Vec::new();
self.decode_into(text, &mut output)?;
Ok(output)
}
#[inline]
pub fn decode_into(&self, text: &str, output: &mut Vec<u8>) -> MiscCodecResult<()> {
let original_len = output.len();
if let Err(error) = self.decode_append(text, output) {
output.truncate(original_len);
return Err(error);
}
Ok(())
}
#[inline]
fn decode_append(&self, text: &str, output: &mut Vec<u8>) -> MiscCodecResult<()> {
let start_index = self.consume_prefix(text)?;
let is_plain = !self.config.ignore_ascii_whitespace
&& self.config.byte_prefix.as_deref().is_none_or(str::is_empty)
&& self.config.separator.as_deref().is_none_or(str::is_empty);
if is_plain {
output.reserve(text.len().saturating_sub(start_index) / 2);
}
if let Some(separator) = self
.config
.separator
.as_deref()
.filter(|separator| !separator.is_empty())
{
return self.decode_separated(text, start_index, separator, output);
}
if let Some(byte_prefix) = self.config.byte_prefix.as_deref().filter(|prefix| !prefix.is_empty()) {
return self.decode_byte_prefixed(text, byte_prefix, start_index, output);
}
self.decode_unprefixed(text, start_index, output)
}
#[inline]
fn consume_prefix(&self, text: &str) -> MiscCodecResult<usize> {
let Some(prefix) = self.config.prefix.as_deref().filter(|prefix| !prefix.is_empty()) else {
return Ok(0);
};
let mut index = 0;
loop {
if self.starts_with_prefix(&text[index..], prefix) {
return Ok(index + prefix.len());
}
if !self.config.ignore_ascii_whitespace
|| index == text.len()
|| !text.as_bytes()[index].is_ascii_whitespace()
{
return Err(MiscCodecError::MissingPrefix {
prefix: prefix.to_owned(),
});
}
index += 1;
}
}
fn decode_separated(
&self,
text: &str,
mut index: usize,
separator: &str,
output: &mut Vec<u8>,
) -> MiscCodecResult<()> {
index = self.next_byte_start(text, index);
if index >= text.len() {
return Ok(());
}
loop {
index = self.consume_byte_prefix(text, index)?;
let (_, high_char, next_index) = read_required_hex_digit(text, index)?;
let (_, low_char, next_index) = read_required_hex_digit(text, next_index)?;
output.push(decode_hex_pair(high_char, low_char));
index = next_index;
let separator_index = self.next_separator_index(text, index, separator);
if separator_index >= text.len() {
return Ok(());
}
let rest = &text[separator_index..];
if !rest.starts_with(separator) {
return Err(invalid_hex_input(&format!(
"missing separator '{separator}' between hex bytes"
)));
}
index = self.next_byte_start(text, separator_index + separator.len());
if index >= text.len() {
return Err(invalid_hex_input("separator must be followed by a hex byte"));
}
}
}
#[inline]
fn consume_byte_prefix(&self, text: &str, index: usize) -> MiscCodecResult<usize> {
let Some(prefix) = self.config.byte_prefix.as_deref().filter(|prefix| !prefix.is_empty()) else {
return Ok(index);
};
let index = if self.starts_with_prefix(&text[index..], prefix) {
index
} else {
self.skip_ignored(text, index)
};
let rest = &text[index..];
if self.starts_with_prefix(rest, prefix) {
Ok(index + prefix.len())
} else {
Err(MiscCodecError::MissingPrefix {
prefix: prefix.to_owned(),
})
}
}
#[must_use]
#[inline]
fn next_separator_index(&self, text: &str, index: usize, separator: &str) -> usize {
if separator.chars().all(|ch| ch.is_ascii_whitespace()) {
let whitespace_end = self.skip_ascii_whitespace(text, index);
if whitespace_end >= text.len() {
return whitespace_end;
}
}
let mut candidate = index;
while candidate < text.len() {
if text[candidate..].starts_with(separator) {
return candidate;
}
if !self.config.ignore_ascii_whitespace || !text.as_bytes()[candidate].is_ascii_whitespace() {
return candidate;
}
candidate += 1;
}
candidate
}
#[must_use]
#[inline]
fn next_byte_start(&self, text: &str, mut index: usize) -> usize {
let prefix = self.config.byte_prefix.as_deref().filter(|prefix| !prefix.is_empty());
while index < text.len() {
if prefix.is_some_and(|prefix| self.starts_with_prefix(&text[index..], prefix)) {
return index;
}
if !self.config.ignore_ascii_whitespace || !text.as_bytes()[index].is_ascii_whitespace() {
return index;
}
index += 1;
}
index
}
fn decode_unprefixed(&self, text: &str, mut index: usize, output: &mut Vec<u8>) -> MiscCodecResult<()> {
let mut pending = None;
let mut digit_count = 0usize;
while index < text.len() {
let rest = &text[index..];
let ch = rest.chars().next().expect("index is below text length");
if self.config.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
index += ch.len_utf8();
continue;
}
if hex_value(ch).is_some() {
digit_count += 1;
if let Some(high_char) = pending.take() {
output.push(decode_hex_pair(high_char, ch));
} else {
pending = Some(ch);
}
index += ch.len_utf8();
continue;
}
return Err(invalid_hex_digit(index, ch));
}
if !digit_count.is_multiple_of(2) {
return Err(invalid_hex_length(digit_count));
}
Ok(())
}
fn decode_byte_prefixed(
&self,
text: &str,
prefix: &str,
mut index: usize,
output: &mut Vec<u8>,
) -> MiscCodecResult<()> {
let mut digit_count = 0usize;
while index < text.len() {
index = self.next_byte_start(text, index);
if index >= text.len() {
break;
}
let rest = &text[index..];
if !self.starts_with_prefix(rest, prefix) {
return Err(MiscCodecError::MissingPrefix {
prefix: prefix.to_owned(),
});
}
index += prefix.len();
let mut pair_count = 0;
let mut pair = [(0usize, '\0'); 2];
while pair_count < 2 && index < text.len() {
let rest = &text[index..];
let ch = rest.chars().next().expect("index is below text length");
if self.config.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
index += ch.len_utf8();
continue;
}
if hex_value(ch).is_some() {
pair[pair_count] = (index, ch);
index += ch.len_utf8();
pair_count += 1;
digit_count += 1;
continue;
}
return Err(invalid_hex_digit(index, ch));
}
if pair_count != 2 {
return Err(invalid_hex_input("byte prefix must be followed by two hex digits"));
}
output.push(decode_hex_pair(pair[0].1, pair[1].1));
}
if !digit_count.is_multiple_of(2) {
return Err(invalid_hex_length(digit_count));
}
Ok(())
}
#[must_use]
#[inline]
fn skip_ignored(&self, text: &str, mut index: usize) -> usize {
while index < text.len() {
let byte = text.as_bytes()[index];
if self.config.ignore_ascii_whitespace && byte.is_ascii_whitespace() {
index += 1;
continue;
}
return index;
}
index
}
#[must_use]
#[inline]
fn skip_ascii_whitespace(&self, text: &str, mut index: usize) -> usize {
while self.config.ignore_ascii_whitespace && index < text.len() {
if !text.as_bytes()[index].is_ascii_whitespace() {
return index;
}
index += 1;
}
index
}
#[must_use]
#[inline]
fn starts_with_prefix(&self, text: &str, prefix: &str) -> bool {
if !self.config.ignore_prefix_case {
return text.starts_with(prefix);
}
let Some(candidate) = text.get(..prefix.len()) else {
return false;
};
candidate.eq_ignore_ascii_case(prefix)
}
}
impl Default for HexCodec {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl ValueEncoder<[u8]> for HexCodec {
type Error = MiscCodecError;
type Output = String;
#[inline]
fn encode(&mut self, input: &[u8]) -> Result<Self::Output, Self::Error> {
Ok(HexCodec::encode(self, input))
}
}
impl ValueDecoder<str> for HexCodec {
type Error = MiscCodecError;
type Output = Vec<u8>;
#[inline]
fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
HexCodec::decode(self, input)
}
}
#[must_use]
#[inline]
pub(crate) fn hex_value(ch: char) -> Option<u8> {
match ch {
'0'..='9' => Some(ch as u8 - b'0'),
'a'..='f' => Some(ch as u8 - b'a' + 10),
'A'..='F' => Some(ch as u8 - b'A' + 10),
_ => None,
}
}
pub(crate) fn invalid_hex_digit(index: usize, character: char) -> MiscCodecError {
MiscCodecError::InvalidDigit {
radix: 16,
index,
character,
}
}
fn invalid_hex_length(actual: usize) -> MiscCodecError {
MiscCodecError::InvalidLength {
context: "hex digits",
expected: "an even number of digits".to_owned(),
actual,
}
}
fn invalid_hex_input(reason: &str) -> MiscCodecError {
MiscCodecError::InvalidInput {
codec: "hex",
reason: reason.to_owned(),
}
}
#[inline]
fn read_required_hex_digit(text: &str, index: usize) -> MiscCodecResult<(usize, char, usize)> {
let rest = &text[index..];
let Some(character) = rest.chars().next() else {
return Err(invalid_hex_input("expected a hexadecimal digit"));
};
if hex_value(character).is_none() {
return Err(invalid_hex_digit(index, character));
}
Ok((index, character, index + character.len_utf8()))
}
#[must_use]
#[inline]
fn decode_hex_pair(high_char: char, low_char: char) -> u8 {
let high = hex_value(high_char).expect("validated hexadecimal digit");
let low = hex_value(low_char).expect("validated hexadecimal digit");
(high << 4) | low
}
#[inline]
fn push_hex_byte(byte: u8, uppercase: bool, output: &mut String) {
output.push(hex_digit(byte >> 4, uppercase));
output.push(hex_digit(byte & 0x0f, uppercase));
}
#[must_use]
#[inline]
pub(crate) fn hex_digit(value: u8, uppercase: bool) -> char {
let digits = if uppercase {
&UPPER_HEX_DIGITS
} else {
&LOWER_HEX_DIGITS
};
digits[(value & 0x0f) as usize]
}