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