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