Skip to main content

qubit_codec_misc/
c_string_literal_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//! C string literal byte codec.
9
10use core::num::NonZeroUsize;
11
12use qubit_codec::Codec;
13use qubit_codec::DecodeFailure;
14use qubit_codec::ValueDecoder;
15use qubit_codec::ValueEncoder;
16use qubit_utils::nonzero;
17
18use crate::MiscCodecError;
19use crate::MiscCodecResult;
20use crate::internal::CStringLiteralParseContext;
21use crate::internal::ParseError;
22use crate::misc_codec_error::map_misc_decode_failure_with_consumed;
23
24const UPPER_HEX_DIGITS: [char; 16] = [
25    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
26];
27
28/// Encodes and decodes byte-oriented C string literal fragments.
29///
30/// This codec is intended for textual formats that embed byte sequences with C
31/// escapes, such as `PK\003\004` or `\xd0\xcf`. It decodes into raw bytes and
32/// does not require surrounding quotes.
33///
34/// Hexadecimal `\x` and `\X` escapes consume one or two digits. Octal escapes
35/// consume one to three digits and retain only the low eight bits. Universal
36/// `\u` and `\U` escapes require exactly four or eight hex digits,
37/// respectively, and their values must fit in one byte. They produce that byte
38/// directly rather than converting a Unicode code point to UTF-8. The decoder
39/// does not remove surrounding quotes. Raw source text is limited to printable
40/// ASCII, tab, line feed, vertical tab, and form feed; other bytes, including
41/// non-ASCII text and raw carriage returns, must be escaped. An escaped space
42/// (`\ `) is also accepted.
43///
44/// The owned [`decode`](Self::decode) helper treats its input as complete text.
45/// For streaming input, use [`Codec::decode_eof`] after confirming EOF to
46/// resolve format-specific short escapes. Open-stream decoding requests more
47/// input only when the visible escape prefix can still form a valid byte value.
48/// For malformed input, a streaming skip/replace hook may consume the invalid
49/// span visible in the current input chunk; recovery can therefore depend on
50/// chunk boundaries. Valid input and strict decoding are unaffected.
51/// See the [English README](https://github.com/qubit-ltd/rs-codec-misc/blob/main/README.md#safe-streaming-hex-byte-adapter)
52/// and [Chinese README](https://github.com/qubit-ltd/rs-codec-misc/blob/main/README.zh_CN.md#安全的-hex-字节流式-adapter)
53/// for a safe streaming adapter example and the linked EOF short-escape test.
54///
55/// Its low-level [`Codec<Value = u8, Unit = u8>`] implementation handles one
56/// raw byte or one C escape fragment. Whole-fragment iteration remains part of
57/// the owned [`encode`](Self::encode) and [`decode`](Self::decode) helpers.
58///
59/// # Examples
60///
61/// ```
62/// use qubit_codec_misc::CStringLiteralCodec;
63///
64/// let codec = CStringLiteralCodec::new();
65/// assert_eq!(codec.encode(b"Hi"), "Hi");
66/// assert_eq!(codec.decode(r"\x48\x69").unwrap(), b"Hi");
67/// ```
68#[must_use]
69#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
70pub struct CStringLiteralCodec;
71
72impl CStringLiteralCodec {
73    /// Creates a C string literal codec.
74    ///
75    /// # Returns
76    /// A stateless C string literal codec.
77    #[inline]
78    pub fn new() -> Self {
79        Self
80    }
81
82    /// Encodes bytes into a C string literal fragment.
83    ///
84    /// # Parameters
85    /// - `bytes`: Raw bytes to encode.
86    ///
87    /// # Returns
88    /// A C string literal fragment without surrounding quotes.
89    #[inline]
90    #[must_use]
91    pub fn encode(&self, bytes: &[u8]) -> String {
92        let mut output = String::with_capacity(bytes.len());
93        for byte in bytes {
94            push_encoded_byte(*byte, &mut output);
95        }
96        output
97    }
98
99    /// Decodes a C string literal fragment into bytes.
100    ///
101    /// # Parameters
102    /// - `text`: C string literal fragment without surrounding quotes.
103    ///
104    /// # Returns
105    /// Decoded raw bytes.
106    ///
107    /// # Errors
108    /// Returns [`MiscCodecError::InvalidEscape`] for malformed escape
109    /// sequences, [`MiscCodecError::InvalidDigit`] for malformed
110    /// fixed-width numeric escapes,
111    /// and [`MiscCodecError::InvalidCharacter`] for unsupported raw source
112    /// characters.
113    #[inline]
114    pub fn decode(&self, text: &str) -> MiscCodecResult<Vec<u8>> {
115        let input = text.as_bytes();
116        let mut output = Vec::with_capacity(text.len());
117        let mut index = 0;
118        while index < input.len() {
119            let (decoded, consumed) =
120                decode_c_string_literal_unit(input, index, CStringLiteralParseContext::CompleteText(text))
121                    .map_err(|error| c_string_parse_error_to_misc(error, input, index))?;
122            debug_assert!(consumed > 0);
123            output.push(decoded);
124            index += consumed;
125        }
126        Ok(output)
127    }
128}
129
130impl ValueEncoder<[u8]> for CStringLiteralCodec {
131    type Error = MiscCodecError;
132    type Output = String;
133
134    /// Encodes bytes into a C string literal fragment.
135    #[inline]
136    fn encode(&mut self, input: &[u8]) -> Result<Self::Output, Self::Error> {
137        Ok(CStringLiteralCodec::encode(self, input))
138    }
139}
140
141impl ValueDecoder<str> for CStringLiteralCodec {
142    type Error = MiscCodecError;
143    type Output = Vec<u8>;
144
145    /// Decodes a C string literal fragment into bytes.
146    #[inline]
147    fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
148        CStringLiteralCodec::decode(self, input)
149    }
150}
151
152impl Codec for CStringLiteralCodec {
153    type Value = u8;
154    type Unit = u8;
155    type DecodeError = MiscCodecError;
156    type EncodeError = MiscCodecError;
157
158    const MIN_UNITS_PER_VALUE: usize = 1;
159    const MAX_ENCODE_UNITS_PER_VALUE: usize = 4;
160
161    const MAX_DECODE_UNITS_PER_VALUE: usize = 10;
162
163    /// Returns the exact C string literal width for one byte.
164    #[inline]
165    fn encode_len(&self, value: &u8) -> usize {
166        encoded_byte_len(*value)
167    }
168
169    /// Decodes one raw byte or one C escape fragment.
170    ///
171    /// # Safety
172    /// The caller must provide an input index with at least one readable unit
173    /// and a slice satisfying the codec trait's decode preconditions.
174    #[inline]
175    unsafe fn decode(
176        &mut self,
177        input: &[u8],
178        input_index: usize,
179    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
180        debug_assert!(input_index < input.len());
181
182        let (value, consumed) = decode_c_string_literal_byte(input, input_index)
183            .map_err(|error| map_c_string_parse_error_to_decode_failure(error, input, input_index))?;
184        debug_assert!(consumed > 0);
185        // SAFETY: `decode_c_string_literal_byte` returns a non-zero width for
186        // every successful raw byte or escape.
187        let consumed = nonzero(consumed);
188        Ok((value, consumed))
189    }
190
191    /// Decodes one value after end of input has been confirmed.
192    ///
193    /// # Safety
194    /// The caller must provide an input index with at least one readable unit
195    /// and a slice satisfying the codec trait's EOF decode preconditions.
196    #[inline]
197    unsafe fn decode_eof(
198        &mut self,
199        input: &[u8],
200        input_index: usize,
201    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
202        debug_assert!(input_index < input.len());
203
204        let (value, consumed) = decode_c_string_literal_unit(input, input_index, CStringLiteralParseContext::EofBytes)
205            .map_err(|error| map_c_string_parse_error_to_decode_failure(error, input, input_index))?;
206        debug_assert!(consumed > 0);
207        let consumed = nonzero(consumed);
208        Ok((value, consumed))
209    }
210
211    /// Encodes one byte as a raw byte or C escape fragment.
212    ///
213    /// # Safety
214    /// The caller must provide enough writable output units for
215    /// [`Self::encode_len`].
216    #[inline]
217    unsafe fn encode(
218        &mut self,
219        value: &u8,
220        output: &mut [u8],
221        output_index: usize,
222    ) -> Result<usize, Self::EncodeError> {
223        let required = encoded_byte_len(*value);
224        debug_assert!(output_index + required <= output.len());
225
226        let written = write_encoded_byte(*value, output, output_index);
227        debug_assert_eq!(written, required);
228        Ok(required)
229    }
230}
231
232/// Encodes one byte into the destination string.
233///
234/// # Parameters
235/// - `byte`: Byte to encode.
236/// - `output`: Destination string.
237#[inline]
238fn push_encoded_byte(byte: u8, output: &mut String) {
239    match byte {
240        b'\'' => output.push_str("\\'"),
241        b'"' => output.push_str("\\\""),
242        b'?' => output.push_str("\\?"),
243        b'\\' => output.push_str("\\\\"),
244        0x07 => output.push_str("\\a"),
245        0x08 => output.push_str("\\b"),
246        0x0c => output.push_str("\\f"),
247        b'\n' => output.push_str("\\n"),
248        b'\r' => output.push_str("\\r"),
249        b'\t' => output.push_str("\\t"),
250        0x0b => output.push_str("\\v"),
251        b' '..=b'~' => output.push(byte as char),
252        _ => {
253            output.push('\\');
254            output.push('x');
255            output.push(uppercase_hex_digit(byte >> 4));
256            output.push(uppercase_hex_digit(byte & 0x0f));
257        }
258    }
259}
260
261/// Decodes one byte-oriented C string literal fragment from `input`.
262///
263/// # Parameters
264/// - `input`: Encoded byte units.
265/// - `index`: Start index in `input`.
266///
267/// # Returns
268/// Decoded byte and consumed unit count.
269///
270/// # Errors
271/// Returns [`MiscCodecError`] when the raw byte or escape fragment is invalid.
272#[inline]
273fn decode_c_string_literal_byte(input: &[u8], index: usize) -> Result<(u8, usize), ParseError> {
274    decode_c_string_literal_unit(input, index, CStringLiteralParseContext::StreamingBytes)
275}
276
277/// Decodes one C string literal unit from `input`.
278///
279/// # Parameters
280/// - `input`: Encoded byte units.
281/// - `index`: Start index in `input`.
282/// - `context`: Complete-text or streaming-byte parsing context.
283///
284/// # Returns
285/// Decoded byte and consumed unit count.
286///
287/// # Errors
288/// Returns [`MiscCodecError`] when the raw byte or escape fragment is invalid.
289#[inline]
290fn decode_c_string_literal_unit(
291    input: &[u8],
292    index: usize,
293    context: CStringLiteralParseContext<'_>,
294) -> Result<(u8, usize), ParseError> {
295    debug_assert!(index < input.len());
296    let available = input.len() - index;
297    let byte = input[index];
298    if byte != b'\\' {
299        validate_source_unit(input, index, byte, context)?;
300        return Ok((byte, 1));
301    }
302    if available < 2 {
303        return Err(context.trailing_escape_error(index, available));
304    }
305    let escape = input[index + 1];
306    match escape {
307        b' ' => Ok((b' ', 2)),
308        b'\'' => Ok((b'\'', 2)),
309        b'"' => Ok((b'"', 2)),
310        b'?' => Ok((b'?', 2)),
311        b'\\' => Ok((b'\\', 2)),
312        b'a' => Ok((0x07, 2)),
313        b'b' => Ok((0x08, 2)),
314        b'f' => Ok((0x0c, 2)),
315        b'n' => Ok((b'\n', 2)),
316        b'r' => Ok((b'\r', 2)),
317        b't' => Ok((b'\t', 2)),
318        b'v' => Ok((0x0b, 2)),
319        b'x' | b'X' => {
320            if !context.is_complete_text() {
321                ensure_variable_hex_escape_complete(input, index, available)?;
322            }
323            parse_variable_hex_escape_units(input, index)
324        }
325        b'u' => {
326            if context.is_streaming() && available < 6 {
327                validate_fixed_escape_prefix(input, index, 4)?;
328            }
329            if matches!(context, CStringLiteralParseContext::CompleteText(_)) || context.is_streaming() {
330                ensure_fixed_escape_complete(available, nonzero(6))?;
331            }
332            parse_fixed_hex_escape_units(input, index, 4, context)
333        }
334        b'U' => {
335            if context.is_streaming() && available < 10 {
336                validate_fixed_escape_prefix(input, index, 8)?;
337            }
338            if matches!(context, CStringLiteralParseContext::CompleteText(_)) || context.is_streaming() {
339                ensure_fixed_escape_complete(available, nonzero(10))?;
340            }
341            parse_fixed_hex_escape_units(input, index, 8, context)
342        }
343        b'0'..=b'7' => {
344            if context.is_streaming() {
345                ensure_octal_escape_complete(input, index, available)?;
346            }
347            Ok(parse_octal_escape_units(input, index))
348        }
349        _ => Err(invalid_escape(
350            index,
351            &context.escape_fragment(input, index, index + 2),
352            "unsupported escape sequence",
353        )
354        .into()),
355    }
356}
357
358/// Ensures a variable-width `\x` escape has enough units to decide one value.
359///
360/// # Parameters
361/// - `input`: Encoded byte units.
362/// - `index`: Start index of the escape marker.
363/// - `available`: Available unit count from `index`.
364///
365/// # Errors
366/// Returns an internal incomplete parser outcome when more units are required.
367#[inline]
368fn ensure_variable_hex_escape_complete(input: &[u8], index: usize, available: usize) -> Result<(), ParseError> {
369    if available < 3 {
370        return Err(ParseError::Incomplete { required: nonzero(3) });
371    }
372    let mut digit_count = 0usize;
373    while digit_count < 2 {
374        let Some(&byte) = input.get(index + 2 + digit_count) else {
375            break;
376        };
377        if hex_value(char::from(byte)).is_none() {
378            break;
379        }
380        digit_count += 1;
381    }
382    if digit_count == 1 && index + 3 == input.len() {
383        return Err(ParseError::Incomplete { required: nonzero(4) });
384    }
385    Ok(())
386}
387
388/// Ensures a fixed-width universal byte escape has enough units.
389///
390/// # Parameters
391/// - `available`: Available unit count from `index`.
392/// - `required`: Required unit count for this escape form.
393///
394/// # Errors
395/// Returns an internal incomplete parser outcome when more units are required.
396#[inline]
397fn ensure_fixed_escape_complete(available: usize, required: core::num::NonZeroUsize) -> Result<(), ParseError> {
398    if available < required.get() {
399        return Err(ParseError::Incomplete { required });
400    }
401    Ok(())
402}
403
404/// Validates the visible digits of an incomplete fixed-width universal escape.
405///
406/// This is called only for an open stream when the complete escape is not yet
407/// available, so the remaining input belongs to the current escape prefix.
408///
409/// # Parameters
410/// - `input`: Encoded byte units.
411/// - `marker_index`: Start index of the `\u` or `\U` escape marker.
412/// - `digits`: Total number of hexadecimal digits in this escape form.
413///
414/// # Errors
415/// Returns an invalid-digit parser outcome when a visible digit is not
416/// hexadecimal, or an invalid-escape parser outcome when the visible high
417/// digits already exceed the one-byte range.
418#[inline]
419fn validate_fixed_escape_prefix(input: &[u8], marker_index: usize, digits: usize) -> Result<(), ParseError> {
420    for (offset, byte) in input[marker_index + 2..].iter().copied().enumerate() {
421        let character = char::from(byte);
422        if hex_value(character).is_none() {
423            return Err(MiscCodecError::InvalidDigit {
424                radix: 16,
425                index: marker_index + 2 + offset,
426                character,
427            }
428            .into());
429        }
430    }
431    let visible_digits = &input[marker_index + 2..];
432    if visible_digits.iter().take(digits - 2).any(|byte| *byte != b'0') {
433        return Err(invalid_escape(
434            marker_index,
435            &escape_fragment(input, marker_index, input.len()),
436            "universal character value must fit in one byte",
437        )
438        .into());
439    }
440    Ok(())
441}
442
443/// Ensures an octal escape has enough units to decide one value.
444///
445/// # Parameters
446/// - `input`: Encoded byte units.
447/// - `index`: Start index of the escape marker.
448/// - `available`: Available unit count from `index`.
449///
450/// # Errors
451/// Returns an internal incomplete parser outcome when more units are required.
452#[inline]
453fn ensure_octal_escape_complete(input: &[u8], index: usize, _available: usize) -> Result<(), ParseError> {
454    let mut digit_count = 0usize;
455    while digit_count < 3 {
456        let Some(&byte) = input.get(index + 1 + digit_count) else {
457            break;
458        };
459        if octal_value(char::from(byte)).is_none() {
460            break;
461        }
462        digit_count += 1;
463    }
464    if digit_count < 3 && index + 1 + digit_count == input.len() {
465        return Err(ParseError::Incomplete {
466            required: nonzero(2 + digit_count),
467        });
468    }
469    Ok(())
470}
471
472/// Validates a raw source unit.
473///
474/// # Parameters
475/// - `input`: Encoded byte units.
476/// - `index`: Byte index in the encoded input.
477/// - `byte`: Raw source byte.
478/// - `context`: Parsing context used for diagnostics.
479///
480/// # Errors
481/// Returns [`MiscCodecError::InvalidCharacter`] when the byte is not allowed as
482/// a raw C string source byte.
483#[inline]
484fn validate_source_unit(
485    input: &[u8],
486    index: usize,
487    byte: u8,
488    context: CStringLiteralParseContext<'_>,
489) -> Result<(), ParseError> {
490    if matches!(byte, b'\t' | b'\n' | 0x0b | 0x0c | b' '..=b'~') {
491        return Ok(());
492    }
493    Err(MiscCodecError::InvalidCharacter {
494        index,
495        character: context.source_character(input, index),
496        reason: context.raw_source_reason().to_owned(),
497    }
498    .into())
499}
500
501/// Parses a byte-oriented `\x` escape from `input`.
502///
503/// # Parameters
504/// - `input`: Encoded byte units.
505/// - `marker_index`: Byte index of the escape marker.
506///
507/// # Returns
508/// Decoded byte and consumed unit count.
509///
510/// # Errors
511/// Returns [`MiscCodecError::InvalidEscape`] when no hexadecimal digit follows
512/// `\x`.
513#[inline]
514fn parse_variable_hex_escape_units(input: &[u8], marker_index: usize) -> Result<(u8, usize), ParseError> {
515    let mut value = 0u8;
516    let mut digit_count = 0usize;
517    let mut index = marker_index + 2;
518    while digit_count < 2 {
519        let Some(&byte) = input.get(index) else {
520            break;
521        };
522        let Some(digit) = hex_value(char::from(byte)) else {
523            break;
524        };
525        value = (value << 4) | digit;
526        index += 1;
527        digit_count += 1;
528    }
529    if digit_count == 0 {
530        return Err(invalid_escape(marker_index, "\\x", "expected at least one hexadecimal digit").into());
531    }
532    Ok((value, 2 + digit_count))
533}
534
535/// Parses a fixed-width universal byte escape from `input`.
536///
537/// # Parameters
538/// - `input`: Encoded byte units.
539/// - `marker_index`: Byte index of the escape marker.
540/// - `digits`: Required hexadecimal digit count.
541///
542/// # Returns
543/// Decoded byte and consumed unit count.
544///
545/// # Errors
546/// Returns [`MiscCodecError::InvalidEscape`] when the escape is incomplete or
547/// larger than one byte, or [`MiscCodecError::InvalidDigit`] when a required
548/// digit is not hexadecimal.
549#[inline]
550fn parse_fixed_hex_escape_units(
551    input: &[u8],
552    marker_index: usize,
553    digits: usize,
554    context: CStringLiteralParseContext<'_>,
555) -> Result<(u8, usize), ParseError> {
556    let mut value = 0u32;
557    let mut index = marker_index + 2;
558    for _ in 0..digits {
559        let Some(_) = input.get(index) else {
560            return Err(invalid_escape(
561                marker_index,
562                &context.escape_fragment(input, marker_index, input.len()),
563                "incomplete universal character escape",
564            )
565            .into());
566        };
567        let character = context.source_character(input, index);
568        let Some(digit) = hex_value(character) else {
569            return Err(MiscCodecError::InvalidDigit {
570                radix: 16,
571                index,
572                character,
573            }
574            .into());
575        };
576        value = (value << 4) | u32::from(digit);
577        index += 1;
578    }
579    if value > u32::from(u8::MAX) {
580        return Err(invalid_escape(
581            marker_index,
582            &context.escape_fragment(input, marker_index, index),
583            "universal character value must fit in one byte",
584        )
585        .into());
586    }
587    Ok((value as u8, 2 + digits))
588}
589
590/// Parses an octal byte escape from `input`.
591///
592/// # Parameters
593/// - `input`: Encoded byte units.
594/// - `marker_index`: Byte index of the escape marker.
595///
596/// # Returns
597/// Decoded byte and consumed unit count. Values above `0o377` are truncated to
598/// their low byte to match the owned decoder.
599#[inline]
600fn parse_octal_escape_units(input: &[u8], marker_index: usize) -> (u8, usize) {
601    let mut value = 0u16;
602    let mut digit_count = 0usize;
603    let mut index = marker_index + 1;
604    while digit_count < 3 {
605        let Some(&byte) = input.get(index) else {
606            break;
607        };
608        let Some(digit) = octal_value(char::from(byte)) else {
609            break;
610        };
611        value = (value << 3) | u16::from(digit);
612        index += 1;
613        digit_count += 1;
614    }
615    (value as u8, 1 + digit_count)
616}
617
618/// Returns the encoded width for one byte.
619///
620/// # Parameters
621/// - `byte`: Byte to inspect.
622///
623/// # Returns
624/// Number of units written by [`write_encoded_byte`].
625#[must_use]
626#[inline]
627fn encoded_byte_len(byte: u8) -> usize {
628    match byte {
629        b'\'' | b'"' | b'?' | b'\\' | 0x07 | 0x08 | 0x0c | b'\n' | b'\r' | b'\t' | 0x0b => 2,
630        b' '..=b'~' => 1,
631        _ => 4,
632    }
633}
634
635/// Encodes one byte into `output`.
636///
637/// # Parameters
638/// - `byte`: Byte to encode.
639/// - `output`: Destination unit buffer.
640/// - `index`: Start index in `output`.
641///
642/// # Returns
643/// Number of units written.
644#[inline]
645fn write_encoded_byte(byte: u8, output: &mut [u8], index: usize) -> usize {
646    match byte {
647        b'\'' => write_ascii_escape(output, index, b'\''),
648        b'"' => write_ascii_escape(output, index, b'"'),
649        b'?' => write_ascii_escape(output, index, b'?'),
650        b'\\' => write_ascii_escape(output, index, b'\\'),
651        0x07 => write_ascii_escape(output, index, b'a'),
652        0x08 => write_ascii_escape(output, index, b'b'),
653        0x0c => write_ascii_escape(output, index, b'f'),
654        b'\n' => write_ascii_escape(output, index, b'n'),
655        b'\r' => write_ascii_escape(output, index, b'r'),
656        b'\t' => write_ascii_escape(output, index, b't'),
657        0x0b => write_ascii_escape(output, index, b'v'),
658        b' '..=b'~' => {
659            output[index] = byte;
660            1
661        }
662        _ => {
663            output[index] = b'\\';
664            output[index + 1] = b'x';
665            output[index + 2] = uppercase_hex_digit(byte >> 4) as u8;
666            output[index + 3] = uppercase_hex_digit(byte & 0x0f) as u8;
667            4
668        }
669    }
670}
671
672/// Writes a two-unit backslash escape.
673///
674/// # Parameters
675/// - `output`: Destination unit buffer.
676/// - `index`: Start index in `output`.
677/// - `escape`: ASCII escape letter after the backslash.
678///
679/// # Returns
680/// Number of units written.
681#[inline]
682fn write_ascii_escape(output: &mut [u8], index: usize, escape: u8) -> usize {
683    output[index] = b'\\';
684    output[index + 1] = escape;
685    2
686}
687
688/// Builds an ASCII escape fragment from a bounded range of encoded units.
689///
690/// Bytes are mapped directly to characters because the fragment is used only
691/// to display malformed input in diagnostics.
692///
693/// # Parameters
694/// - `input`: Encoded byte units.
695/// - `start`: Start index.
696/// - `end`: Exclusive end index.
697///
698/// # Returns
699/// String fragment used in diagnostics.
700fn escape_fragment(input: &[u8], start: usize, end: usize) -> String {
701    let bounded_end = end.min(input.len());
702    input[start..bounded_end].iter().map(|byte| char::from(*byte)).collect()
703}
704
705/// Converts one hexadecimal character to its nibble value.
706///
707/// # Parameters
708/// - `character`: Character to inspect.
709///
710/// # Returns
711/// Nibble value, or `None` when `character` is not hexadecimal.
712#[inline]
713fn hex_value(character: char) -> Option<u8> {
714    match character {
715        '0'..='9' => Some(character as u8 - b'0'),
716        'a'..='f' => Some(character as u8 - b'a' + 10),
717        'A'..='F' => Some(character as u8 - b'A' + 10),
718        _ => None,
719    }
720}
721
722/// Converts one octal character to its value.
723///
724/// # Parameters
725/// - `character`: Character to inspect.
726///
727/// # Returns
728/// Octal digit value, or `None` when `character` is not octal.
729#[inline]
730fn octal_value(character: char) -> Option<u8> {
731    match character {
732        '0'..='7' => Some(character as u8 - b'0'),
733        _ => None,
734    }
735}
736
737/// Converts one nibble to an uppercase hexadecimal digit.
738///
739/// # Parameters
740/// - `value`: Nibble value. Values above `0x0f` are masked to their low nibble.
741///
742/// # Returns
743/// Uppercase hexadecimal digit.
744#[inline]
745fn uppercase_hex_digit(value: u8) -> char {
746    UPPER_HEX_DIGITS[(value & 0x0f) as usize]
747}
748
749/// Converts a parser outcome into an owned complete-input error.
750///
751/// # Parameters
752/// - `error`: Parser outcome to convert.
753/// - `input`: Encoded source units.
754/// - `index`: Start index of the current value.
755///
756/// # Returns
757/// A concrete codec error suitable for complete or EOF-confirmed input.
758#[inline]
759fn c_string_parse_error_to_misc(error: ParseError, input: &[u8], index: usize) -> MiscCodecError {
760    match error {
761        ParseError::Incomplete { required, .. } => MiscCodecError::InvalidEscape {
762            index,
763            escape: escape_fragment(input, index, input.len()),
764            reason: format!("incomplete escape sequence; expected at least {required} units"),
765        },
766        ParseError::Invalid(error) => error,
767    }
768}
769
770/// Maps a C string parser outcome while retaining the malformed fragment width.
771///
772/// # Parameters
773/// - `error`: Parser outcome to convert.
774/// - `input`: Encoded source units containing the malformed fragment.
775/// - `index`: Start index of that fragment in `input`.
776///
777/// # Returns
778/// A decoding failure that preserves incomplete-input requirements or the
779/// number of units consumed by an invalid fragment.
780#[inline]
781fn map_c_string_parse_error_to_decode_failure(
782    error: ParseError,
783    input: &[u8],
784    index: usize,
785) -> DecodeFailure<MiscCodecError> {
786    match error {
787        ParseError::Incomplete { required, .. } => DecodeFailure::incomplete(required),
788        ParseError::Invalid(error) => {
789            map_misc_decode_failure_with_consumed(error, c_string_invalid_consumed(input, index))
790        }
791    }
792}
793
794/// Returns the number of units belonging to a malformed C string fragment.
795///
796/// # Parameters
797/// - `input`: Encoded source units containing the malformed fragment.
798/// - `index`: Start index of the fragment in `input`.
799///
800/// # Returns
801/// A non-zero width clamped to the available input length, with a minimum of
802/// one to satisfy the decoding failure contract.
803#[inline]
804fn c_string_invalid_consumed(input: &[u8], index: usize) -> NonZeroUsize {
805    let available = input.len().saturating_sub(index);
806    let width = match input.get(index..index.saturating_add(2)) {
807        Some([b'\\', b'u']) => 6,
808        Some([b'\\', b'U']) => 10,
809        Some([b'\\', _]) => 2,
810        _ => 1,
811    };
812    NonZeroUsize::new(width.min(available).max(1)).expect("malformed C string input has at least one unit")
813}
814
815/// Builds a structured error for a malformed or unsupported escape.
816///
817/// # Parameters
818/// - `index`: Byte index of the escape marker in the original input.
819/// - `escape`: Escape fragment that caused the error.
820/// - `reason`: Human-readable rejection reason.
821///
822/// # Returns
823/// An invalid escape error retaining the source position and diagnostic
824/// context.
825pub(crate) fn invalid_escape(index: usize, escape: &str, reason: &str) -> MiscCodecError {
826    MiscCodecError::InvalidEscape {
827        index,
828        escape: escape.to_owned(),
829        reason: reason.to_owned(),
830    }
831}