diff --git a/cargo-typify/README.md b/cargo-typify/README.md index 4b6b3408..d63e5f9b 100644 --- a/cargo-typify/README.md +++ b/cargo-typify/README.md @@ -134,7 +134,7 @@ impl std::str::FromStr for Name { if value.len() > 63usize { return Err("longer than 63 characters"); } - if regress::Regex::new("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$") + if regress::Regex::with_flags("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$", "u") .unwrap() .find(value) .is_none() diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 7c1716e7..bc7f6cae 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -875,11 +875,12 @@ impl TypeSpace { Some(validation) => { if let Some(pattern) = &validation.pattern { - let _ = - regress::Regex::new(pattern).map_err(|e| Error::InvalidSchema { + let _ = regress::Regex::with_flags(pattern, "u").map_err(|e| { + Error::InvalidSchema { type_name: type_name.clone().into_option(), reason: format!("invalid pattern '{}' {}", pattern, e), - })?; + } + })?; self.uses_regress = true; } @@ -2192,6 +2193,21 @@ mod tests { assert_eq!(type_space.iter_types().count(), 4); } + #[test] + fn test_unicode_pattern_rejects_identity_escape() { + let schema = serde_json::from_value(json!({ + "title": "InvalidUnicodePattern", + "type": "string", + "pattern": r"\a", + })) + .unwrap(); + + assert!(matches!( + TypeSpace::default().add_root_schema(schema), + Err(Error::InvalidSchema { .. }) + )); + } + #[test] fn test_basic_option_flat() { #[derive(JsonSchema, Schema)] diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 2881f555..d90d0b07 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1596,7 +1596,7 @@ impl TypeEntry { let err = format!("doesn't match pattern \"{}\"", p); quote! { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new(|| { - ::regress::Regex::new(#p).unwrap() + ::regress::Regex::with_flags(#p, "u").unwrap() }); if PATTERN.find(value).is_none() { return Err(#err.into()); diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs index 9880b91b..0b8e065f 100644 --- a/typify-impl/src/util.rs +++ b/typify-impl/src/util.rs @@ -909,7 +909,7 @@ impl StringValidator { .pattern .as_ref() .map(|pattern| { - regress::Regex::new(pattern).map_err(|e| Error::InvalidSchema { + regress::Regex::with_flags(pattern, "u").map_err(|e| Error::InvalidSchema { type_name: type_name.clone().into_option(), reason: format!("invalid pattern '{}' {}", pattern, e), }) @@ -1195,6 +1195,25 @@ mod tests { assert!(!ach.is_valid("Abednego")); } + #[test] + fn test_string_validation_unicode_pattern() { + for (pattern, valid, invalid) in [ + (r"^\p{Letter}+$", "héllo", "123"), + (r"^\u{1F354}$", "🍔", "a"), + ] { + let validator = StringValidator::new( + &Name::Unknown, + Some(&StringValidation { + pattern: Some(pattern.to_string()), + ..Default::default() + }), + ) + .unwrap(); + assert!(validator.is_valid(valid), "{pattern} should match {valid}"); + assert!(!validator.is_valid(invalid)); + } + } + #[test] fn test_string_validation_multi_byte() { // minLength/maxLength count Unicode code points, not UTF-8 bytes. diff --git a/typify-impl/tests/vega.out b/typify-impl/tests/vega.out index 6c3dea2c..72d74e60 100644 --- a/typify-impl/tests/vega.out +++ b/typify-impl/tests/vega.out @@ -7165,7 +7165,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -7451,7 +7453,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -7779,7 +7783,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -8112,7 +8118,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -8635,7 +8643,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -8921,7 +8931,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -9249,7 +9261,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -9582,7 +9596,9 @@ impl ::std::convert::TryFrom<&str> type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^(date|utc):.*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(date|utc):.*$\"".into()); } @@ -11176,7 +11192,7 @@ impl ::std::convert::TryFrom<&str> for EncodeKey { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^.+$").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags("^.+$", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^.+$\"".into()); } @@ -42548,7 +42564,7 @@ impl ::std::convert::TryFrom<&str> for TitleObjectEncodeSubtype0Key { fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new(|| { - ::regress::Regex::new("^(?!interactive|name|style).+$").unwrap() + ::regress::Regex::with_flags("^(?!interactive|name|style).+$", "u").unwrap() }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^(?!interactive|name|style).+$\"".into()); diff --git a/typify/tests/schemas/id-or-name.rs b/typify/tests/schemas/id-or-name.rs index 1d3f2b13..0069c88c 100644 --- a/typify/tests/schemas/id-or-name.rs +++ b/typify/tests/schemas/id-or-name.rs @@ -177,7 +177,7 @@ impl ::std::convert::TryFrom<&str> for IdOrYoloYolo { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new(".*").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags(".*", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \".*\"".into()); } @@ -232,7 +232,7 @@ impl ::std::convert::TryFrom<&str> for Name { } static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new( || { - :: regress :: Regex :: new ("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$") . unwrap () + :: regress :: Regex :: with_flags ("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$" , "u") . unwrap () }, ); if PATTERN.find(value).is_none() { diff --git a/typify/tests/schemas/merged-schemas.rs b/typify/tests/schemas/merged-schemas.rs index 25b39cf2..ba12f242 100644 --- a/typify/tests/schemas/merged-schemas.rs +++ b/typify/tests/schemas/merged-schemas.rs @@ -393,7 +393,7 @@ impl ::std::convert::TryFrom<&str> for TriplePattern { fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new(|| { - ::regress::Regex::new("(?=^[a-z].+$)(?=^.{4,8}$)(?=.+[a-z]$)").unwrap() + ::regress::Regex::with_flags("(?=^[a-z].+$)(?=^.{4,8}$)(?=.+[a-z]$)", "u").unwrap() }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"(?=^[a-z].+$)(?=^.{4,8}$)(?=.+[a-z]$)\"".into()); diff --git a/typify/tests/schemas/property-pattern.rs b/typify/tests/schemas/property-pattern.rs index 54652f5e..6f07646a 100644 --- a/typify/tests/schemas/property-pattern.rs +++ b/typify/tests/schemas/property-pattern.rs @@ -36,7 +36,9 @@ impl ::std::convert::TryFrom<&str> for TestGrammarForPatternPropertiesRulesKey { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("^[a-zA-Z_]\\w*$").unwrap()); + ::std::sync::LazyLock::new(|| { + ::regress::Regex::with_flags("^[a-zA-Z_]\\w*$", "u").unwrap() + }); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"^[a-zA-Z_]\\w*$\"".into()); } diff --git a/typify/tests/schemas/types-with-more-impls.rs b/typify/tests/schemas/types-with-more-impls.rs index c996bee4..69a81957 100644 --- a/typify/tests/schemas/types-with-more-impls.rs +++ b/typify/tests/schemas/types-with-more-impls.rs @@ -24,7 +24,7 @@ impl ::std::convert::TryFrom<&str> for PatternString { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new("xx").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags("xx", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \"xx\"".into()); } diff --git a/typify/tests/schemas/various-enums-json-schema.rs b/typify/tests/schemas/various-enums-json-schema.rs index 57540b41..862538ae 100644 --- a/typify/tests/schemas/various-enums-json-schema.rs +++ b/typify/tests/schemas/various-enums-json-schema.rs @@ -435,7 +435,7 @@ impl ::std::convert::TryFrom<&str> for Ipv4Net { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new(".*").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags(".*", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \".*\"".into()); } @@ -496,7 +496,7 @@ impl ::std::convert::TryFrom<&str> for Ipv6Net { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new(".*").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags(".*", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \".*\"".into()); } diff --git a/typify/tests/schemas/various-enums.rs b/typify/tests/schemas/various-enums.rs index 38a513f6..5e819ea7 100644 --- a/typify/tests/schemas/various-enums.rs +++ b/typify/tests/schemas/various-enums.rs @@ -386,7 +386,7 @@ impl ::std::convert::TryFrom<&str> for Ipv4Net { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new(".*").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags(".*", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \".*\"".into()); } @@ -437,7 +437,7 @@ impl ::std::convert::TryFrom<&str> for Ipv6Net { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { static PATTERN: ::std::sync::LazyLock<::regress::Regex> = - ::std::sync::LazyLock::new(|| ::regress::Regex::new(".*").unwrap()); + ::std::sync::LazyLock::new(|| ::regress::Regex::with_flags(".*", "u").unwrap()); if PATTERN.find(value).is_none() { return Err("doesn't match pattern \".*\"".into()); }