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