fix: Precision loss in `minimum`, `maximum`, `exclusiveMinimum` and `exclusiveMaximum` validators

This commit is contained in:
Dmitry Dygalo 2020-05-27 14:11:14 +02:00 committed by Dmitry Dygalo
parent 093ea199ea
commit f169c8e527
7 changed files with 296 additions and 138 deletions

View File

@ -36,6 +36,7 @@
- Wrong implementation of `is_valid` for `additionalProperties: false` keyword case. [#61](https://github.com/Stranger6667/jsonschema-rs/pull/61)
- Possible panic due to type conversion in some numeric validators. [#72](https://github.com/Stranger6667/jsonschema-rs/pull/72)
- Precision loss in `minimum`, `maximum`, `exclusiveMinimum` and `exclusiveMaximum` validators. [#84](https://github.com/Stranger6667/jsonschema-rs/issues/84)
## [0.2.0] - 2020-03-30

View File

@ -24,6 +24,7 @@ chrono = ">= 0.2"
rayon = "1"
reqwest = { version = ">= 0.10", features = ["blocking", "json"]}
parking_lot = ">= 0.1"
num-cmp = ">= 0.1"
[dev-dependencies]
paste = ">= 0.1"

View File

@ -3,55 +3,92 @@ use crate::{
compilation::{CompilationContext, JSONSchema},
error::{error, no_error, CompilationError, ErrorIterator, ValidationError},
};
use num_cmp::NumCmp;
use serde_json::{Map, Value};
pub struct ExclusiveMaximumValidator {
pub struct ExclusiveMaximumU64Validator {
limit: u64,
}
pub struct ExclusiveMaximumI64Validator {
limit: i64,
}
pub struct ExclusiveMaximumF64Validator {
limit: f64,
}
impl ExclusiveMaximumValidator {
#[inline]
pub(crate) fn compile(schema: &Value) -> CompilationResult {
if let Value::Number(limit) = schema {
return Ok(Box::new(ExclusiveMaximumValidator {
limit: limit.as_f64().expect("Always valid"),
}));
}
Err(CompilationError::SchemaError)
}
}
macro_rules! validate {
($validator: ty) => {
impl Validate for $validator {
fn validate<'a>(
&self,
schema: &'a JSONSchema,
instance: &'a Value,
) -> ErrorIterator<'a> {
if self.is_valid(schema, instance) {
no_error()
} else {
error(ValidationError::exclusive_maximum(
instance,
self.limit as f64,
)) // do not cast
}
}
impl Validate for ExclusiveMaximumValidator {
fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item >= self.limit {
return error(ValidationError::exclusive_maximum(instance, self.limit));
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
return if let Some(item) = item.as_u64() {
NumCmp::num_lt(item, self.limit)
} else if let Some(item) = item.as_i64() {
NumCmp::num_lt(item, self.limit)
} else {
let item = item.as_f64().expect("Always valid");
NumCmp::num_lt(item, self.limit)
};
}
true
}
fn name(&self) -> String {
format!("exclusiveMaximum: {}", self.limit)
}
}
no_error()
}
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item >= self.limit {
return false;
}
}
true
}
fn name(&self) -> String {
format!("exclusiveMaximum: {}", self.limit)
}
};
}
validate!(ExclusiveMaximumU64Validator);
validate!(ExclusiveMaximumI64Validator);
validate!(ExclusiveMaximumF64Validator);
#[inline]
pub fn compile(
_: &Map<String, Value>,
schema: &Value,
_: &CompilationContext,
) -> Option<CompilationResult> {
Some(ExclusiveMaximumValidator::compile(schema))
if let Value::Number(limit) = schema {
return if let Some(limit) = limit.as_u64() {
Some(Ok(Box::new(ExclusiveMaximumU64Validator { limit })))
} else if let Some(limit) = limit.as_i64() {
Some(Ok(Box::new(ExclusiveMaximumI64Validator { limit })))
} else {
let limit = limit.as_f64().expect("Always valid");
Some(Ok(Box::new(ExclusiveMaximumF64Validator { limit })))
};
}
Some(Err(CompilationError::SchemaError))
}
#[cfg(test)]
mod tests {
use crate::tests_util;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(json!({"exclusiveMaximum": 1u64 << 54}), json!(1u64 << 54))]
#[test_case(json!({"exclusiveMaximum": 1i64 << 54}), json!(1i64 << 54))]
#[test_case(json!({"exclusiveMaximum": 1u64 << 54}), json!(1u64 << 54 + 1))]
#[test_case(json!({"exclusiveMaximum": 1i64 << 54}), json!(1i64 << 54 + 1))]
fn is_not_valid(schema: Value, instance: Value) {
tests_util::is_not_valid(schema, instance)
}
}

View File

@ -3,53 +3,92 @@ use crate::{
compilation::{CompilationContext, JSONSchema},
error::{error, no_error, CompilationError, ErrorIterator, ValidationError},
};
use num_cmp::NumCmp;
use serde_json::{Map, Value};
pub struct ExclusiveMinimumValidator {
pub struct ExclusiveMinimumU64Validator {
limit: u64,
}
pub struct ExclusiveMinimumI64Validator {
limit: i64,
}
pub struct ExclusiveMinimumF64Validator {
limit: f64,
}
impl ExclusiveMinimumValidator {
#[inline]
pub(crate) fn compile(schema: &Value) -> CompilationResult {
if let Value::Number(limit) = schema {
let limit = limit.as_f64().expect("Always valid");
return Ok(Box::new(ExclusiveMinimumValidator { limit }));
}
Err(CompilationError::SchemaError)
}
}
macro_rules! validate {
($validator: ty) => {
impl Validate for $validator {
fn validate<'a>(
&self,
schema: &'a JSONSchema,
instance: &'a Value,
) -> ErrorIterator<'a> {
if self.is_valid(schema, instance) {
no_error()
} else {
error(ValidationError::exclusive_minimum(
instance,
self.limit as f64,
)) // do not cast
}
}
impl Validate for ExclusiveMinimumValidator {
fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item <= self.limit {
return error(ValidationError::exclusive_minimum(instance, self.limit));
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
return if let Some(item) = item.as_u64() {
NumCmp::num_gt(item, self.limit)
} else if let Some(item) = item.as_i64() {
NumCmp::num_gt(item, self.limit)
} else {
let item = item.as_f64().expect("Always valid");
NumCmp::num_gt(item, self.limit)
};
}
true
}
fn name(&self) -> String {
format!("exclusiveMinimum: {}", self.limit)
}
}
no_error()
}
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item <= self.limit {
return false;
}
}
true
}
fn name(&self) -> String {
format!("exclusiveMinimum: {}", self.limit)
}
};
}
validate!(ExclusiveMinimumU64Validator);
validate!(ExclusiveMinimumI64Validator);
validate!(ExclusiveMinimumF64Validator);
#[inline]
pub fn compile(
_: &Map<String, Value>,
schema: &Value,
_: &CompilationContext,
) -> Option<CompilationResult> {
Some(ExclusiveMinimumValidator::compile(schema))
if let Value::Number(limit) = schema {
return if let Some(limit) = limit.as_u64() {
Some(Ok(Box::new(ExclusiveMinimumU64Validator { limit })))
} else if let Some(limit) = limit.as_i64() {
Some(Ok(Box::new(ExclusiveMinimumI64Validator { limit })))
} else {
let limit = limit.as_f64().expect("Always valid");
Some(Ok(Box::new(ExclusiveMinimumF64Validator { limit })))
};
}
Some(Err(CompilationError::SchemaError))
}
#[cfg(test)]
mod tests {
use crate::tests_util;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(json!({"exclusiveMinimum": 1u64 << 54}), json!(1u64 << 54))]
#[test_case(json!({"exclusiveMinimum": 1i64 << 54}), json!(1i64 << 54))]
#[test_case(json!({"exclusiveMinimum": 1u64 << 54}), json!(1u64 << 54 - 1))]
#[test_case(json!({"exclusiveMinimum": 1i64 << 54}), json!(1i64 << 54 - 1))]
fn is_not_valid(schema: Value, instance: Value) {
tests_util::is_not_valid(schema, instance)
}
}

View File

@ -3,54 +3,87 @@ use crate::{
compilation::{CompilationContext, JSONSchema},
error::{error, no_error, CompilationError, ErrorIterator, ValidationError},
};
use num_cmp::NumCmp;
use serde_json::{Map, Value};
pub struct MaximumValidator {
pub struct MaximumU64Validator {
limit: u64,
}
pub struct MaximumI64Validator {
limit: i64,
}
pub struct MaximumF64Validator {
limit: f64,
}
impl MaximumValidator {
#[inline]
pub(crate) fn compile(schema: &Value) -> CompilationResult {
if let Value::Number(limit) = schema {
let limit = limit.as_f64().expect("Always valid");
return Ok(Box::new(MaximumValidator { limit }));
}
Err(CompilationError::SchemaError)
}
}
macro_rules! validate {
($validator: ty) => {
impl Validate for $validator {
fn validate<'a>(
&self,
schema: &'a JSONSchema,
instance: &'a Value,
) -> ErrorIterator<'a> {
if self.is_valid(schema, instance) {
no_error()
} else {
error(ValidationError::maximum(instance, self.limit as f64)) // do not cast
}
}
impl Validate for MaximumValidator {
fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item > self.limit {
return error(ValidationError::maximum(instance, self.limit));
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
return if let Some(item) = item.as_u64() {
!NumCmp::num_gt(item, self.limit)
} else if let Some(item) = item.as_i64() {
!NumCmp::num_gt(item, self.limit)
} else {
let item = item.as_f64().expect("Always valid");
!NumCmp::num_gt(item, self.limit)
};
}
true
}
fn name(&self) -> String {
format!("maximum: {}", self.limit)
}
}
no_error()
}
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item > self.limit {
return false;
}
}
true
}
fn name(&self) -> String {
format!("maximum: {}", self.limit)
}
};
}
validate!(MaximumU64Validator);
validate!(MaximumI64Validator);
validate!(MaximumF64Validator);
#[inline]
pub fn compile(
_: &Map<String, Value>,
schema: &Value,
_: &CompilationContext,
) -> Option<CompilationResult> {
Some(MaximumValidator::compile(schema))
if let Value::Number(limit) = schema {
return if let Some(limit) = limit.as_u64() {
Some(Ok(Box::new(MaximumU64Validator { limit })))
} else if let Some(limit) = limit.as_i64() {
Some(Ok(Box::new(MaximumI64Validator { limit })))
} else {
let limit = limit.as_f64().expect("Always valid");
Some(Ok(Box::new(MaximumF64Validator { limit })))
};
}
Some(Err(CompilationError::SchemaError))
}
#[cfg(test)]
mod tests {
use crate::tests_util;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(json!({"maximum": 1u64 << 54}), json!(1u64 << 54 + 1))]
#[test_case(json!({"maximum": 1i64 << 54}), json!(1i64 << 54 + 1))]
fn is_not_valid(schema: Value, instance: Value) {
tests_util::is_not_valid(schema, instance)
}
}

View File

@ -3,54 +3,87 @@ use crate::{
compilation::{CompilationContext, JSONSchema},
error::{error, no_error, CompilationError, ErrorIterator, ValidationError},
};
use num_cmp::NumCmp;
use serde_json::{Map, Value};
pub struct MinimumValidator {
pub struct MinimumU64Validator {
limit: u64,
}
pub struct MinimumI64Validator {
limit: i64,
}
pub struct MinimumF64Validator {
limit: f64,
}
impl MinimumValidator {
#[inline]
pub(crate) fn compile(schema: &Value) -> CompilationResult {
if let Value::Number(limit) = schema {
let limit = limit.as_f64().expect("Always valid");
return Ok(Box::new(MinimumValidator { limit }));
}
Err(CompilationError::SchemaError)
}
}
macro_rules! validate {
($validator: ty) => {
impl Validate for $validator {
fn validate<'a>(
&self,
schema: &'a JSONSchema,
instance: &'a Value,
) -> ErrorIterator<'a> {
if self.is_valid(schema, instance) {
no_error()
} else {
error(ValidationError::minimum(instance, self.limit as f64)) // do not cast
}
}
impl Validate for MinimumValidator {
fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item < self.limit {
return error(ValidationError::minimum(instance, self.limit));
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
return if let Some(item) = item.as_u64() {
!NumCmp::num_lt(item, self.limit)
} else if let Some(item) = item.as_i64() {
!NumCmp::num_lt(item, self.limit)
} else {
let item = item.as_f64().expect("Always valid");
!NumCmp::num_lt(item, self.limit)
};
}
true
}
fn name(&self) -> String {
format!("minimum: {}", self.limit)
}
}
no_error()
}
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
if item < self.limit {
return false;
}
}
true
}
fn name(&self) -> String {
format!("minimum: {}", self.limit)
}
};
}
validate!(MinimumU64Validator);
validate!(MinimumI64Validator);
validate!(MinimumF64Validator);
#[inline]
pub fn compile(
_: &Map<String, Value>,
schema: &Value,
_: &CompilationContext,
) -> Option<CompilationResult> {
Some(MinimumValidator::compile(schema))
if let Value::Number(limit) = schema {
return if let Some(limit) = limit.as_u64() {
Some(Ok(Box::new(MinimumU64Validator { limit })))
} else if let Some(limit) = limit.as_i64() {
Some(Ok(Box::new(MinimumI64Validator { limit })))
} else {
let limit = limit.as_f64().expect("Always valid");
Some(Ok(Box::new(MinimumF64Validator { limit })))
};
}
Some(Err(CompilationError::SchemaError))
}
#[cfg(test)]
mod tests {
use crate::tests_util;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(json!({"minimum": 1u64 << 54}), json!(1u64 << 54 - 1))]
#[test_case(json!({"minimum": 1i64 << 54}), json!(1i64 << 54 - 1))]
fn is_not_valid(schema: Value, instance: Value) {
tests_util::is_not_valid(schema, instance)
}
}

View File

@ -46,7 +46,6 @@
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unused_extern_crates,
unused_import_braces,
unused_qualifications,
@ -82,6 +81,21 @@ pub fn is_valid(schema: &Value, instance: &Value) -> bool {
compiled.is_valid(instance)
}
#[cfg(test)]
mod tests_util {
use super::JSONSchema;
use serde_json::Value;
pub fn is_not_valid(schema: Value, instance: Value) {
let compiled = JSONSchema::compile(&schema, None).unwrap();
assert!(!compiled.is_valid(&instance), "{} should not be valid");
assert!(
compiled.validate(&instance).is_err(),
"{} should not be valid"
);
}
}
#[cfg(test)]
mod tests {
use super::*;