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