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