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