diff --git a/jsonschema/Cargo.toml b/jsonschema/Cargo.toml index b0b2addc..a0c74d0f 100644 --- a/jsonschema/Cargo.toml +++ b/jsonschema/Cargo.toml @@ -24,6 +24,7 @@ url = "2" lazy_static = "1" percent-encoding = "2" regex = "1" +fancy-regex = "0.5" base64 = ">= 0.2" chrono = ">= 0.2" reqwest = { version = ">= 0.10", features = ["blocking", "json"], optional = true} diff --git a/jsonschema/src/error.rs b/jsonschema/src/error.rs index c71c36f6..cbf341f7 100644 --- a/jsonschema/src/error.rs +++ b/jsonschema/src/error.rs @@ -33,9 +33,9 @@ impl fmt::Display for CompilationError { } } -impl From for CompilationError { +impl From for CompilationError { #[inline] - fn from(_: regex::Error) -> Self { + fn from(_: fancy_regex::Error) -> Self { CompilationError::SchemaError } } @@ -98,13 +98,15 @@ pub enum ValidationErrorKind { AdditionalProperties { unexpected: Vec }, /// The input value is not valid under any of the given schemas. AnyOf, + /// Results from a [`fancy_regex::Error::BacktrackLimitExceeded`] variant when matching + BacktrackLimitExceeded { error: fancy_regex::Error }, /// The input value doesn't match expected constant. Constant { expected_value: Value }, /// The input array doesn't contain items conforming to the specified schema. Contains, - /// Ths input value does not respect the defined contentEncoding + /// The input value does not respect the defined contentEncoding ContentEncoding { content_encoding: String }, - /// Ths input value does not respect the defined contentMediaType + /// The input value does not respect the defined contentMediaType ContentMediaType { content_media_type: String }, /// The input value doesn't match any of specified options. Enum { options: Value }, @@ -219,6 +221,17 @@ impl<'a> ValidationError<'a> { kind: ValidationErrorKind::AnyOf, } } + pub(crate) fn backtrack_limit( + instance_path: JSONPointer, + instance: &'a Value, + error: fancy_regex::Error, + ) -> ValidationError<'a> { + ValidationError { + instance_path, + instance: Cow::Borrowed(instance), + kind: ValidationErrorKind::BacktrackLimitExceeded { error }, + } + } pub(crate) fn constant_array( instance_path: JSONPointer, instance: &'a Value, @@ -709,6 +722,7 @@ impl fmt::Display for ValidationError<'_> { ValidationErrorKind::Reqwest { error } => write!(f, "{}", error), ValidationErrorKind::FileNotFound { error } => write!(f, "{}", error), ValidationErrorKind::InvalidURL { error } => write!(f, "{}", error), + ValidationErrorKind::BacktrackLimitExceeded { error } => write!(f, "{}", error), ValidationErrorKind::UnknownReferenceScheme { scheme } => { write!(f, "Unknown scheme: {}", scheme) } diff --git a/jsonschema/src/keywords/additional_properties.rs b/jsonschema/src/keywords/additional_properties.rs index bd4cd779..4c4e0f75 100644 --- a/jsonschema/src/keywords/additional_properties.rs +++ b/jsonschema/src/keywords/additional_properties.rs @@ -14,7 +14,7 @@ use crate::{ validator::Validate, }; use ahash::AHashMap; -use regex::Regex; +use fancy_regex::Regex; use serde_json::{Map, Value}; pub(crate) type PatternedValidators = Vec<(Regex, Validators)>; @@ -106,7 +106,7 @@ macro_rules! is_valid_patterns { let mut has_match = false; for (re, validators) in $patterns { // If there is a match, then the value should match the sub-schema - if re.is_match($property) { + if re.is_match($property).unwrap_or(false) { has_match = true; is_valid_pattern_schema!(validators, $schema, $value) } @@ -516,7 +516,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { for (property, value) in item.iter() { let mut has_match = false; for (re, validators) in &self.patterns { - if re.is_match(property) { + if re.is_match(property).unwrap_or(false) { has_match = true; is_valid_pattern_schema!(validators, schema, value) } @@ -542,7 +542,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { errors.extend( self.patterns .iter() - .filter(|(re, _)| re.is_match(property)) + .filter(|(re, _)| re.is_match(property).unwrap_or(false)) .flat_map(|(_, validators)| { has_match = true; validate!(validators, schema, value, instance_path, property) @@ -631,7 +631,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator { errors.extend( self.patterns .iter() - .filter(|(re, _)| re.is_match(property)) + .filter(|(re, _)| re.is_match(property).unwrap_or(false)) .flat_map(|(_, validators)| { has_match = true; validate!(validators, schema, value, instance_path, property) @@ -735,7 +735,7 @@ impl Validate for AdditionalPropertiesWithPatternsNo // Valid for `properties`, check `patternProperties` for (re, validators) in &self.patterns { // If there is a match, then the value should match the sub-schema - if re.is_match(property) { + if re.is_match(property).unwrap_or(false) { is_valid_pattern_schema!(validators, schema, value) } } @@ -747,7 +747,7 @@ impl Validate for AdditionalPropertiesWithPatternsNo let mut has_match = false; for (re, validators) in &self.patterns { // If there is a match, then the value should match the sub-schema - if re.is_match(property) { + if re.is_match(property).unwrap_or(false) { has_match = true; is_valid_pattern_schema!(validators, schema, value) } @@ -777,7 +777,7 @@ impl Validate for AdditionalPropertiesWithPatternsNo errors.extend( self.patterns .iter() - .filter(|(re, _)| re.is_match(property)) + .filter(|(re, _)| re.is_match(property).unwrap_or(false)) .flat_map(|(_, validators)| { validate!(validators, schema, value, instance_path, name) }), @@ -787,7 +787,7 @@ impl Validate for AdditionalPropertiesWithPatternsNo errors.extend( self.patterns .iter() - .filter(|(re, _)| re.is_match(property)) + .filter(|(re, _)| re.is_match(property).unwrap_or(false)) .flat_map(|(_, validators)| { has_match = true; validate!(validators, schema, value, instance_path, property) @@ -892,7 +892,7 @@ impl Validate // Valid for `properties`, check `patternProperties` for (re, validators) in &self.patterns { // If there is a match, then the value should match the sub-schema - if re.is_match(property) { + if re.is_match(property).unwrap_or(false) { is_valid_pattern_schema!(validators, schema, value) } } @@ -924,7 +924,7 @@ impl Validate errors.extend( self.patterns .iter() - .filter(|(re, _)| re.is_match(property)) + .filter(|(re, _)| re.is_match(property).unwrap_or(false)) .flat_map(|(_, validators)| { validate!(validators, schema, value, instance_path, name) }), @@ -934,7 +934,7 @@ impl Validate errors.extend( self.patterns .iter() - .filter(|(re, _)| re.is_match(property)) + .filter(|(re, _)| re.is_match(property).unwrap_or(false)) .flat_map(|(_, validators)| { has_match = true; validate!(validators, schema, value, instance_path, property) @@ -1070,7 +1070,7 @@ fn compile_patterns( #[cfg(test)] mod tests { - use crate::{tests_util, JSONSchema}; + use crate::tests_util; use serde_json::{json, Value}; use test_case::test_case; @@ -1440,22 +1440,4 @@ mod tests { tests_util::is_not_valid(&schema, instance); tests_util::expect_errors(&schema, instance, expected) } - - #[test] - fn unsupported_regex_does_not_compile() { - // See GH-213 - let schema = json!({ - "type": "object", - "properties": { - "eo:cloud_cover": { - "type": "number", - }, - }, - "patternProperties": { - "^(?!eo:)": {} - }, - "additionalProperties": false - }); - assert!(JSONSchema::compile(&schema).is_err()) - } } diff --git a/jsonschema/src/keywords/format.rs b/jsonschema/src/keywords/format.rs index 3732c7f6..783d7f69 100644 --- a/jsonschema/src/keywords/format.rs +++ b/jsonschema/src/keywords/format.rs @@ -8,7 +8,7 @@ use crate::{ Draft, }; use chrono::{DateTime, NaiveDate}; -use regex::Regex; +use fancy_regex::Regex; use serde_json::{Map, Value}; use std::{net::IpAddr, str::FromStr}; @@ -82,7 +82,9 @@ impl Validate for DateValidator { // Padding with zeroes is ignored by the underlying parser. The most efficient // way to check it will be to use a custom parser that won't ignore zeroes, // but this regex will do the trick and costs ~20% extra time in this validator. - DATE_RE.is_match(item.as_str()) + DATE_RE + .is_match(item.as_str()) + .expect("Simple DATE_RE pattern") } else { false } @@ -219,7 +221,9 @@ impl Validate for IRIReferenceValidator { validate!("iri-reference"); fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - IRI_REFERENCE_RE.is_match(item) + IRI_REFERENCE_RE + .is_match(item) + .expect("Simple IRI_REFERENCE_RE pattern") } else { true } @@ -230,7 +234,9 @@ impl Validate for JSONPointerValidator { validate!("json-pointer"); fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - JSON_POINTER_RE.is_match(item) + JSON_POINTER_RE + .is_match(item) + .expect("Simple JSON_POINTER_RE pattern") } else { true } @@ -252,7 +258,9 @@ impl Validate for RelativeJSONPointerValidator { validate!("relative-json-pointer"); fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - RELATIVE_JSON_POINTER_RE.is_match(item) + RELATIVE_JSON_POINTER_RE + .is_match(item) + .expect("Simple RELATIVE_JSON_POINTER_RE pattern") } else { true } @@ -263,7 +271,7 @@ impl Validate for TimeValidator { validate!("time"); fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - TIME_RE.is_match(item) + TIME_RE.is_match(item).expect("Simple TIME_RE pattern") } else { true } @@ -274,7 +282,9 @@ impl Validate for URIReferenceValidator { validate!("uri-reference"); fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - URI_REFERENCE_RE.is_match(item) + URI_REFERENCE_RE + .is_match(item) + .expect("Simple URI_REFERENCE_RE pattern") } else { true } @@ -285,7 +295,9 @@ impl Validate for URITemplateValidator { validate!("uri-template"); fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - URI_TEMPLATE_RE.is_match(item) + URI_TEMPLATE_RE + .is_match(item) + .expect("Simple URI_TEMPLATE_RE pattern") } else { true } diff --git a/jsonschema/src/keywords/pattern.rs b/jsonschema/src/keywords/pattern.rs index bfab1d38..d92635ec 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -5,18 +5,18 @@ use crate::{ paths::InstancePath, validator::Validate, }; -use regex::{Captures, Regex}; use serde_json::{Map, Value}; use std::ops::Index; lazy_static::lazy_static! { - static ref CONTROL_GROUPS_RE: Regex = Regex::new(r"\\c[A-Za-z]").expect("Is a valid regex"); + // Use regex::Regex here to take advantage of replace_all method not available in fancy_regex::Regex + static ref CONTROL_GROUPS_RE: regex::Regex = regex::Regex::new(r"\\c[A-Za-z]").expect("Is a valid regex"); } pub(crate) struct PatternValidator { original: String, - pattern: Regex, + pattern: fancy_regex::Regex, } impl PatternValidator { @@ -43,12 +43,23 @@ impl Validate for PatternValidator { instance_path: &InstancePath, ) -> ErrorIterator<'a> { if let Value::String(item) = instance { - if !self.pattern.is_match(item) { - return error(ValidationError::pattern( - instance_path.into(), - instance, - self.original.clone(), - )); + match self.pattern.is_match(item) { + Ok(is_match) => { + if !is_match { + return error(ValidationError::pattern( + instance_path.into(), + instance, + self.original.clone(), + )); + } + } + Err(e) => { + return error(ValidationError::backtrack_limit( + instance_path.into(), + instance, + e, + )); + } } } no_error() @@ -56,9 +67,7 @@ impl Validate for PatternValidator { fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - if !self.pattern.is_match(item) { - return false; - } + return self.pattern.is_match(item).unwrap_or(false); } true } @@ -66,12 +75,12 @@ impl Validate for PatternValidator { impl ToString for PatternValidator { fn to_string(&self) -> String { - format!("pattern: {}", self.pattern) + format!("pattern: {:?}", self.pattern) } } // ECMA 262 has differences -fn convert_regex(pattern: &str) -> Result { +fn convert_regex(pattern: &str) -> Result { // replace control chars let new_pattern = CONTROL_GROUPS_RE.replace_all(pattern, replace_control_group); let mut out = String::with_capacity(new_pattern.len()); @@ -110,11 +119,11 @@ fn convert_regex(pattern: &str) -> Result { out.push(current); } } - Regex::new(&out) + fancy_regex::Regex::new(&out) } #[allow(clippy::integer_arithmetic)] -fn replace_control_group(captures: &Captures) -> String { +fn replace_control_group(captures: ®ex::Captures) -> String { // There will be no overflow, because the minimum value is 65 (char 'A') ((captures .index(0) @@ -139,6 +148,7 @@ pub(crate) fn compile( #[cfg(test)] mod tests { use super::*; + use serde_json::{json, Value}; use test_case::test_case; #[test_case(r"^[\w\-\.\+]+$", "CC-BY-4.0", true)] @@ -147,7 +157,10 @@ mod tests { #[test_case(r"\\w", r"\w", true)] fn regex_matches(pattern: &str, text: &str, is_matching: bool) { let compiled = convert_regex(pattern).expect("A valid regex"); - assert_eq!(compiled.is_match(text), is_matching); + assert_eq!( + compiled.is_match(text).expect("A valid pattern"), + is_matching + ); } #[test_case(r"\")] @@ -155,4 +168,16 @@ mod tests { fn invalid_escape_sequences(pattern: &str) { assert!(convert_regex(pattern).is_err()) } + + #[test_case("^(?!eo:)", "eo:bands", false)] + #[test_case("^(?!eo:)", "proj:epsg", true)] + fn negative_lookbehind_match(pattern: &str, text: &str, is_matching: bool) { + let pattern = Value::String(pattern.into()); + let text = Value::String(text.into()); + let schema = json!({}); + + let compiled = PatternValidator::compile(&pattern).unwrap(); + let schema = JSONSchema::compile(&schema).unwrap(); + assert_eq!(compiled.is_valid(&schema, &text), is_matching,) + } } diff --git a/jsonschema/src/keywords/pattern_properties.rs b/jsonschema/src/keywords/pattern_properties.rs index e6d6fc14..5deadb32 100644 --- a/jsonschema/src/keywords/pattern_properties.rs +++ b/jsonschema/src/keywords/pattern_properties.rs @@ -5,7 +5,7 @@ use crate::{ paths::InstancePath, validator::Validate, }; -use regex::Regex; +use fancy_regex::Regex; use serde_json::{Map, Value}; pub(crate) struct PatternPropertiesValidator { @@ -34,7 +34,7 @@ impl Validate for PatternPropertiesValidator { if let Value::Object(item) = instance { self.patterns.iter().all(move |(re, validators)| { item.iter() - .filter(move |(key, _)| re.is_match(key)) + .filter(move |(key, _)| re.is_match(key).unwrap_or(false)) .all(move |(_key, value)| { validators .iter() @@ -58,7 +58,7 @@ impl Validate for PatternPropertiesValidator { .iter() .flat_map(move |(re, validators)| { item.iter() - .filter(move |(key, _)| re.is_match(key)) + .filter(move |(key, _)| re.is_match(key).unwrap_or(false)) .flat_map(move |(key, value)| { let instance_path = instance_path.push(key.to_owned()); validators.iter().flat_map(move |validator| { @@ -80,7 +80,9 @@ impl ToString for PatternPropertiesValidator { "patternProperties: {{{}}}", self.patterns .iter() - .map(|(key, validators)| { format!("{}: {}", key, format_validators(validators)) }) + .map(|(key, validators)| { + format!("{:?}: {}", key, format_validators(validators)) + }) .collect::>() .join(", ") ) @@ -110,7 +112,7 @@ impl Validate for SingleValuePatternPropertiesValidator { fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool { if let Value::Object(item) = instance { item.iter() - .filter(move |(key, _)| self.pattern.is_match(key)) + .filter(move |(key, _)| self.pattern.is_match(key).unwrap_or(false)) .all(move |(_key, value)| { self.validators .iter() @@ -130,7 +132,7 @@ impl Validate for SingleValuePatternPropertiesValidator { if let Value::Object(item) = instance { let errors: Vec<_> = item .iter() - .filter(move |(key, _)| self.pattern.is_match(key)) + .filter(move |(key, _)| self.pattern.is_match(key).unwrap_or(false)) .flat_map(move |(key, value)| { let instance_path = instance_path.push(key.to_owned()); self.validators.iter().flat_map(move |validator| { @@ -148,7 +150,7 @@ impl Validate for SingleValuePatternPropertiesValidator { impl ToString for SingleValuePatternPropertiesValidator { fn to_string(&self) -> String { format!( - "patternProperties: {{{}: {}}}", + "patternProperties: {{{:?}: {}}}", self.pattern, format_validators(&self.validators) )