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}