Skip to content

Commit 597551e

Browse files
committed
fix: multipleOf validation for integer values between 2^53 and i64::MAX with arbitrary-precision feature
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent f99610b commit 597551e

File tree

2 files changed

+19
-8
lines changed

2 files changed

+19
-8
lines changed

crates/jsonschema/src/ext/numeric.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,15 @@ const MAX_SAFE_INTEGER: u64 = 1u64 << 53;
9393

9494
pub(crate) fn is_multiple_of_integer(value: &Number, multiple: f64) -> bool {
9595
// For large integer values beyond 2^53, as_f64() loses precision.
96-
// Use integer arithmetic directly for these cases.
96+
// Use integer arithmetic directly for these cases, but only when the divisor
97+
// itself can be exactly represented in f64 (i.e., <= 2^53). Divisors > 2^53
98+
// may have already lost precision when converted to f64 during schema compilation.
9799
#[cfg(feature = "arbitrary-precision")]
98100
{
99101
if let Some(v) = value.as_u64() {
100102
if v > MAX_SAFE_INTEGER
101103
&& multiple > 0.0
102-
&& multiple <= u64::MAX as f64
104+
&& multiple <= MAX_SAFE_INTEGER as f64
103105
&& multiple.fract() == 0.0
104106
{
105107
return (v % (multiple as u64)) == 0;
@@ -108,7 +110,7 @@ pub(crate) fn is_multiple_of_integer(value: &Number, multiple: f64) -> bool {
108110
if let Some(v) = value.as_i64() {
109111
if v.unsigned_abs() > MAX_SAFE_INTEGER
110112
&& multiple > 0.0
111-
&& multiple <= i64::MAX as f64
113+
&& multiple <= MAX_SAFE_INTEGER as f64
112114
&& multiple.fract() == 0.0
113115
{
114116
return (v % (multiple as i64)) == 0;
@@ -308,13 +310,18 @@ pub(crate) mod bignum {
308310
/// Try to parse a Number as `BigInt` if it's outside i64 range or for compile-time
309311
/// schema values that need exact representation
310312
pub(crate) fn try_parse_bigint(num: &Number) -> Option<BigInt> {
313+
use super::MAX_SAFE_INTEGER;
314+
311315
let num_str = num.as_str();
312316

313-
// Only parse as BigInt if it doesn't fit in i64
314-
// We include u64 values beyond i64::MAX because they can't be accurately
315-
// represented when cast to i64, which is needed for certain operations
316-
if num.as_i64().is_some() {
317-
return None;
317+
// Parse as BigInt if it's beyond 2^53 (where f64 loses precision).
318+
// Values beyond 2^53 need BigInt for accurate arithmetic even if they fit in i64/u64.
319+
// Note: If as_i64() fails but as_u64() succeeds, the value is in [2^63, 2^64-1],
320+
// which is always > 2^53, so no additional check needed for u64.
321+
if let Some(v) = num.as_i64() {
322+
if v.unsigned_abs() <= MAX_SAFE_INTEGER {
323+
return None;
324+
}
318325
}
319326

320327
let has_fraction_or_exponent = num_str.bytes().any(|b| b == b'.' || b == b'e' || b == b'E');

crates/jsonschema/src/keywords/multiple_of.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,10 @@ mod tests {
449449
#[test_case(r#"{"multipleOf": 10}"#, "9223372036854775870", true; "u64 beyond i64 max multiple of 10")]
450450
#[test_case(r#"{"multipleOf": 10}"#, "9223372036854775871", false; "u64 beyond i64 max not multiple of 10")]
451451
#[test_case(r#"{"type": "integer", "minimum": 9223372036854775800, "maximum": 9223372036854775900, "multipleOf": 10}"#, "9223372036854775870", true; "combined schema with u64 beyond i64 max")]
452+
#[test_case(r#"{"multipleOf": 9007199254740992}"#, "18014398509481984", true; "divisor at 2^53 with exact double")]
453+
#[test_case(r#"{"multipleOf": 9007199254740992}"#, "18014398509481985", false; "divisor at 2^53 with non-multiple")]
454+
#[test_case(r#"{"multipleOf": 9007199254740993}"#, "18014398509481986", true; "divisor beyond 2^53 with double value")]
455+
#[test_case(r#"{"multipleOf": 9007199254740993}"#, "9007199254740993", true; "divisor beyond 2^53 with equal value")]
452456
#[test_case(r#"{"multipleOf": 18446744073709551616}"#, "36893488147419103232", true; "large bigint multiple")]
453457
#[test_case(r#"{"multipleOf": 18446744073709551616}"#, "18446744073709551617", false; "large bigint non-multiple")]
454458
#[test_case(r#"{"multipleOf": 18446744073709551616}"#, "100", false; "small int not multiple of large")]

0 commit comments

Comments
 (0)