diff --git a/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java index 333273e33..a14a9cd6c 100644 --- a/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java +++ b/src/main/java/com/networknt/schema/regex/AllowRegularExpressionFactory.java @@ -19,6 +19,7 @@ import com.networknt.schema.InvalidSchemaException; import com.networknt.schema.Error; +import com.networknt.schema.SchemaContext; /** * {@link RegularExpressionFactory} that allows regular expressions to be used. @@ -34,9 +35,16 @@ public AllowRegularExpressionFactory(RegularExpressionFactory delegate, Predicat @Override public RegularExpression getRegularExpression(String regex) { + return getRegularExpression(regex, null); + } + + @Override + public RegularExpression getRegularExpression(String regex, SchemaContext schemaContext) { if (this.allowed.test(regex)) { // Allowed to delegate - return this.delegate.getRegularExpression(regex); + return schemaContext != null + ? this.delegate.getRegularExpression(regex, schemaContext) + : this.delegate.getRegularExpression(regex); } throw new InvalidSchemaException(Error.builder() .message("Regular expression ''{0}'' is not allowed to be used.").arguments(regex).build()); diff --git a/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java index 72c5fecd1..0158599b4 100644 --- a/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java +++ b/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java @@ -15,6 +15,7 @@ */ package com.networknt.schema.regex; +import com.networknt.schema.SchemaContext; import com.networknt.schema.utils.Classes; /** @@ -45,4 +46,9 @@ public static ECMAScriptRegularExpressionFactory getInstance() { public RegularExpression getRegularExpression(String regex) { return DELEGATE.getRegularExpression(regex); } + + @Override + public RegularExpression getRegularExpression(String regex, SchemaContext schemaContext) { + return DELEGATE.getRegularExpression(regex, schemaContext); + } } diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java index 0ab33cd80..6cbd67a2f 100644 --- a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java +++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java @@ -28,9 +28,13 @@ class GraalJSRegularExpression implements RegularExpression { private final Value function; GraalJSRegularExpression(String regex, GraalJSRegularExpressionContext context) { + this(regex, context, true); + } + + GraalJSRegularExpression(String regex, GraalJSRegularExpressionContext context, boolean unicode) { this.context = context; synchronized(context.getContext()) { - this.function = context.getRegExpBuilder().execute(regex); + this.function = context.getRegExpBuilder(unicode).execute(regex); } } @@ -40,4 +44,4 @@ public boolean matches(String value) { return !function.execute(value).isNull(); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java index 14c63a8af..574bf00c8 100644 --- a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java +++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java @@ -22,12 +22,13 @@ * GraalJSRegularExpressionContext. */ public class GraalJSRegularExpressionContext { - private static final String SOURCE = "pattern => {\n" - + " const regex = new RegExp(pattern, 'u');\n" + private static final String SOURCE = "flags => pattern => {\n" + + " const regex = new RegExp(pattern, flags);\n" + " return text => text.match(regex)\n" + "};"; private final Context context; + private final Value nonUnicodeRegExpBuilder; private final Value regExpBuilder; /** @@ -41,7 +42,9 @@ public class GraalJSRegularExpressionContext { public GraalJSRegularExpressionContext(Context context) { this.context = context; synchronized(this.context) { - this.regExpBuilder = this.context.eval("js", SOURCE); + Value builder = this.context.eval("js", SOURCE); + this.nonUnicodeRegExpBuilder = builder.execute(""); + this.regExpBuilder = builder.execute("u"); } } @@ -63,4 +66,14 @@ public Context getContext() { public Value getRegExpBuilder() { return regExpBuilder; } + + /** + * Gets the RegExp builder. + * + * @param unicode whether the unicode flag should be used + * @return the regexp builder + */ + public Value getRegExpBuilder(boolean unicode) { + return unicode ? regExpBuilder : nonUnicodeRegExpBuilder; + } } diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java index 113da0209..c53b0e8b1 100644 --- a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java +++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java @@ -15,6 +15,9 @@ */ package com.networknt.schema.regex; +import com.networknt.schema.SchemaContext; +import com.networknt.schema.SpecificationVersion; + import org.graalvm.polyglot.Context; /** @@ -62,4 +65,14 @@ public GraalJSRegularExpressionFactory(Context context) { public RegularExpression getRegularExpression(String regex) { return new GraalJSRegularExpression(regex, this.context); } + + @Override + public RegularExpression getRegularExpression(String regex, SchemaContext schemaContext) { + SpecificationVersion specificationVersion = schemaContext != null + ? schemaContext.getDialect().getSpecificationVersion() + : null; + boolean unicode = specificationVersion == null + || specificationVersion.getOrder() >= SpecificationVersion.DRAFT_2019_09.getOrder(); + return new GraalJSRegularExpression(regex, this.context, unicode); + } } diff --git a/src/main/java/com/networknt/schema/regex/RegularExpression.java b/src/main/java/com/networknt/schema/regex/RegularExpression.java index e3cb6950e..830ecb7a5 100644 --- a/src/main/java/com/networknt/schema/regex/RegularExpression.java +++ b/src/main/java/com/networknt/schema/regex/RegularExpression.java @@ -11,7 +11,8 @@ public interface RegularExpression { static RegularExpression compile(String regex, SchemaContext schemaContext) { if (null == regex) return s -> true; - return schemaContext.getSchemaRegistryConfig().getRegularExpressionFactory().getRegularExpression(regex); + return schemaContext.getSchemaRegistryConfig().getRegularExpressionFactory().getRegularExpression(regex, + schemaContext); } -} \ No newline at end of file +} diff --git a/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java index 5aca068bf..1c9f9dc77 100644 --- a/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java +++ b/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java @@ -15,6 +15,8 @@ */ package com.networknt.schema.regex; +import com.networknt.schema.SchemaContext; + /** * Factory for {@link RegularExpression}. */ @@ -26,4 +28,15 @@ public interface RegularExpressionFactory { * @return the regular expression */ RegularExpression getRegularExpression(String regex); + + /** + * Gets a {@link RegularExpression} in the context of the schema being evaluated. + * + * @param regex the regular expression text value + * @param schemaContext the schema context + * @return the regular expression + */ + default RegularExpression getRegularExpression(String regex, SchemaContext schemaContext) { + return getRegularExpression(regex); + } } diff --git a/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java index 54bc3ae38..de44a730a 100644 --- a/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java +++ b/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java @@ -16,6 +16,7 @@ package com.networknt.schema.regex; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -23,6 +24,7 @@ import com.networknt.schema.Error; import com.networknt.schema.InputFormat; +import com.networknt.schema.OutputFormat; import com.networknt.schema.Schema; import com.networknt.schema.SchemaRegistry; import com.networknt.schema.SchemaRegistryConfig; @@ -32,6 +34,19 @@ * RegularExpressionTest. */ public class RegularExpressionTest { + private static final String DRAFT4_REGEX_FORMAT_SCHEMA = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\r\n" + + " \"format\": \"regex\"\r\n" + + "}"; + + private static final String DRAFT7_REGEX_FORMAT_SCHEMA = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"format\": \"regex\"\r\n" + + "}"; + + private static final String LEGACY_IDENTITY_ESCAPE_REGEX = "\"\\\\a\""; + private static final String ISSUE_1248_REGEX = "\"\\\\d+:\\\\[1-9]+|N\""; + @Test public void testInvalidRegexValidatorECMA262() throws Exception { SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() @@ -41,9 +56,44 @@ public void testInvalidRegexValidatorECMA262() throws Exception { Schema schema = schemaRegistry.getSchema("{\r\n" + " \"format\": \"regex\"\r\n" + "}"); - List errors = schema.validate("\"\\\\a\"", InputFormat.JSON, executionContext -> { + List errors = schema.validate(LEGACY_IDENTITY_ESCAPE_REGEX, InputFormat.JSON, executionContext -> { executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); }); assertFalse(errors.isEmpty()); } -} \ No newline at end of file + + @Test + public void testDraft4RegexFormatValidatorUsesNonUnicodeECMA262() { + assertRegexFormatValid(GraalJSRegularExpressionFactory.getInstance(), DRAFT4_REGEX_FORMAT_SCHEMA, + LEGACY_IDENTITY_ESCAPE_REGEX); + assertRegexFormatValid(GraalJSRegularExpressionFactory.getInstance(), DRAFT4_REGEX_FORMAT_SCHEMA, + ISSUE_1248_REGEX); + } + + @Test + public void testDraft7RegexFormatValidatorUsesNonUnicodeECMA262() { + assertRegexFormatValid(GraalJSRegularExpressionFactory.getInstance(), DRAFT7_REGEX_FORMAT_SCHEMA, + ISSUE_1248_REGEX); + } + + @Test + public void testDelegatingRegularExpressionFactoriesPreserveSchemaContext() { + assertRegexFormatValid(ECMAScriptRegularExpressionFactory.getInstance(), DRAFT4_REGEX_FORMAT_SCHEMA, + ISSUE_1248_REGEX); + assertRegexFormatValid(new AllowRegularExpressionFactory(GraalJSRegularExpressionFactory.getInstance(), + regex -> true), DRAFT4_REGEX_FORMAT_SCHEMA, ISSUE_1248_REGEX); + } + + private static void assertRegexFormatValid(RegularExpressionFactory regularExpressionFactory, String schemaText, + String valueText) { + SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() + .regularExpressionFactory(regularExpressionFactory).build(); + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(Dialects.getDraft202012(), + builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); + Schema schema = schemaRegistry.getSchema(schemaText); + boolean valid = schema.validate(valueText, InputFormat.JSON, OutputFormat.BOOLEAN, + executionContext -> executionContext.executionConfig( + executionConfig -> executionConfig.formatAssertionsEnabled(true))); + assertTrue(valid); + } +}