qubit_codec_misc/hex_codec.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Hexadecimal byte codec.
9
10use qubit_codec::ValueDecoder;
11use qubit_codec::ValueEncoder;
12
13use crate::MiscCodecError;
14use crate::MiscCodecResult;
15use crate::hex_codec_config::HexCodecConfig;
16
17const LOWER_HEX_DIGITS: [char; 16] = [
18 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
19];
20
21const UPPER_HEX_DIGITS: [char; 16] = [
22 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
23];
24
25/// Encodes and decodes hexadecimal byte strings.
26///
27/// `HexCodec` is an owned facade for whole byte slices. Whole-string prefix,
28/// per-byte prefix, separator, and whitespace handling are part of
29/// [`encode`](Self::encode) and [`decode`](Self::decode). Use
30/// [`crate::HexByteCodec`] when a low-level
31/// [`qubit_codec::Codec<Value = u8, Unit = u8>`] is
32/// required.
33///
34/// # Examples
35///
36/// ```
37/// use qubit_codec_misc::HexCodec;
38///
39/// let codec = HexCodec::new();
40/// let encoded = codec.encode(&[0x1f, 0x8b]);
41/// assert_eq!(encoded, "1f8b");
42/// assert_eq!(codec.decode(&encoded).unwrap(), [0x1f, 0x8b]);
43/// ```
44#[must_use]
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct HexCodec {
47 /// Configuration shared by whole-slice encoding and decoding operations.
48 config: HexCodecConfig,
49}
50
51impl HexCodec {
52 /// Creates a lowercase codec without prefix or separators.
53 ///
54 /// # Returns
55 /// A hexadecimal codec using lowercase digits.
56 #[inline]
57 pub fn new() -> Self {
58 Self::from_config(HexCodecConfig::new())
59 }
60
61 /// Creates an uppercase codec without prefix or separators.
62 ///
63 /// # Returns
64 /// A hexadecimal codec using uppercase digits.
65 #[inline]
66 pub fn upper() -> Self {
67 Self::from_config(HexCodecConfig::new().with_uppercase(true))
68 }
69
70 /// Creates a codec from an explicit configuration.
71 #[inline]
72 pub fn from_config(config: HexCodecConfig) -> Self {
73 Self { config }
74 }
75
76 /// Returns the configuration used by this codec.
77 #[inline]
78 pub fn config(&self) -> &HexCodecConfig {
79 &self.config
80 }
81
82 /// Sets whether encoded digits should be uppercase.
83 ///
84 /// # Parameters
85 /// - `uppercase`: Whether to use uppercase hexadecimal digits.
86 ///
87 /// # Returns
88 /// The updated codec.
89 #[inline]
90 pub fn with_uppercase(mut self, uppercase: bool) -> Self {
91 self.config = self.config.with_uppercase(uppercase);
92 self
93 }
94
95 /// Sets a whole-output prefix.
96 ///
97 /// The prefix is written once before the encoded bytes and required once
98 /// before decoded input. For example, using prefix `0x` encodes bytes as
99 /// `0x1f8b`.
100 ///
101 /// # Parameters
102 /// - `prefix`: Whole-output prefix text such as `0x`.
103 ///
104 /// # Returns
105 /// The updated codec.
106 #[inline]
107 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
108 self.config = self.config.with_prefix(prefix);
109 self
110 }
111
112 /// Sets a per-byte prefix.
113 ///
114 /// The prefix is written before every encoded byte and required before
115 /// every decoded byte. For example, using byte prefix `0x` and separator
116 /// ` ` encodes bytes as `0x1f 0x8b`.
117 /// When ASCII whitespace is ignored, a complete per-byte prefix is matched
118 /// before additional whitespace is skipped.
119 ///
120 /// # Parameters
121 /// - `prefix`: Per-byte prefix text such as `0x`.
122 ///
123 /// # Returns
124 /// The updated codec.
125 #[inline]
126 pub fn with_byte_prefix(mut self, prefix: impl Into<String>) -> Self {
127 self.config = self.config.with_byte_prefix(prefix);
128 self
129 }
130
131 /// Sets a separator written and accepted between encoded bytes.
132 ///
133 /// With a non-empty separator, the two hexadecimal digits of each byte
134 /// must be contiguous. Ignored ASCII whitespace may surround complete
135 /// byte tokens and configured separators, but does not replace a required
136 /// prefix or separator. Without a non-empty separator, ignored whitespace
137 /// may also occur between the two digits of a byte. A complete structural
138 /// token is matched before skipping additional whitespace. Non-whitespace
139 /// separators are matched exactly; a whitespace-only separator still
140 /// requires whitespace between bytes, and ignored ASCII whitespace may
141 /// consume additional adjacent whitespace. Prefix case folding does not
142 /// apply to separators.
143 ///
144 /// # Parameters
145 /// - `separator`: Separator text.
146 ///
147 /// # Returns
148 /// The updated codec.
149 #[inline]
150 pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
151 self.config = self.config.with_separator(separator);
152 self
153 }
154
155 /// Sets whether ASCII whitespace is ignored while decoding.
156 ///
157 /// With a non-empty separator, the two hexadecimal digits of each byte
158 /// must be contiguous. Ignored ASCII whitespace may surround complete
159 /// byte tokens and configured separators, but does not replace a required
160 /// prefix or separator. Without a non-empty separator, ignored whitespace
161 /// may also occur between the two digits of a byte. A complete structural
162 /// token is matched before skipping additional whitespace. Non-whitespace
163 /// separators are matched exactly; a whitespace-only separator still
164 /// requires whitespace between bytes, and ignored ASCII whitespace may
165 /// consume additional adjacent whitespace. Prefix case folding does not
166 /// apply to separators.
167 ///
168 /// # Parameters
169 /// - `ignore`: Whether to ignore ASCII whitespace.
170 ///
171 /// # Returns
172 /// The updated codec.
173 #[inline]
174 pub fn with_ignored_ascii_whitespace(mut self, ignore: bool) -> Self {
175 self.config = self.config.with_ignored_ascii_whitespace(ignore);
176 self
177 }
178
179 /// Sets whether ASCII case is ignored when decoding configured prefixes.
180 ///
181 /// This option affects whole-output prefixes and per-byte prefixes during
182 /// decoding only. Encoding writes prefixes exactly as configured.
183 ///
184 /// # Parameters
185 /// - `ignore`: Whether to ignore ASCII case while matching prefixes.
186 ///
187 /// # Returns
188 /// The updated codec.
189 #[inline]
190 pub fn with_ignore_prefix_case(mut self, ignore: bool) -> Self {
191 self.config = self.config.with_ignore_prefix_case(ignore);
192 self
193 }
194
195 /// Encodes bytes into a hexadecimal string.
196 ///
197 /// # Parameters
198 /// - `bytes`: Bytes to encode.
199 ///
200 /// # Returns
201 /// Hexadecimal text.
202 #[inline]
203 #[must_use]
204 pub fn encode(&self, bytes: &[u8]) -> String {
205 let separator_len = self.config.separator.as_ref().map_or(0, String::len);
206 let prefix_len = self.config.prefix.as_ref().map_or(0, String::len);
207 let byte_prefix_len = self.config.byte_prefix.as_ref().map_or(0, String::len);
208 let capacity = prefix_len.saturating_add(
209 bytes
210 .len()
211 .saturating_mul(byte_prefix_len.saturating_add(2))
212 .saturating_add(bytes.len().saturating_sub(1).saturating_mul(separator_len)),
213 );
214 let mut output = String::with_capacity(capacity);
215 self.encode_into(bytes, &mut output);
216 output
217 }
218
219 /// Encodes bytes into an existing string.
220 ///
221 /// # Parameters
222 /// - `bytes`: Bytes to encode.
223 /// - `output`: Destination string.
224 #[inline]
225 pub fn encode_into(&self, bytes: &[u8], output: &mut String) {
226 if let Some(prefix) = &self.config.prefix {
227 output.push_str(prefix);
228 }
229 for (index, byte) in bytes.iter().enumerate() {
230 if index > 0
231 && let Some(separator) = &self.config.separator
232 {
233 output.push_str(separator);
234 }
235 if let Some(byte_prefix) = &self.config.byte_prefix {
236 output.push_str(byte_prefix);
237 }
238 push_hex_byte(*byte, self.config.uppercase, output);
239 }
240 }
241
242 /// Decodes hexadecimal text into bytes.
243 ///
244 /// # Parameters
245 /// - `text`: Hexadecimal text.
246 ///
247 /// # Returns
248 /// Decoded bytes.
249 ///
250 /// # Errors
251 /// Returns [`MiscCodecError`] when a configured whole or per-byte prefix is
252 /// missing, when the normalized digit count is odd, or when a non-hex
253 /// digit is found.
254 #[inline]
255 pub fn decode(&self, text: &str) -> MiscCodecResult<Vec<u8>> {
256 let mut output = Vec::new();
257 self.decode_into(text, &mut output)?;
258 Ok(output)
259 }
260
261 /// Decodes hexadecimal text into an existing byte vector.
262 ///
263 /// # Parameters
264 /// - `text`: Hexadecimal text.
265 /// - `output`: Destination byte vector.
266 ///
267 /// # Errors
268 /// Returns [`MiscCodecError`] when the input is malformed. On such an
269 /// error, the vector's elements and length remain unchanged; its capacity
270 /// may have changed.
271 #[inline]
272 pub fn decode_into(&self, text: &str, output: &mut Vec<u8>) -> MiscCodecResult<()> {
273 let original_len = output.len();
274 if let Err(error) = self.decode_append(text, output) {
275 output.truncate(original_len);
276 return Err(error);
277 }
278 Ok(())
279 }
280
281 /// Decodes text by appending bytes directly to `output`.
282 ///
283 /// # Parameters
284 /// - `text`: Text to decode.
285 /// - `output`: Destination byte vector, which may contain existing bytes.
286 ///
287 /// # Returns
288 /// `Ok(())` when all bytes are decoded.
289 ///
290 /// # Errors
291 /// Returns [`MiscCodecError::MissingPrefix`] when a configured prefix is
292 /// absent, [`MiscCodecError::InvalidLength`] for an odd digit count,
293 /// [`MiscCodecError::InvalidDigit`] for unsupported characters, or
294 /// [`MiscCodecError::InvalidInput`] for malformed separator syntax.
295 #[inline]
296 fn decode_append(&self, text: &str, output: &mut Vec<u8>) -> MiscCodecResult<()> {
297 let start_index = self.consume_prefix(text)?;
298 let is_plain = !self.config.ignore_ascii_whitespace
299 && self.config.byte_prefix.as_deref().is_none_or(str::is_empty)
300 && self.config.separator.as_deref().is_none_or(str::is_empty);
301 if is_plain {
302 output.reserve(text.len().saturating_sub(start_index) / 2);
303 }
304 if let Some(separator) = self
305 .config
306 .separator
307 .as_deref()
308 .filter(|separator| !separator.is_empty())
309 {
310 return self.decode_separated(text, start_index, separator, output);
311 }
312 if let Some(byte_prefix) = self.config.byte_prefix.as_deref().filter(|prefix| !prefix.is_empty()) {
313 return self.decode_byte_prefixed(text, byte_prefix, start_index, output);
314 }
315 self.decode_unprefixed(text, start_index, output)
316 }
317
318 /// Consumes the configured whole-output prefix.
319 ///
320 /// # Parameters
321 /// - `text`: Text to decode.
322 ///
323 /// # Returns
324 /// Byte index where byte parsing should start.
325 ///
326 /// # Errors
327 /// Returns [`MiscCodecError::MissingPrefix`] when a non-empty whole-output
328 /// prefix is configured but absent.
329 ///
330 /// A complete configured prefix is matched before ignored ASCII whitespace
331 /// is skipped, so whitespace that belongs to the prefix is preserved.
332 #[inline]
333 fn consume_prefix(&self, text: &str) -> MiscCodecResult<usize> {
334 let Some(prefix) = self.config.prefix.as_deref().filter(|prefix| !prefix.is_empty()) else {
335 return Ok(0);
336 };
337 let mut index = 0;
338 loop {
339 if self.starts_with_prefix(&text[index..], prefix) {
340 return Ok(index + prefix.len());
341 }
342 if !self.config.ignore_ascii_whitespace
343 || index == text.len()
344 || !text.as_bytes()[index].is_ascii_whitespace()
345 {
346 return Err(MiscCodecError::MissingPrefix {
347 prefix: prefix.to_owned(),
348 });
349 }
350 index += 1;
351 }
352 }
353
354 /// Decodes separator-delimited input into bytes.
355 ///
356 /// # Parameters
357 /// - `text`: Text to decode.
358 /// - `index`: Byte index where parsing should start.
359 /// - `separator`: Required separator between complete bytes.
360 ///
361 /// # Returns
362 /// `Ok(())` when all bytes are decoded.
363 ///
364 /// # Errors
365 /// Returns [`MiscCodecError::MissingPrefix`] when a configured per-byte
366 /// prefix is absent, or [`MiscCodecError::InvalidInput`] when a byte or
367 /// separator syntax is malformed.
368 fn decode_separated(
369 &self,
370 text: &str,
371 mut index: usize,
372 separator: &str,
373 output: &mut Vec<u8>,
374 ) -> MiscCodecResult<()> {
375 index = self.next_byte_start(text, index);
376 if index >= text.len() {
377 return Ok(());
378 }
379 loop {
380 index = self.consume_byte_prefix(text, index)?;
381 let (_, high_char, next_index) = read_required_hex_digit(text, index)?;
382 let (_, low_char, next_index) = read_required_hex_digit(text, next_index)?;
383 output.push(decode_hex_pair(high_char, low_char));
384 index = next_index;
385
386 let separator_index = self.next_separator_index(text, index, separator);
387 if separator_index >= text.len() {
388 return Ok(());
389 }
390 let rest = &text[separator_index..];
391 if !rest.starts_with(separator) {
392 return Err(invalid_hex_input(&format!(
393 "missing separator '{separator}' between hex bytes"
394 )));
395 }
396 index = self.next_byte_start(text, separator_index + separator.len());
397 if index >= text.len() {
398 return Err(invalid_hex_input("separator must be followed by a hex byte"));
399 }
400 }
401 }
402
403 /// Consumes the configured per-byte prefix.
404 ///
405 /// # Parameters
406 /// - `text`: Text to decode.
407 /// - `index`: Current byte index.
408 ///
409 /// # Returns
410 /// Index after the per-byte prefix, or `index` when no non-empty per-byte
411 /// prefix is configured.
412 ///
413 /// # Errors
414 /// Returns [`MiscCodecError::MissingPrefix`] when the configured per-byte
415 /// prefix is absent.
416 #[inline]
417 fn consume_byte_prefix(&self, text: &str, index: usize) -> MiscCodecResult<usize> {
418 let Some(prefix) = self.config.byte_prefix.as_deref().filter(|prefix| !prefix.is_empty()) else {
419 return Ok(index);
420 };
421 let index = if self.starts_with_prefix(&text[index..], prefix) {
422 index
423 } else {
424 self.skip_ignored(text, index)
425 };
426 let rest = &text[index..];
427 if self.starts_with_prefix(rest, prefix) {
428 Ok(index + prefix.len())
429 } else {
430 Err(MiscCodecError::MissingPrefix {
431 prefix: prefix.to_owned(),
432 })
433 }
434 }
435
436 /// Finds the position where the next separator must appear.
437 ///
438 /// # Parameters
439 /// - `text`: Text being decoded.
440 /// - `index`: Current byte index after a complete hex byte.
441 /// - `separator`: Configured separator.
442 ///
443 /// # Returns
444 /// Index where the separator must start, or `text.len()` when only ignored
445 /// trailing whitespace remains.
446 #[inline]
447 fn next_separator_index(&self, text: &str, index: usize, separator: &str) -> usize {
448 if separator.chars().all(|ch| ch.is_ascii_whitespace()) {
449 let whitespace_end = self.skip_ascii_whitespace(text, index);
450 if whitespace_end >= text.len() {
451 return whitespace_end;
452 }
453 }
454 let mut candidate = index;
455 while candidate < text.len() {
456 if text[candidate..].starts_with(separator) {
457 return candidate;
458 }
459 if !self.config.ignore_ascii_whitespace || !text.as_bytes()[candidate].is_ascii_whitespace() {
460 return candidate;
461 }
462 candidate += 1;
463 }
464 candidate
465 }
466
467 /// Finds the next byte start while preserving a matching per-byte prefix.
468 ///
469 /// # Parameters
470 /// - `text`: Text being decoded.
471 /// - `index`: Byte index where the next byte may begin.
472 ///
473 /// # Returns
474 /// The current index when a configured per-byte prefix matches, otherwise
475 /// the first non-ignored-whitespace index. A structural prefix takes
476 /// precedence over whitespace skipping.
477 #[inline]
478 fn next_byte_start(&self, text: &str, mut index: usize) -> usize {
479 let prefix = self.config.byte_prefix.as_deref().filter(|prefix| !prefix.is_empty());
480 while index < text.len() {
481 if prefix.is_some_and(|prefix| self.starts_with_prefix(&text[index..], prefix)) {
482 return index;
483 }
484 if !self.config.ignore_ascii_whitespace || !text.as_bytes()[index].is_ascii_whitespace() {
485 return index;
486 }
487 index += 1;
488 }
489 index
490 }
491
492 /// Decodes unprefixed input characters into bytes.
493 ///
494 /// # Parameters
495 /// - `text`: Text to decode.
496 ///
497 /// # Returns
498 /// `Ok(())` when all bytes are decoded.
499 ///
500 /// # Errors
501 /// Returns [`MiscCodecError::InvalidLength`] when the normalized digit
502 /// count is odd, or [`MiscCodecError::InvalidDigit`] for unsupported
503 /// characters.
504 fn decode_unprefixed(&self, text: &str, mut index: usize, output: &mut Vec<u8>) -> MiscCodecResult<()> {
505 let mut pending = None;
506 let mut digit_count = 0usize;
507 while index < text.len() {
508 let rest = &text[index..];
509 let ch = rest.chars().next().expect("index is below text length");
510 if self.config.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
511 index += ch.len_utf8();
512 continue;
513 }
514 if hex_value(ch).is_some() {
515 digit_count += 1;
516 if let Some(high_char) = pending.take() {
517 output.push(decode_hex_pair(high_char, ch));
518 } else {
519 pending = Some(ch);
520 }
521 index += ch.len_utf8();
522 continue;
523 }
524 return Err(invalid_hex_digit(index, ch));
525 }
526 if !digit_count.is_multiple_of(2) {
527 return Err(invalid_hex_length(digit_count));
528 }
529 Ok(())
530 }
531
532 /// Decodes byte-prefixed input characters into bytes.
533 ///
534 /// # Parameters
535 /// - `text`: Text to decode.
536 /// - `prefix`: Required prefix before each byte.
537 /// - `index`: Byte index where parsing should start.
538 ///
539 /// # Returns
540 /// `Ok(())` when all bytes are decoded.
541 ///
542 /// # Errors
543 /// Returns [`MiscCodecError::MissingPrefix`] when a byte prefix is missing,
544 /// [`MiscCodecError::InvalidInput`] when a prefix is not followed by a
545 /// complete byte, or [`MiscCodecError::InvalidDigit`] for unsupported
546 /// characters.
547 fn decode_byte_prefixed(
548 &self,
549 text: &str,
550 prefix: &str,
551 mut index: usize,
552 output: &mut Vec<u8>,
553 ) -> MiscCodecResult<()> {
554 let mut digit_count = 0usize;
555 while index < text.len() {
556 index = self.next_byte_start(text, index);
557 if index >= text.len() {
558 break;
559 }
560 let rest = &text[index..];
561 if !self.starts_with_prefix(rest, prefix) {
562 return Err(MiscCodecError::MissingPrefix {
563 prefix: prefix.to_owned(),
564 });
565 }
566 index += prefix.len();
567
568 let mut pair_count = 0;
569 let mut pair = [(0usize, '\0'); 2];
570 while pair_count < 2 && index < text.len() {
571 let rest = &text[index..];
572 let ch = rest.chars().next().expect("index is below text length");
573 if self.config.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
574 index += ch.len_utf8();
575 continue;
576 }
577 if hex_value(ch).is_some() {
578 pair[pair_count] = (index, ch);
579 index += ch.len_utf8();
580 pair_count += 1;
581 digit_count += 1;
582 continue;
583 }
584 return Err(invalid_hex_digit(index, ch));
585 }
586 if pair_count != 2 {
587 return Err(invalid_hex_input("byte prefix must be followed by two hex digits"));
588 }
589 output.push(decode_hex_pair(pair[0].1, pair[1].1));
590 }
591 if !digit_count.is_multiple_of(2) {
592 return Err(invalid_hex_length(digit_count));
593 }
594 Ok(())
595 }
596
597 /// Skips ignored ASCII whitespace.
598 ///
599 /// # Parameters
600 /// - `text`: Text being decoded.
601 /// - `index`: Current byte index.
602 ///
603 /// # Returns
604 /// The next byte index that should be parsed.
605 #[inline]
606 fn skip_ignored(&self, text: &str, mut index: usize) -> usize {
607 while index < text.len() {
608 let byte = text.as_bytes()[index];
609 if self.config.ignore_ascii_whitespace && byte.is_ascii_whitespace() {
610 index += 1;
611 continue;
612 }
613 return index;
614 }
615 index
616 }
617
618 /// Skips ignored leading ASCII whitespace.
619 ///
620 /// # Parameters
621 /// - `text`: Text being decoded.
622 /// - `index`: Current byte index.
623 ///
624 /// # Returns
625 /// The next byte index after ignored ASCII whitespace.
626 #[inline]
627 fn skip_ascii_whitespace(&self, text: &str, mut index: usize) -> usize {
628 while self.config.ignore_ascii_whitespace && index < text.len() {
629 if !text.as_bytes()[index].is_ascii_whitespace() {
630 return index;
631 }
632 index += 1;
633 }
634 index
635 }
636
637 /// Tests whether `text` starts with a configured prefix.
638 ///
639 /// # Parameters
640 /// - `text`: Text slice to inspect.
641 /// - `prefix`: Configured prefix.
642 ///
643 /// # Returns
644 /// `true` when `text` starts with `prefix`, honoring the configured
645 /// ASCII case sensitivity for decoding prefixes.
646 #[inline]
647 fn starts_with_prefix(&self, text: &str, prefix: &str) -> bool {
648 if !self.config.ignore_prefix_case {
649 return text.starts_with(prefix);
650 }
651 let Some(candidate) = text.get(..prefix.len()) else {
652 return false;
653 };
654 candidate.eq_ignore_ascii_case(prefix)
655 }
656}
657
658impl Default for HexCodec {
659 /// Creates a lowercase codec without prefix or separators.
660 #[inline]
661 fn default() -> Self {
662 Self::new()
663 }
664}
665
666impl ValueEncoder<[u8]> for HexCodec {
667 type Error = MiscCodecError;
668 type Output = String;
669
670 /// Encodes bytes into hexadecimal text.
671 #[inline]
672 fn encode(&mut self, input: &[u8]) -> Result<Self::Output, Self::Error> {
673 Ok(HexCodec::encode(self, input))
674 }
675}
676
677impl ValueDecoder<str> for HexCodec {
678 type Error = MiscCodecError;
679 type Output = Vec<u8>;
680
681 /// Decodes hexadecimal text into bytes.
682 #[inline]
683 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
684 HexCodec::decode(self, input)
685 }
686}
687
688/// Converts one hex digit to its value.
689///
690/// # Parameters
691/// - `ch`: Character to inspect.
692///
693/// # Returns
694/// Nibble value, or `None` when `ch` is not a hex digit.
695#[inline]
696pub(crate) fn hex_value(ch: char) -> Option<u8> {
697 match ch {
698 '0'..='9' => Some(ch as u8 - b'0'),
699 'a'..='f' => Some(ch as u8 - b'a' + 10),
700 'A'..='F' => Some(ch as u8 - b'A' + 10),
701 _ => None,
702 }
703}
704
705/// Builds an invalid hexadecimal digit error.
706///
707/// # Parameters
708/// - `index`: Byte index of the invalid character in the original input.
709/// - `character`: Invalid character.
710///
711/// # Returns
712/// A radix-16 digit error.
713pub(crate) fn invalid_hex_digit(index: usize, character: char) -> MiscCodecError {
714 MiscCodecError::InvalidDigit {
715 radix: 16,
716 index,
717 character,
718 }
719}
720
721/// Builds an invalid hexadecimal length error.
722///
723/// # Parameters
724/// - `actual`: Number of normalized hexadecimal digits.
725///
726/// # Returns
727/// An invalid length error describing the even-digit requirement.
728fn invalid_hex_length(actual: usize) -> MiscCodecError {
729 MiscCodecError::InvalidLength {
730 context: "hex digits",
731 expected: "an even number of digits".to_owned(),
732 actual,
733 }
734}
735
736/// Builds an invalid hexadecimal input error.
737///
738/// # Parameters
739/// - `reason`: Human-readable reason the input was rejected.
740///
741/// # Returns
742/// An invalid input error for the hexadecimal codec.
743fn invalid_hex_input(reason: &str) -> MiscCodecError {
744 MiscCodecError::InvalidInput {
745 codec: "hex",
746 reason: reason.to_owned(),
747 }
748}
749
750/// Reads one required hexadecimal digit at a byte boundary.
751///
752/// # Parameters
753/// - `text`: Text being decoded.
754/// - `index`: Byte index where the digit is expected.
755///
756/// # Returns
757/// Original digit index, digit character, and the next byte index.
758///
759/// # Errors
760/// Returns [`MiscCodecError::InvalidInput`] when input ends before the digit,
761/// or [`MiscCodecError::InvalidDigit`] when the next character is not hex.
762#[inline]
763fn read_required_hex_digit(text: &str, index: usize) -> MiscCodecResult<(usize, char, usize)> {
764 let rest = &text[index..];
765 let Some(character) = rest.chars().next() else {
766 return Err(invalid_hex_input("expected a hexadecimal digit"));
767 };
768 if hex_value(character).is_none() {
769 return Err(invalid_hex_digit(index, character));
770 }
771 Ok((index, character, index + character.len_utf8()))
772}
773
774/// Decodes two hexadecimal characters into one byte.
775///
776/// # Parameters
777/// - `high_char`: High-nibble character.
778/// - `low_char`: Low-nibble character.
779///
780/// # Returns
781/// The decoded byte.
782///
783/// The callers validate both characters before constructing the pair.
784#[inline]
785fn decode_hex_pair(high_char: char, low_char: char) -> u8 {
786 let high = hex_value(high_char).expect("validated hexadecimal digit");
787 let low = hex_value(low_char).expect("validated hexadecimal digit");
788 (high << 4) | low
789}
790
791/// Appends one encoded byte to `output`.
792///
793/// # Parameters
794/// - `byte`: Byte to encode.
795/// - `uppercase`: Whether to use uppercase digits.
796/// - `output`: Destination string.
797#[inline]
798fn push_hex_byte(byte: u8, uppercase: bool, output: &mut String) {
799 output.push(hex_digit(byte >> 4, uppercase));
800 output.push(hex_digit(byte & 0x0f, uppercase));
801}
802
803/// Converts one nibble to a hexadecimal digit.
804///
805/// # Parameters
806/// - `value`: Nibble value.
807/// - `uppercase`: Whether to use uppercase digits.
808///
809/// # Returns
810/// Hexadecimal digit. Values above `0x0f` are masked to their low nibble.
811#[inline]
812pub(crate) fn hex_digit(value: u8, uppercase: bool) -> char {
813 let digits = if uppercase {
814 &UPPER_HEX_DIGITS
815 } else {
816 &LOWER_HEX_DIGITS
817 };
818 digits[(value & 0x0f) as usize]
819}