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