Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -549,14 +549,14 @@ class PrimitiveInstantiator(
is BigIntegerShape -> {
val value = data.toString()
rustTemplate(
"<#{BigInteger} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()",
"<#{BigInteger} as ::std::str::FromStr>::from_str(${value.dq()}).expect(\"Invalid string for BigInteger\")",
"BigInteger" to RuntimeType.bigInteger(runtimeConfig),
)
}
is BigDecimalShape -> {
val value = data.toString()
rustTemplate(
"<#{BigDecimal} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()",
"<#{BigDecimal} as ::std::str::FromStr>::from_str(${value.dq()}).expect(\"invalid string for BigDecimal\")",
"BigDecimal" to RuntimeType.bigDecimal(runtimeConfig),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,13 @@ class CborParserGenerator(
// (binary bignum representation), but aws-smithy-cbor doesn't implement these tags yet.
is BigIntegerShape ->
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers:

I have added serialization and parsing logic for the following protocols:

  • JSON
  • CBOR
  • XML
  • AWS Query
  • AWS EC2

Let me know if there are any other protocols.

throw CodegenException(
"BigInteger is not supported with Concise Binary Object Representation (CBOR) protocol",
"BigInteger is not supported with Concise Binary Object Representation (CBOR) protocol. " +
"See https://github.com/smithy-lang/smithy-rs/issues/4473",
)
is BigDecimalShape ->
throw CodegenException(
"BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol",
"BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol. " +
"See https://github.com/smithy-lang/smithy-rs/issues/4473",
)

// Aggregate shapes: https://smithy.io/2.0/spec/aggregate-types.html
Expand Down
34 changes: 19 additions & 15 deletions rust-runtime/aws-smithy-json/src/deserialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,8 @@ impl<'a> JsonTokenIterator<'a> {
Ok(Token::ValueNumber {
offset,
value: if floating {
// For BigInteger/BigDecimal support: Use NaN for f64 validation when the number
// exceeds f64 range (e.g., 1.8e308 > f64::MAX), allowing tokenization to succeed
// while preserving the original string for arbitrary precision types.
Number::Float(
f64::from_str(number_str)
.map_err(|_| self.error_at(start, InvalidNumber))
.map(|f| if f.is_finite() { f } else { f64::NAN })?,
f64::from_str(number_str).map_err(|_| self.error_at(start, InvalidNumber))?,
)
} else if negative {
// If the negative value overflows, then stuff it into an f64
Expand Down Expand Up @@ -666,29 +661,38 @@ mod tests {
}

#[test]
fn out_of_range_floats_use_nan() {
// Values exceeding f64::MAX should tokenize successfully with NaN
// to support BigInteger/BigDecimal arbitrary precision types
let expect_nan = |input| {
fn out_of_range_floats_produce_infinity() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this change to infinity? I thought we chose nan?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I see. the behavior of the parser is Infinity, that's fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is +/- infinity a valid value for Smithy bigInteger or bigDecimal shapes ? (e.g. infinity or -infinity)?

// Values exceeding f64::MAX should tokenize as infinity
// The consumer layer (token.rs) will convert to NaN for BigInteger/BigDecimal
let expect_infinity = |input, should_be_positive| {
let token = json_token_iter(input).next().unwrap().unwrap();
if let Token::ValueNumber {
value: Number::Float(f),
..
} = token
{
assert!(f.is_nan(), "Expected NaN for out-of-range value, got {}", f);
assert!(
f.is_infinite(),
"Expected infinity for out-of-range value, got {}",
f
);
if should_be_positive {
assert!(f.is_sign_positive(), "Expected positive infinity");
} else {
assert!(f.is_sign_negative(), "Expected negative infinity");
}
} else {
panic!("Expected Float token, got {:?}", token);
}
};

// Values > f64::MAX
expect_nan(b"1.8e308");
expect_nan(b"9.9e999");
expect_infinity(b"1.8e308", true);
expect_infinity(b"9.9e999", true);

// Negative values < -f64::MAX
expect_nan(b"-1.8e308");
expect_nan(b"-9.9e999");
expect_infinity(b"-1.8e308", false);
expect_infinity(b"-9.9e999", false);
}

// These cases actually shouldn't parse according to the spec, but it's easier
Expand Down
14 changes: 13 additions & 1 deletion rust-runtime/aws-smithy-json/src/deserialize/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,15 @@ pub fn expect_number_or_null(
) -> Result<Option<Number>, Error> {
match token.transpose()? {
Some(Token::ValueNull { .. }) => Ok(None),
Some(Token::ValueNumber { value, .. }) => Ok(Some(value)),
Some(Token::ValueNumber { value, offset, .. }) => {
// Validate finite numbers - error on infinity/NaN
match value {
Number::Float(f) if !f.is_finite() => {
Err(Error::custom("number must be finite").with_offset(offset.0))
}
_ => Ok(Some(value)),
}
}
Some(Token::ValueString { value, offset }) => match value.to_unescaped() {
Err(err) => Err(Error::custom_source( "expected a valid string, escape was invalid", err).with_offset(offset.0)),
Ok(v) => f64::parse_smithy_primitive(v.as_ref())
Expand Down Expand Up @@ -616,6 +624,10 @@ pub mod test {
panic!("expected nan, found: {not_ok:?}")
}
}

// Test that infinity in ValueNumber token returns an error
let result = expect_number_or_null(value_number(0, Number::Float(f64::INFINITY)));
assert!(result.is_err(), "Expected error for infinity token");
}

#[test]
Expand Down
48 changes: 43 additions & 5 deletions rust-runtime/aws-smithy-types/src/big_number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,28 @@ impl std::fmt::Display for BigNumberError {

impl std::error::Error for BigNumberError {}

/// Validates that a string contains only valid JSON number characters.
/// Prevents JSON injection by rejecting strings with quotes, commas, braces, etc.
fn is_valid_number_string(s: &str) -> bool {
/// Validates that a string is a valid BigInteger format.
/// Only allows digits and an optional leading sign.
fn is_valid_big_integer(s: &str) -> bool {
if s.is_empty() {
return false;
}

let mut chars = s.chars();

// Check first character (can be sign or digit)
match chars.next() {
Some('-') | Some('+') | Some('0'..='9') => {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you really proceed a number with +?

_ => return false,
}

// Rest must be digits only
chars.all(|c| matches!(c, '0'..='9'))
}

/// Validates that a string is a valid BigDecimal format.
/// Allows digits, sign, decimal point, and scientific notation.
fn is_valid_big_decimal(s: &str) -> bool {
if s.is_empty() {
return false;
}
Expand All @@ -54,7 +73,7 @@ impl std::str::FromStr for BigInteger {
type Err = BigNumberError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if !is_valid_number_string(s) {
if !is_valid_big_integer(s) {
return Err(BigNumberError::InvalidFormat(s.to_string()));
}
Ok(Self(s.to_string()))
Expand Down Expand Up @@ -84,7 +103,7 @@ impl std::str::FromStr for BigDecimal {
type Err = BigNumberError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if !is_valid_number_string(s) {
if !is_valid_big_decimal(s) {
return Err(BigNumberError::InvalidFormat(s.to_string()));
}
Ok(Self(s.to_string()))
Expand Down Expand Up @@ -166,6 +185,25 @@ mod tests {
assert!(BigInteger::from_str("").is_err());
}

#[test]
fn big_integer_rejects_decimal_and_scientific() {
// BigInteger should reject decimal points
assert!(BigInteger::from_str("123.45").is_err());
assert!(BigInteger::from_str("123.0").is_err());

// BigInteger should reject scientific notation
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_accepts_signs() {
assert!(BigInteger::from_str("+123").is_ok());
assert!(BigInteger::from_str("-123").is_ok());
assert_eq!(BigInteger::from_str("+123").unwrap().as_ref(), "+123");
}

#[test]
fn big_decimal_rejects_invalid_chars() {
assert!(BigDecimal::from_str("abc").is_err());
Expand Down
Loading