Skip to main content

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