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