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//! # Accepted input
12//!
13//! `FromStr` validates the *structure* of the input, not just its character
14//! set. `BigInteger` uses the JSON integer grammar with one deliberate
15//! relaxation (leading zeros), while `BigDecimal` accepts the forms supported
16//! by the CBOR decimal-fraction implementation:
17//!
18//! ```text
19//! BigInteger := '-'? DIGIT+
20//! BigDecimal := ('+' | '-')? ( DIGIT+ ( '.' DIGIT* )? | '.' DIGIT+ )
21//!               ( ('e' | 'E') ('+' | '-')? DIGIT+ )?
22//! ```
23//!
24//! Both types accept `"-12"` and `"00123"`; `BigDecimal` also accepts `"+5"`,
25//! `".5"`, `"5."`, and `"1.23E-10"`. Neither type accepts `"1.2.3"`, `"--5"`,
26//! `"1e"`, `"e10"`, or `"-"`.
27//! Decimal exponents and the resulting scale must also fit the supported range;
28//! otherwise parsing returns [`BigNumberError::ExponentOutOfRange`].
29//!
30//! Leading zeros are accepted because they have exactly one numeric reading and
31//! were accepted by previously released versions. They are *not* valid RFC 8259
32//! JSON numbers, so a protocol that emits an arbitrary-precision value as a raw
33//! JSON number rejects them at the wire boundary instead.
34
35/// Error type for BigInteger and BigDecimal parsing.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum BigNumberError {
39    /// The input string is not a valid number format.
40    InvalidFormat(String),
41    /// The number's exponent is outside the supported range.
42    ExponentOutOfRange(String),
43}
44
45impl std::fmt::Display for BigNumberError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            BigNumberError::InvalidFormat(s) => write!(f, "invalid number format: {s}"),
49            BigNumberError::ExponentOutOfRange(s) => {
50                write!(f, "number exponent is outside the supported range: {s}")
51            }
52        }
53    }
54}
55
56impl std::error::Error for BigNumberError {}
57
58/// `true` iff `s` is non-empty and consists entirely of ASCII digits.
59fn is_ascii_digits(s: &str) -> bool {
60    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
61}
62
63/// Validates that a string is a valid `BigInteger`: `'-'? DIGIT+`.
64///
65/// A leading `-` is optional; a leading `+` is not accepted. At least one digit
66/// is mandatory, so a bare sign (`"-"`, `"+"`) and the empty string are
67/// rejected, as are repeated signs (`"--5"`), decimal points and exponents.
68///
69/// Leading zeros (`"00123"`) are accepted — see the module docs.
70fn is_valid_big_integer(s: &str) -> bool {
71    is_ascii_digits(s.strip_prefix('-').unwrap_or(s))
72}
73
74/// Validates that a string is a supported `BigDecimal`.
75///
76/// The coefficient accepts an optional leading sign and requires at least one
77/// digit across its integer and fractional parts. Consequently, `"+5"`,
78/// `".5"`, `"-.5"`, and `"5."` are valid, while `"."`, `"--5"`, and
79/// `"1.2.3"` are not. A scientific-notation exponent may also have a sign but
80/// must contain digits.
81///
82/// The exponent and the scale derived from the fraction length and exponent
83/// must fit the range supported by the CBOR decimal-fraction implementation.
84fn validate_big_decimal(s: &str) -> Result<(), BigNumberError> {
85    let invalid_format = || BigNumberError::InvalidFormat(s.to_string());
86    let exponent_out_of_range = || BigNumberError::ExponentOutOfRange(s.to_string());
87
88    let (coefficient, exponent) = match s.split_once(['e', 'E']) {
89        Some((coefficient, exponent)) => match exponent.parse::<i128>() {
90            Ok(exponent) => (coefficient, exponent),
91            Err(error)
92                if matches!(
93                    error.kind(),
94                    std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
95                ) =>
96            {
97                return Err(exponent_out_of_range());
98            }
99            Err(_) => return Err(invalid_format()),
100        },
101        None => (s, 0),
102    };
103
104    let coefficient = coefficient.strip_prefix(['-', '+']).unwrap_or(coefficient);
105    let (integer, fraction) = coefficient.split_once('.').unwrap_or((coefficient, ""));
106
107    if (integer.is_empty() && fraction.is_empty())
108        || !integer
109            .bytes()
110            .chain(fraction.bytes())
111            .all(|byte| byte.is_ascii_digit())
112    {
113        return Err(invalid_format());
114    }
115
116    match (fraction.len() as i128).checked_sub(exponent) {
117        Some(scale) if (-(i64::MAX as i128)..=i64::MAX as i128).contains(&scale) => Ok(()),
118        _ => Err(exponent_out_of_range()),
119    }
120}
121
122/// A BigInteger represented as a string.
123///
124/// This type does not perform arithmetic operations. Users should parse the string
125/// with their preferred big integer library.
126///
127/// See the [module docs](self) for the grammar accepted by `FromStr`.
128#[derive(Debug, Clone, PartialEq, Eq, Hash)]
129#[cfg_attr(
130    all(aws_sdk_unstable, feature = "serde-serialize"),
131    derive(serde::Serialize)
132)]
133pub struct BigInteger(String);
134
135#[cfg(all(aws_sdk_unstable, feature = "serde-deserialize"))]
136impl<'de> serde::Deserialize<'de> for BigInteger {
137    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138    where
139        D: serde::Deserializer<'de>,
140    {
141        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
142        value.parse().map_err(serde::de::Error::custom)
143    }
144}
145
146impl Default for BigInteger {
147    fn default() -> Self {
148        Self("0".to_string())
149    }
150}
151
152impl std::str::FromStr for BigInteger {
153    type Err = BigNumberError;
154
155    fn from_str(s: &str) -> Result<Self, Self::Err> {
156        if !is_valid_big_integer(s) {
157            return Err(BigNumberError::InvalidFormat(s.to_string()));
158        }
159        Ok(Self(s.to_string()))
160    }
161}
162
163impl AsRef<str> for BigInteger {
164    fn as_ref(&self) -> &str {
165        &self.0
166    }
167}
168
169/// A big decimal represented as a string.
170///
171/// This type does not perform arithmetic operations. Users should parse the string
172/// with their preferred big decimal library.
173///
174/// See the [module docs](self) for the grammar accepted by `FromStr`.
175#[derive(Debug, Clone, PartialEq, Eq, Hash)]
176#[cfg_attr(
177    all(aws_sdk_unstable, feature = "serde-serialize"),
178    derive(serde::Serialize)
179)]
180pub struct BigDecimal(String);
181
182#[cfg(all(aws_sdk_unstable, feature = "serde-deserialize"))]
183impl<'de> serde::Deserialize<'de> for BigDecimal {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
189        value.parse().map_err(serde::de::Error::custom)
190    }
191}
192
193impl Default for BigDecimal {
194    fn default() -> Self {
195        Self("0.0".to_string())
196    }
197}
198
199impl std::str::FromStr for BigDecimal {
200    type Err = BigNumberError;
201
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        validate_big_decimal(s)?;
204        Ok(Self(s.to_string()))
205    }
206}
207
208impl AsRef<str> for BigDecimal {
209    fn as_ref(&self) -> &str {
210        &self.0
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use std::str::FromStr;
218
219    #[test]
220    fn big_integer_basic() {
221        let bi = BigInteger::from_str("12345678901234567890").unwrap();
222        assert_eq!(bi.as_ref(), "12345678901234567890");
223    }
224
225    #[test]
226    fn big_integer_default() {
227        let bi = BigInteger::default();
228        assert_eq!(bi.as_ref(), "0");
229        // The default must itself be parseable.
230        assert_eq!(BigInteger::from_str(bi.as_ref()).unwrap(), bi);
231    }
232
233    #[test]
234    fn big_decimal_basic() {
235        let bd = BigDecimal::from_str("123.456789").unwrap();
236        assert_eq!(bd.as_ref(), "123.456789");
237    }
238
239    #[test]
240    fn big_decimal_default() {
241        let bd = BigDecimal::default();
242        assert_eq!(bd.as_ref(), "0.0");
243        assert_eq!(BigDecimal::from_str(bd.as_ref()).unwrap(), bd);
244    }
245
246    #[test]
247    fn big_integer_negative() {
248        let bi = BigInteger::from_str("-12345").unwrap();
249        assert_eq!(bi.as_ref(), "-12345");
250    }
251
252    #[test]
253    fn big_decimal_scientific() {
254        let bd = BigDecimal::from_str("1.23e10").unwrap();
255        assert_eq!(bd.as_ref(), "1.23e10");
256
257        let bd = BigDecimal::from_str("1.23E-10").unwrap();
258        assert_eq!(bd.as_ref(), "1.23E-10");
259    }
260
261    #[test]
262    fn big_decimal_accepts_supported_formats() {
263        for value in ["0", "+5", "-0.0", ".5", "-.5", "5.", "1.5E+3"] {
264            assert!(BigDecimal::from_str(value).is_ok(), "{value}");
265        }
266    }
267
268    #[test]
269    fn big_decimal_rejects_malformed_values() {
270        for value in [
271            "1.2.3", "-", "+", ".", "e", "E", "1e", "1e+", "--5", "1-2", "12-34", "..", "1.2e3.4",
272            "+-1",
273        ] {
274            assert_eq!(
275                BigDecimal::from_str(value),
276                Err(BigNumberError::InvalidFormat(value.to_string())),
277                "{value}"
278            );
279        }
280    }
281
282    #[test]
283    fn big_decimal_rejects_out_of_range_exponents() {
284        for value in [
285            "1E99999999999999999999",
286            "1E-99999999999999999999",
287            "1e9223372036854775808",
288            "1e-9223372036854775808",
289            "1e999999999999999999999999999999999999999",
290        ] {
291            assert_eq!(
292                BigDecimal::from_str(value),
293                Err(BigNumberError::ExponentOutOfRange(value.to_string())),
294                "{value}"
295            );
296        }
297    }
298
299    #[test]
300    fn big_integer_rejects_json_injection() {
301        // Reject strings with JSON special characters
302        assert!(BigInteger::from_str("123, \"injected\": true").is_err());
303        assert!(BigInteger::from_str("123}").is_err());
304        assert!(BigInteger::from_str("{\"hacked\": 1}").is_err());
305        assert!(BigInteger::from_str("123\"").is_err());
306        assert!(BigInteger::from_str("123\\n456").is_err());
307    }
308
309    #[test]
310    fn big_decimal_rejects_json_injection() {
311        assert!(BigDecimal::from_str("123.45, \"injected\": true").is_err());
312        assert!(BigDecimal::from_str("123.45}").is_err());
313        assert!(BigDecimal::from_str("{\"hacked\": 1.0}").is_err());
314    }
315
316    #[test]
317    fn big_integer_rejects_invalid_chars() {
318        assert!(BigInteger::from_str("abc").is_err());
319        assert!(BigInteger::from_str("123abc").is_err());
320        assert!(BigInteger::from_str("12 34").is_err());
321        assert!(BigInteger::from_str("").is_err());
322    }
323
324    #[test]
325    fn big_integer_rejects_decimal_and_scientific() {
326        // BigInteger should reject decimal points
327        assert!(BigInteger::from_str("123.45").is_err());
328        assert!(BigInteger::from_str("123.0").is_err());
329
330        // BigInteger should reject scientific notation
331        assert!(BigInteger::from_str("1e10").is_err());
332        assert!(BigInteger::from_str("1E10").is_err());
333        assert!(BigInteger::from_str("1.23e10").is_err());
334    }
335
336    #[test]
337    fn big_decimal_rejects_invalid_chars() {
338        assert!(BigDecimal::from_str("abc").is_err());
339        assert!(BigDecimal::from_str("123.45abc").is_err());
340        assert!(BigDecimal::from_str("12.34 56").is_err());
341        assert!(BigDecimal::from_str("").is_err());
342    }
343
344    // --- Structural grammar -----------------------------------------------------
345
346    #[test]
347    fn big_integer_rejects_a_leading_plus() {
348        // `+123` is not a JSON integer and is not accepted by BigInteger.
349        // BigDecimal intentionally accepts a leading plus sign.
350        assert!(BigInteger::from_str("+123").is_err());
351        assert!(BigInteger::from_str("+0").is_err());
352    }
353
354    #[test]
355    fn big_numbers_reject_bare_and_repeated_signs() {
356        for bad in ["-", "+", "--5", "++5", "-+5", "+-5", "5-", "5+"] {
357            assert!(
358                BigInteger::from_str(bad).is_err(),
359                "BigInteger::from_str({bad:?}) should fail"
360            );
361            assert!(
362                BigDecimal::from_str(bad).is_err(),
363                "BigDecimal::from_str({bad:?}) should fail"
364            );
365        }
366    }
367
368    #[test]
369    fn big_decimal_rejects_malformed_separators_and_exponents() {
370        for bad in [
371            "1..2",   // repeated point
372            "1.2.3",  // repeated point
373            "0.0.0",  // repeated point
374            "1e",     // no exponent digits
375            "1E",     // no exponent digits
376            "1e+",    // sign but no exponent digits
377            "1e-",    // sign but no exponent digits
378            "e10",    // no mantissa
379            "-e10",   // no mantissa after the sign
380            "1e2e3",  // repeated exponent
381            "1.5e",   // no exponent digits
382            "1e2.5",  // fractional exponent
383            "1e 2",   // space inside the exponent
384            "1 .5",   // space inside the mantissa
385            ".",      // point only
386            "1.2e3e", // trailing exponent marker
387        ] {
388            assert!(
389                BigDecimal::from_str(bad).is_err(),
390                "BigDecimal::from_str({bad:?}) should fail"
391            );
392        }
393    }
394
395    #[test]
396    fn big_decimal_accepts_additional_supported_formats() {
397        for good in [
398            "0",
399            "-0",
400            "123",
401            "-123",
402            "1.5",
403            "-0.5",
404            "1e3",
405            "1E3",
406            "1e+9",
407            "1e-9",
408            "1.23e10",
409            "1.23E-10",
410            "10.0",
411            "5e-3",
412            "0.123456789012345678901234567890",
413            "12345678901234567890.123",
414            "1.234e500",
415        ] {
416            let bd = BigDecimal::from_str(good)
417                .unwrap_or_else(|e| panic!("BigDecimal::from_str({good:?}) should succeed: {e}"));
418            // The stored text is exactly the input: parsing never rewrites it.
419            assert_eq!(bd.as_ref(), good);
420        }
421    }
422
423    #[test]
424    fn big_numbers_still_accept_leading_zeros() {
425        // Retained for compatibility with previously released versions:
426        // leading zeros have one numeric reading. They are *not* valid RFC
427        // 8259 JSON numbers, and are rejected separately when an
428        // arbitrary-precision value is emitted as a raw JSON number.
429        for good in ["00123", "007", "-01", "0000", "-0000"] {
430            let bi = BigInteger::from_str(good)
431                .unwrap_or_else(|e| panic!("BigInteger::from_str({good:?}) should succeed: {e}"));
432            assert_eq!(bi.as_ref(), good);
433            let bd = BigDecimal::from_str(good)
434                .unwrap_or_else(|e| panic!("BigDecimal::from_str({good:?}) should succeed: {e}"));
435            assert_eq!(bd.as_ref(), good);
436        }
437        for good in ["00.1", "-01.5", "007e2"] {
438            let bd = BigDecimal::from_str(good)
439                .unwrap_or_else(|e| panic!("BigDecimal::from_str({good:?}) should succeed: {e}"));
440            assert_eq!(bd.as_ref(), good);
441        }
442    }
443
444    #[test]
445    fn big_integer_accepts_only_digits_after_an_optional_minus() {
446        for good in ["0", "-0", "1", "-1", "12345678901234567890"] {
447            assert!(
448                BigInteger::from_str(good).is_ok(),
449                "BigInteger::from_str({good:?}) should succeed"
450            );
451        }
452        for bad in ["1_000", "1,000", "0x1f", " 1", "1 ", "\t1", "1\n"] {
453            assert!(
454                BigInteger::from_str(bad).is_err(),
455                "BigInteger::from_str({bad:?}) should fail"
456            );
457        }
458    }
459
460    #[test]
461    fn big_number_error_reports_the_offending_input() {
462        let err = BigInteger::from_str("+123").unwrap_err();
463        assert_eq!(err, BigNumberError::InvalidFormat("+123".to_string()));
464        assert!(err.to_string().contains("+123"));
465    }
466
467    #[cfg(all(aws_sdk_unstable, feature = "serde-deserialize"))]
468    #[test]
469    fn serde_deserialization_preserves_big_number_invariants() {
470        let integer: BigInteger = serde_json::from_str(r#""00123""#).unwrap();
471        assert_eq!(integer.as_ref(), "00123");
472        assert!(serde_json::from_str::<BigInteger>(r#""+123""#).is_err());
473
474        for value in ["+5", ".5", "-.5", "5.", "1.5E+3"] {
475            let json = format!(r#""{value}""#);
476            let decimal: BigDecimal = serde_json::from_str(&json).unwrap();
477            assert_eq!(decimal.as_ref(), value);
478        }
479
480        for value in ["1.2.3", "--5", "1e"] {
481            let json = format!(r#""{value}""#);
482            assert!(serde_json::from_str::<BigDecimal>(&json).is_err());
483        }
484
485        let error = serde_json::from_str::<BigDecimal>(r#""1E99999999999999999999""#).unwrap_err();
486        assert!(error.to_string().contains("outside the supported range"));
487    }
488}