From b689ac5b32c29db0a36cf7ad3890e78f0cea96e3 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 5 May 2021 15:00:43 -0400 Subject: [PATCH 1/7] feat: Add support for look-around patterns using fancy-regex --- jsonschema/Cargo.toml | 1 + jsonschema/src/error.rs | 4 +- .../src/keywords/additional_properties.rs | 44 ++++++------------ jsonschema/src/keywords/format.rs | 28 ++++++++---- jsonschema/src/keywords/pattern.rs | 45 +++++++++++-------- jsonschema/src/keywords/pattern_properties.rs | 16 ++++--- 6 files changed, 71 insertions(+), 67 deletions(-) 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..5e7ffda7 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 } } 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..ab165549 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -5,18 +5,16 @@ 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 +41,14 @@ 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(), - )); + if let Ok(is_match) = self.pattern.is_match(item) { + if !is_match { + return error(ValidationError::pattern( + instance_path.into(), + instance, + self.original.clone(), + )); + } } } no_error() @@ -56,8 +56,10 @@ 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; + if let Ok(is_match) = self.pattern.is_match(item) { + if !is_match { + return false; + } } } true @@ -66,12 +68,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,14 +112,16 @@ 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) + .get(0) + .map(|m| m.as_str()) + .expect("index 0 to return the whole match") .trim_start_matches(r"\c") .chars() .next() @@ -147,7 +151,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"\")] 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) ) From bd1f88a72b6a2a05a1c22f87efcc1fb6e4c4ca38 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 6 May 2021 10:48:23 -0400 Subject: [PATCH 2/7] fix: Resolve is_match to false for Err variant --- jsonschema/src/keywords/pattern.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jsonschema/src/keywords/pattern.rs b/jsonschema/src/keywords/pattern.rs index ab165549..9a3aeb37 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -56,11 +56,7 @@ impl Validate for PatternValidator { fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { if let Value::String(item) = instance { - if let Ok(is_match) = self.pattern.is_match(item) { - if !is_match { - return false; - } - } + return self.pattern.is_match(item).unwrap_or(false); } true } From 59ae778bab0c777107fcd34731c81e3a4b668201 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 6 May 2021 12:57:34 -0400 Subject: [PATCH 3/7] fix: Add ValidationError variant for backtrack limit errors --- jsonschema/src/error.rs | 14 ++++++++++++++ jsonschema/src/keywords/pattern.rs | 17 +++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/jsonschema/src/error.rs b/jsonschema/src/error.rs index 5e7ffda7..d6e19261 100644 --- a/jsonschema/src/error.rs +++ b/jsonschema/src/error.rs @@ -98,6 +98,8 @@ 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. @@ -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/pattern.rs b/jsonschema/src/keywords/pattern.rs index 9a3aeb37..8bbc385f 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -41,12 +41,21 @@ impl Validate for PatternValidator { instance_path: &InstancePath, ) -> ErrorIterator<'a> { if let Value::String(item) = instance { - if let Ok(is_match) = self.pattern.is_match(item) { - if !is_match { - return error(ValidationError::pattern( + 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, - self.original.clone(), + e, )); } } From 15755ddf91e6385cfe355101fb9ab89bc6328163 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 6 May 2021 12:58:05 -0400 Subject: [PATCH 4/7] fix: Fix some pre-existing typos in the ValidationErrorKind variant docstrings --- jsonschema/src/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonschema/src/error.rs b/jsonschema/src/error.rs index d6e19261..cbf341f7 100644 --- a/jsonschema/src/error.rs +++ b/jsonschema/src/error.rs @@ -104,9 +104,9 @@ pub enum ValidationErrorKind { 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 }, From 0a980a1689c4182fd9248258404a06cb0d933d57 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 6 May 2021 16:24:48 -0400 Subject: [PATCH 5/7] test: Add negative lookbehind pattern test --- jsonschema/src/keywords/pattern.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jsonschema/src/keywords/pattern.rs b/jsonschema/src/keywords/pattern.rs index 8bbc385f..842e37f9 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -147,6 +147,7 @@ pub(crate) fn compile( #[cfg(test)] mod tests { + use serde_json::{Value, json}; use super::*; use test_case::test_case; @@ -167,4 +168,19 @@ 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, + ) + } } From 332dfc85fa401976d560c95e5e0d0eb423f2f7e8 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 6 May 2021 16:31:53 -0400 Subject: [PATCH 6/7] fix: Fix rustfmt errors --- jsonschema/src/keywords/pattern.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/jsonschema/src/keywords/pattern.rs b/jsonschema/src/keywords/pattern.rs index 842e37f9..19a2f522 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -147,8 +147,8 @@ pub(crate) fn compile( #[cfg(test)] mod tests { - use serde_json::{Value, json}; use super::*; + use serde_json::{json, Value}; use test_case::test_case; #[test_case(r"^[\w\-\.\+]+$", "CC-BY-4.0", true)] @@ -178,9 +178,6 @@ mod tests { let compiled = PatternValidator::compile(&pattern).unwrap(); let schema = JSONSchema::compile(&schema).unwrap(); - assert_eq!( - compiled.is_valid(&schema, &text), - is_matching, - ) + assert_eq!(compiled.is_valid(&schema, &text), is_matching,) } } From aa5c67f66557ba1f89b27f7da3c3ccbf8b55d200 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 6 May 2021 16:48:55 -0400 Subject: [PATCH 7/7] fix: Revert to using index --- jsonschema/src/keywords/pattern.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonschema/src/keywords/pattern.rs b/jsonschema/src/keywords/pattern.rs index 19a2f522..d92635ec 100644 --- a/jsonschema/src/keywords/pattern.rs +++ b/jsonschema/src/keywords/pattern.rs @@ -7,6 +7,8 @@ use crate::{ }; use serde_json::{Map, Value}; +use std::ops::Index; + lazy_static::lazy_static! { // 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"); @@ -124,9 +126,7 @@ fn convert_regex(pattern: &str) -> Result String { // There will be no overflow, because the minimum value is 65 (char 'A') ((captures - .get(0) - .map(|m| m.as_str()) - .expect("index 0 to return the whole match") + .index(0) .trim_start_matches(r"\c") .chars() .next()