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}