qubit_codec_misc/c_integer_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 integer literal decoder.
9
10use qubit_codec::ValueDecoder;
11
12use crate::MiscCodecError;
13use crate::MiscCodecResult;
14use crate::internal::LiteralComponents;
15
16/// Decodes restricted non-negative C integer literal fragments.
17///
18/// This codec accepts decimal literals such as `123`, octal literals such as
19/// `0123`, and hexadecimal literals such as `0x123` or `0X123`. It trims
20/// surrounding whitespace and returns a `u64`. It intentionally accepts only
21/// a complete unsigned token: signs, integer suffixes, digit separators, and
22/// token-stream boundaries are outside this codec's contract.
23#[must_use]
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct CIntegerLiteralCodec;
26
27impl CIntegerLiteralCodec {
28 /// Creates a C integer literal codec.
29 ///
30 /// # Returns
31 /// A stateless C integer literal codec.
32 #[inline]
33 pub fn new() -> Self {
34 Self
35 }
36
37 /// Decodes a C integer literal into a `u64`.
38 ///
39 /// # Parameters
40 /// - `text`: C integer literal text.
41 ///
42 /// # Returns
43 /// Parsed integer value.
44 ///
45 /// # Errors
46 /// Returns [`MiscCodecError::InvalidInput`] when the input is empty, lacks
47 /// digits, or overflows `u64`; returns [`MiscCodecError::InvalidDigit`]
48 /// when a character is not valid for the detected radix.
49 #[inline]
50 pub fn decode(&self, text: &str) -> MiscCodecResult<u64> {
51 let (trimmed, trim_offset) = trim_with_offset(text);
52 if trimmed.is_empty() {
53 return Err(invalid_c_integer_input("expected at least one digit"));
54 }
55 let components = LiteralComponents::parse(trimmed, trim_offset)?;
56 validate_digits(components)?;
57 u64::from_str_radix(components.digits, components.radix).map_err(|error| {
58 invalid_c_integer_input(&format!("integer literal is out of range: {error}"))
59 })
60 }
61}
62
63impl ValueDecoder<str> for CIntegerLiteralCodec {
64 type Error = MiscCodecError;
65 type Output = u64;
66
67 /// Decodes a C integer literal into a `u64`.
68 #[inline]
69 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
70 CIntegerLiteralCodec::decode(self, input)
71 }
72}
73
74/// Trims surrounding whitespace while preserving the start byte offset.
75///
76/// # Parameters
77/// - `text`: Input text.
78///
79/// # Returns
80/// Trimmed text and the byte offset where it starts in `text`.
81#[inline]
82fn trim_with_offset(text: &str) -> (&str, usize) {
83 let trimmed_start = text.trim_start();
84 let start = text.len() - trimmed_start.len();
85 (trimmed_start.trim_end(), start)
86}
87
88/// Validates that every character is valid for the detected radix.
89///
90/// # Parameters
91/// - `components`: Parsed literal components.
92///
93/// # Errors
94/// Returns [`MiscCodecError::InvalidDigit`] with the original input byte index
95/// of the invalid character.
96fn validate_digits(components: LiteralComponents<'_>) -> MiscCodecResult<()> {
97 for (index, character) in components.digits.char_indices() {
98 if character.is_digit(components.radix) {
99 continue;
100 }
101 return Err(MiscCodecError::InvalidDigit {
102 radix: components.radix,
103 index: components.digits_offset + index,
104 character,
105 });
106 }
107 Ok(())
108}
109
110/// Builds an invalid C integer literal input error.
111///
112/// # Parameters
113/// - `reason`: Human-readable reason the input was rejected.
114///
115/// # Returns
116/// An invalid input error for the C integer literal codec.
117fn invalid_c_integer_input(reason: &str) -> MiscCodecError {
118 MiscCodecError::InvalidInput {
119 codec: "c-integer-literal",
120 reason: reason.to_owned(),
121 }
122}