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