Skip to main content

aws_smithy_types/
big_number.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Big number types represented as strings.
7//!
8//! These types are simple string wrappers that allow users to parse and format
9//! big numbers using their preferred library.
10
11/// Error type for BigInteger and BigDecimal parsing.
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum BigNumberError {
15    /// The input string is not a valid number format.
16    InvalidFormat(String),
17    /// The number's exponent is outside the supported range.
18    ExponentOutOfRange(String),
19}
20
21impl std::fmt::Display for BigNumberError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            BigNumberError::InvalidFormat(s) => write!(f, "invalid number format: {s}"),
25            BigNumberError::ExponentOutOfRange(s) => {
26                write!(f, "number exponent is outside the supported range: {s}")
27            }
28        }
29    }
30}
31
32impl std::error::Error for BigNumberError {}
33
34/// Validates that a string is a valid BigInteger format.
35/// Only allows digits and an optional leading sign.
36fn is_valid_big_integer(s: &str) -> bool {
37    if s.is_empty() {
38        return false;
39    }
40
41    let mut chars = s.chars();
42
43    // Check first character (can be sign or digit)
44    match chars.next() {
45        Some('-') | Some('+') | Some('0'..='9') => {}
46        _ => return false,
47    }
48
49    // Rest must be digits only
50    chars.all(|c| c.is_ascii_digit())
51}
52
53/// Validates that a string is a valid BigDecimal format.
54/// Allows digits, sign, decimal point, and scientific notation.
55fn validate_big_decimal(s: &str) -> Result<(), BigNumberError> {
56    let invalid_format = || BigNumberError::InvalidFormat(s.to_string());
57    let exponent_out_of_range = || BigNumberError::ExponentOutOfRange(s.to_string());
58
59    let (coefficient, exponent) = match s.split_once(['e', 'E']) {
60        Some((coefficient, exponent)) => match exponent.parse::<i128>() {
61            Ok(exponent) => (coefficient, exponent),
62            Err(error)
63                if matches!(
64                    error.kind(),
65                    std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
66                ) =>
67            {
68                return Err(exponent_out_of_range());
69            }
70            Err(_) => return Err(invalid_format()),
71        },
72        None => (s, 0),
73    };
74
75    let coefficient = coefficient.strip_prefix(['-', '+']).unwrap_or(coefficient);
76    let (integer, fraction) = coefficient.split_once('.').unwrap_or((coefficient, ""));
77
78    if (integer.is_empty() && fraction.is_empty())
79        || !integer
80            .bytes()
81            .chain(fraction.bytes())
82            .all(|byte| byte.is_ascii_digit())
83    {
84        return Err(invalid_format());
85    }
86
87    match (fraction.len() as i128).checked_sub(exponent) {
88        Some(scale) if (-(i64::MAX as i128)..=i64::MAX as i128).contains(&scale) => Ok(()),
89        _ => Err(exponent_out_of_range()),
90    }
91}
92
93/// A BigInteger represented as a string.
94///
95/// This type does not perform arithmetic operations. Users should parse the string
96/// with their preferred big integer library.
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98pub struct BigInteger(String);
99
100impl Default for BigInteger {
101    fn default() -> Self {
102        Self("0".to_string())
103    }
104}
105
106impl std::str::FromStr for BigInteger {
107    type Err = BigNumberError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        if !is_valid_big_integer(s) {
111            return Err(BigNumberError::InvalidFormat(s.to_string()));
112        }
113        Ok(Self(s.to_string()))
114    }
115}
116
117impl AsRef<str> for BigInteger {
118    fn as_ref(&self) -> &str {
119        &self.0
120    }
121}
122
123/// A big decimal represented as a string.
124///
125/// This type does not perform arithmetic operations. Users should parse the string
126/// with their preferred big decimal library.
127#[derive(Debug, Clone, PartialEq, Eq, Hash)]
128pub struct BigDecimal(String);
129
130impl Default for BigDecimal {
131    fn default() -> Self {
132        Self("0.0".to_string())
133    }
134}
135
136impl std::str::FromStr for BigDecimal {
137    type Err = BigNumberError;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        validate_big_decimal(s)?;
141        Ok(Self(s.to_string()))
142    }
143}
144
145impl AsRef<str> for BigDecimal {
146    fn as_ref(&self) -> &str {
147        &self.0
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::str::FromStr;
155
156    #[test]
157    fn big_integer_basic() {
158        let bi = BigInteger::from_str("12345678901234567890").unwrap();
159        assert_eq!(bi.as_ref(), "12345678901234567890");
160    }
161
162    #[test]
163    fn big_integer_default() {
164        let bi = BigInteger::default();
165        assert_eq!(bi.as_ref(), "0");
166    }
167
168    #[test]
169    fn big_decimal_basic() {
170        let bd = BigDecimal::from_str("123.456789").unwrap();
171        assert_eq!(bd.as_ref(), "123.456789");
172    }
173
174    #[test]
175    fn big_decimal_default() {
176        let bd = BigDecimal::default();
177        assert_eq!(bd.as_ref(), "0.0");
178    }
179
180    #[test]
181    fn big_integer_negative() {
182        let bi = BigInteger::from_str("-12345").unwrap();
183        assert_eq!(bi.as_ref(), "-12345");
184    }
185
186    #[test]
187    fn big_decimal_scientific() {
188        let bd = BigDecimal::from_str("1.23e10").unwrap();
189        assert_eq!(bd.as_ref(), "1.23e10");
190
191        let bd = BigDecimal::from_str("1.23E-10").unwrap();
192        assert_eq!(bd.as_ref(), "1.23E-10");
193    }
194
195    #[test]
196    fn big_decimal_accepts_supported_formats() {
197        for value in ["0", "+5", "-0.0", ".5", "-.5", "5.", "1.5E+3"] {
198            assert!(BigDecimal::from_str(value).is_ok(), "{value}");
199        }
200    }
201
202    #[test]
203    fn big_decimal_rejects_malformed_values() {
204        for value in [
205            "1.2.3", "-", "+", ".", "e", "E", "1e", "1e+", "--5", "1-2", "12-34", "..", "1.2e3.4",
206            "+-1",
207        ] {
208            assert_eq!(
209                BigDecimal::from_str(value),
210                Err(BigNumberError::InvalidFormat(value.to_string())),
211                "{value}"
212            );
213        }
214    }
215
216    #[test]
217    fn big_decimal_rejects_out_of_range_exponents() {
218        for value in [
219            "1E99999999999999999999",
220            "1E-99999999999999999999",
221            "1e9223372036854775808",
222            "1e-9223372036854775808",
223            "1e999999999999999999999999999999999999999",
224        ] {
225            assert_eq!(
226                BigDecimal::from_str(value),
227                Err(BigNumberError::ExponentOutOfRange(value.to_string())),
228                "{value}"
229            );
230        }
231    }
232
233    #[test]
234    fn big_integer_rejects_json_injection() {
235        // Reject strings with JSON special characters
236        assert!(BigInteger::from_str("123, \"injected\": true").is_err());
237        assert!(BigInteger::from_str("123}").is_err());
238        assert!(BigInteger::from_str("{\"hacked\": 1}").is_err());
239        assert!(BigInteger::from_str("123\"").is_err());
240        assert!(BigInteger::from_str("123\\n456").is_err());
241    }
242
243    #[test]
244    fn big_decimal_rejects_json_injection() {
245        assert!(BigDecimal::from_str("123.45, \"injected\": true").is_err());
246        assert!(BigDecimal::from_str("123.45}").is_err());
247        assert!(BigDecimal::from_str("{\"hacked\": 1.0}").is_err());
248    }
249
250    #[test]
251    fn big_integer_rejects_invalid_chars() {
252        assert!(BigInteger::from_str("abc").is_err());
253        assert!(BigInteger::from_str("123abc").is_err());
254        assert!(BigInteger::from_str("12 34").is_err());
255        assert!(BigInteger::from_str("").is_err());
256    }
257
258    #[test]
259    fn big_integer_rejects_decimal_and_scientific() {
260        // BigInteger should reject decimal points
261        assert!(BigInteger::from_str("123.45").is_err());
262        assert!(BigInteger::from_str("123.0").is_err());
263
264        // BigInteger should reject scientific notation
265        assert!(BigInteger::from_str("1e10").is_err());
266        assert!(BigInteger::from_str("1E10").is_err());
267        assert!(BigInteger::from_str("1.23e10").is_err());
268    }
269
270    #[test]
271    fn big_integer_accepts_signs() {
272        assert!(BigInteger::from_str("+123").is_ok());
273        assert!(BigInteger::from_str("-123").is_ok());
274        assert_eq!(BigInteger::from_str("+123").unwrap().as_ref(), "+123");
275    }
276
277    #[test]
278    fn big_decimal_rejects_invalid_chars() {
279        assert!(BigDecimal::from_str("abc").is_err());
280        assert!(BigDecimal::from_str("123.45abc").is_err());
281        assert!(BigDecimal::from_str("12.34 56").is_err());
282        assert!(BigDecimal::from_str("").is_err());
283    }
284}