#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BigNumberError {
InvalidFormat(String),
}
impl std::fmt::Display for BigNumberError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BigNumberError::InvalidFormat(s) => write!(f, "invalid number format: {s}"),
}
}
}
impl std::error::Error for BigNumberError {}
fn is_ascii_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
fn is_valid_big_integer(s: &str) -> bool {
is_ascii_digits(s.strip_prefix('-').unwrap_or(s))
}
fn is_valid_big_decimal(s: &str) -> bool {
let mantissa_and_exp = s.strip_prefix('-').unwrap_or(s);
let (mantissa, exponent) = match mantissa_and_exp.split_once(['e', 'E']) {
Some((mantissa, exponent)) => (mantissa, Some(exponent)),
None => (mantissa_and_exp, None),
};
let mantissa_ok = match mantissa.split_once('.') {
Some((int_part, frac_part)) => is_ascii_digits(int_part) && is_ascii_digits(frac_part),
None => is_ascii_digits(mantissa),
};
let exponent_ok = match exponent {
None => true,
Some(exponent) => is_ascii_digits(exponent.strip_prefix(['+', '-']).unwrap_or(exponent)),
};
mantissa_ok && exponent_ok
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
all(aws_sdk_unstable, feature = "serde-serialize"),
derive(serde::Serialize)
)]
#[cfg_attr(
all(aws_sdk_unstable, feature = "serde-deserialize"),
derive(serde::Deserialize)
)]
pub struct BigInteger(String);
impl Default for BigInteger {
fn default() -> Self {
Self("0".to_string())
}
}
impl std::str::FromStr for BigInteger {
type Err = BigNumberError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !is_valid_big_integer(s) {
return Err(BigNumberError::InvalidFormat(s.to_string()));
}
Ok(Self(s.to_string()))
}
}
impl AsRef<str> for BigInteger {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
all(aws_sdk_unstable, feature = "serde-serialize"),
derive(serde::Serialize)
)]
#[cfg_attr(
all(aws_sdk_unstable, feature = "serde-deserialize"),
derive(serde::Deserialize)
)]
pub struct BigDecimal(String);
impl Default for BigDecimal {
fn default() -> Self {
Self("0.0".to_string())
}
}
impl std::str::FromStr for BigDecimal {
type Err = BigNumberError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !is_valid_big_decimal(s) {
return Err(BigNumberError::InvalidFormat(s.to_string()));
}
Ok(Self(s.to_string()))
}
}
impl AsRef<str> for BigDecimal {
fn as_ref(&self) -> &str {
&self.0
}
}
impl BigDecimal {
const MAX_INTEGER_DIGITS: usize = 1 << 20;
pub(crate) fn to_integer_string(&self) -> Option<String> {
let (negative, rest) = match self.0.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, self.0.as_str()),
};
let (mantissa, exp) = match rest.split_once(['e', 'E']) {
Some((mantissa, exp_str)) => match exp_str.parse::<i64>() {
Ok(exp) => (mantissa, exp),
Err(_) => {
return if exp_str.starts_with('-') {
Some("0".to_string())
} else {
None
}
}
},
None => (rest, 0),
};
let (int_digits, frac_digits) = match mantissa.split_once('.') {
Some((int_digits, frac_digits)) => (int_digits, frac_digits),
None => (mantissa, ""),
};
let point = (int_digits.len() as i64).saturating_add(exp);
let int_part = if point <= 0 {
"0".to_string()
} else {
let point = point as usize;
let mut digits = String::with_capacity(int_digits.len() + frac_digits.len());
digits.push_str(int_digits);
digits.push_str(frac_digits);
if point >= digits.len() {
if point > Self::MAX_INTEGER_DIGITS {
return None;
}
digits.push_str(&"0".repeat(point - digits.len()));
digits
} else {
digits.truncate(point);
digits
}
};
let normalized = match int_part.trim_start_matches('0') {
"" => "0",
trimmed => trimmed,
};
Some(if negative && normalized != "0" {
format!("-{normalized}")
} else {
normalized.to_string()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn big_integer_basic() {
let bi = BigInteger::from_str("12345678901234567890").unwrap();
assert_eq!(bi.as_ref(), "12345678901234567890");
}
#[test]
fn big_integer_default() {
let bi = BigInteger::default();
assert_eq!(bi.as_ref(), "0");
}
#[test]
fn big_decimal_basic() {
let bd = BigDecimal::from_str("123.456789").unwrap();
assert_eq!(bd.as_ref(), "123.456789");
}
#[test]
fn big_decimal_default() {
let bd = BigDecimal::default();
assert_eq!(bd.as_ref(), "0.0");
}
#[test]
fn big_integer_negative() {
let bi = BigInteger::from_str("-12345").unwrap();
assert_eq!(bi.as_ref(), "-12345");
}
#[test]
fn big_decimal_scientific() {
let bd = BigDecimal::from_str("1.23e10").unwrap();
assert_eq!(bd.as_ref(), "1.23e10");
let bd = BigDecimal::from_str("1.23E-10").unwrap();
assert_eq!(bd.as_ref(), "1.23E-10");
}
#[test]
fn big_integer_rejects_json_injection() {
assert!(BigInteger::from_str("123, \"injected\": true").is_err());
assert!(BigInteger::from_str("123}").is_err());
assert!(BigInteger::from_str("{\"hacked\": 1}").is_err());
assert!(BigInteger::from_str("123\"").is_err());
assert!(BigInteger::from_str("123\\n456").is_err());
}
#[test]
fn big_decimal_rejects_json_injection() {
assert!(BigDecimal::from_str("123.45, \"injected\": true").is_err());
assert!(BigDecimal::from_str("123.45}").is_err());
assert!(BigDecimal::from_str("{\"hacked\": 1.0}").is_err());
}
#[test]
fn big_integer_rejects_invalid_chars() {
assert!(BigInteger::from_str("abc").is_err());
assert!(BigInteger::from_str("123abc").is_err());
assert!(BigInteger::from_str("12 34").is_err());
assert!(BigInteger::from_str("").is_err());
}
#[test]
fn big_integer_rejects_decimal_and_scientific() {
assert!(BigInteger::from_str("123.45").is_err());
assert!(BigInteger::from_str("123.0").is_err());
assert!(BigInteger::from_str("1e10").is_err());
assert!(BigInteger::from_str("1E10").is_err());
assert!(BigInteger::from_str("1.23e10").is_err());
}
#[test]
fn big_integer_sign_handling() {
assert!(BigInteger::from_str("-123").is_ok());
assert_eq!(BigInteger::from_str("-123").unwrap().as_ref(), "-123");
assert!(BigInteger::from_str("+123").is_err());
assert!(BigInteger::from_str("--5").is_err());
assert!(BigInteger::from_str("-").is_err());
}
#[test]
fn big_decimal_rejects_invalid_chars() {
assert!(BigDecimal::from_str("abc").is_err());
assert!(BigDecimal::from_str("123.45abc").is_err());
assert!(BigDecimal::from_str("12.34 56").is_err());
assert!(BigDecimal::from_str("").is_err());
}
#[test]
fn big_integer_rejects_malformed_grammar() {
for bad in ["+123", "--5", "-", "1.0", "1e3", "", "1 2", "0x1f"] {
assert!(
BigInteger::from_str(bad).is_err(),
"expected {bad:?} to be rejected"
);
}
for good in ["0", "123", "-123", "00123"] {
assert!(
BigInteger::from_str(good).is_ok(),
"expected {good:?} to be accepted"
);
}
}
#[test]
fn big_decimal_rejects_malformed_grammar() {
for bad in [
"1.2.3", "--5", "1e", "+.", "+1.0", ".5", "1.", "e10", "1e+", "1e2e3", "-", "1..2",
] {
assert!(
BigDecimal::from_str(bad).is_err(),
"expected {bad:?} to be rejected"
);
}
}
#[test]
fn big_decimal_accepts_valid_grammar() {
for good in [
"0", "123", "-123", "1.5", "-0.5", "1.23e10", "1.23E-10", "1e3", "1.0e+9",
] {
assert!(
BigDecimal::from_str(good).is_ok(),
"expected {good:?} to be accepted"
);
}
}
#[test]
fn big_decimal_to_integer_string_truncates_and_expands() {
let cases = [
("0", "0"),
("123", "123"),
("123.99", "123"), ("-123.99", "-123"),
("0.5", "0"),
("-0.5", "0"), ("1.23e10", "12300000000"), ("1.23e1", "12"), ("1.5e1", "15"),
("1e3", "1000"),
("5e-3", "0"), ("-1.23e10", "-12300000000"),
("10.0", "10"),
("00123", "123"), ("1.23E10", "12300000000"), ];
for (input, expected) in cases {
let bd = BigDecimal::from_str(input).unwrap();
assert_eq!(
bd.to_integer_string().as_deref(),
Some(expected),
"to_integer_string({input:?})"
);
}
}
#[test]
fn big_decimal_to_integer_string_guards_pathological_exponent() {
assert_eq!(
BigDecimal::from_str("1e1000000000")
.unwrap()
.to_integer_string(),
None
);
assert_eq!(
BigDecimal::from_str("1e99999999999999999999")
.unwrap()
.to_integer_string(),
None
);
assert_eq!(
BigDecimal::from_str("1e-99999999999999999999")
.unwrap()
.to_integer_string()
.as_deref(),
Some("0")
);
}
}