From a385dbc9a2b8c699525d4932bdf9f5677fcc6603 Mon Sep 17 00:00:00 2001 From: Elsaid Achraf <60660214+asaid-0@users.noreply.github.com> Date: Sat, 3 Apr 2021 02:22:21 +0200 Subject: [PATCH 1/2] Add unicodeRegExp option to pass or remove RegExp unicode flag from pattern and patternProperties --- docs/options.md | 10 +++ lib/core.ts | 5 +- .../applicator/additionalProperties.ts | 6 +- .../applicator/patternProperties.ts | 3 +- lib/vocabularies/code.ts | 6 +- lib/vocabularies/validation/pattern.ts | 8 ++- spec/options/unicodeRegExp.spec.ts | 67 +++++++++++++++++++ 7 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 spec/options/unicodeRegExp.spec.ts diff --git a/docs/options.md b/docs/options.md index b104eb1ee..4eb3f0c71 100644 --- a/docs/options.md +++ b/docs/options.md @@ -42,6 +42,7 @@ const defaultOptions = { schemas: {}, logger: undefined, loadSchema: undefined, // *, function(uri: string): Promise {} + unicodeRegExp: true // options to modify validated data: removeAdditional: false, useDefaults: false, // * @@ -239,6 +240,15 @@ Option values: - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). +### unicodeRegExp + +By default Ajv passes unicode flag "u" to RegExp function when evaluating "pattern" and "patternProperties". See [RegExp.prototype.unicode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode) . + +Option values: + +- `true` (default) - add unicode flag "u". +- `false` - remove unicode flag "u". + ## Advanced options ### meta diff --git a/lib/core.ts b/lib/core.ts index 40aa56d33..469bf05ee 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -93,6 +93,7 @@ export interface CurrentOptions { allowUnionTypes?: boolean validateFormats?: boolean // validation and reporting options: + unicodeRegExp?: boolean $data?: boolean allErrors?: boolean verbose?: boolean @@ -212,7 +213,8 @@ type RequiredInstanceOptions = { | "messages" | "addUsedSchema" | "validateSchema" - | "validateFormats"]: NonNullable + | "validateFormats" + | "unicodeRegExp"]: NonNullable } & {code: InstanceCodeOptions} export type InstanceOptions = Options & RequiredInstanceOptions @@ -239,6 +241,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions { addUsedSchema: o.addUsedSchema ?? true, validateSchema: o.validateSchema ?? true, validateFormats: o.validateFormats ?? true, + unicodeRegExp: o.unicodeRegExp ?? true, } } diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 03ca8080a..5ade451a1 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -60,7 +60,11 @@ const def: CodeKeywordDefinition & AddedKeywordDefinition = { definedProp = nil } if (patProps.length) { - definedProp = or(definedProp, ...patProps.map((p) => _`${usePattern(gen, p)}.test(${key})`)) + const regExpFlags = it.opts.unicodeRegExp ? "u" : "" + definedProp = or( + definedProp, + ...patProps.map((p) => _`${usePattern(gen, p, regExpFlags)}.test(${key})`) + ) } return not(definedProp) } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 25c3e308f..fcfd66943 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -50,7 +50,8 @@ const def: CodeKeywordDefinition = { function validateProperties(pat: string): void { gen.forIn("key", data, (key) => { - gen.if(_`${usePattern(gen, pat)}.test(${key})`, () => { + const regExpFlags = it.opts.unicodeRegExp ? "u" : "" + gen.if(_`${usePattern(gen, pat, regExpFlags)}.test(${key})`, () => { cxt.subschema( { keyword: "patternProperties", diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index 80387c81a..c880d85c8 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -90,11 +90,11 @@ export function callValidateCode( return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` } -export function usePattern(gen: CodeGen, pattern: string): Name { +export function usePattern(gen: CodeGen, pattern: string, regExpFlags = "u"): Name { return gen.scopeValue("pattern", { key: pattern, - ref: new RegExp(pattern, "u"), - code: _`new RegExp(${pattern}, "u")`, + ref: new RegExp(pattern, regExpFlags), + code: _`new RegExp(${pattern}, ${regExpFlags})`, }) } diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 880279f8f..e6aea18d4 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -17,8 +17,12 @@ const def: CodeKeywordDefinition = { $data: true, error, code(cxt: KeywordCxt) { - const {gen, data, $data, schema, schemaCode} = cxt - const regExp = $data ? _`(new RegExp(${schemaCode}, "u"))` : usePattern(gen, schema) // TODO regexp should be wrapped in try/catch + const {gen, data, $data, schema, schemaCode, it} = cxt + const regExpFlags = it.opts.unicodeRegExp ? "u" : "" + // TODO regexp should be wrapped in try/catchs + const regExp = $data + ? _`(new RegExp(${schemaCode}, ${regExpFlags}))` + : usePattern(gen, schema, regExpFlags) cxt.fail$data(_`!${regExp}.test(${data})`) }, } diff --git a/spec/options/unicodeRegExp.spec.ts b/spec/options/unicodeRegExp.spec.ts new file mode 100644 index 000000000..737fa0432 --- /dev/null +++ b/spec/options/unicodeRegExp.spec.ts @@ -0,0 +1,67 @@ +import _Ajv from "../ajv" +import chai from "../chai" +const should = chai.should() + +describe("unicodeRegExp option", () => { + const unicodeChar = "\uD83D\uDC4D" + const unicodeSchema = { + type: "string", + pattern: `^[${unicodeChar}]$`, + } + + const schemaWithEscape = { + type: "string", + pattern: "^[\\:]$", + } + + const patternPropertiesSchema = { + type: "object", + patternProperties: { + "^\\:.*$": {type: "number"}, + }, + additionalProperties: false, + } + + describe("= true (default)", () => { + const ajv = new _Ajv() + it("should fail schema compilation if used invalid (unnecessary) escape sequence for pattern", () => { + should.throw(() => { + ajv.compile(schemaWithEscape) + }, /Invalid escape/) + }) + + it("should fail schema compilation if used invalid (unnecessary) escape sequence for patternProperties", () => { + should.throw(() => { + ajv.compile(patternPropertiesSchema) + }, /Invalid escape/) + }) + + it("should validate unicode character", () => { + const validate = ajv.compile(unicodeSchema) + validate(unicodeChar).should.equal(true) + }) + }) + + describe("= false", () => { + const ajv = new _Ajv({unicodeRegExp: false}) + it("should pass schema compilation if used unnecessary escape sequence for pattern", () => { + should.not.throw(() => { + const validate = ajv.compile(schemaWithEscape) + validate(":").should.equal(true) + }) + }) + + it("should pass schema compilation if used unnecessary escape sequence for patternProperties", () => { + should.not.throw(() => { + const validate = ajv.compile(patternPropertiesSchema) + validate({":test": 1}).should.equal(true) + validate({test: 1}).should.equal(false) + }) + }) + + it("should not validate unicode character", () => { + const validate = ajv.compile(unicodeSchema) + validate(unicodeChar).should.equal(false) + }) + }) +}) From 5cab9bf3dcff3070b198b27c7274e5415966a97b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 11 Apr 2021 17:32:55 +0100 Subject: [PATCH 2/2] refactor: move flag to usePattern function --- docs/options.md | 20 +++++++++---------- lib/core.ts | 2 +- .../applicator/additionalProperties.ts | 6 +----- .../applicator/patternProperties.ts | 3 +-- lib/vocabularies/code.ts | 7 ++++--- lib/vocabularies/validation/pattern.ts | 8 +++----- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/docs/options.md b/docs/options.md index 4eb3f0c71..b4d526560 100644 --- a/docs/options.md +++ b/docs/options.md @@ -36,13 +36,13 @@ const defaultOptions = { allErrors: false, verbose: false, discriminator: false, // * + unicodeRegExp: true // * $comment: false, // * formats: {}, keywords: {}, schemas: {}, logger: undefined, loadSchema: undefined, // *, function(uri: string): Promise {} - unicodeRegExp: true // options to modify validated data: removeAdditional: false, useDefaults: false, // * @@ -168,6 +168,15 @@ Include the reference to the part of the schema (`schema` and `parentSchema`) an Support [discriminator keyword](./json-schema.md#discriminator) from [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md). +### unicodeRegExp + +By default Ajv uses unicode flag "u" with "pattern" and "patternProperties", as per JSON Schema spec. See [RegExp.prototype.unicode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode) . + +Option values: + +- `true` (default) - use unicode flag "u". +- `false` - do not use flag "u". + ### $comment Log or pass the value of `$comment` keyword to a function. @@ -240,15 +249,6 @@ Option values: - `true` - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). -### unicodeRegExp - -By default Ajv passes unicode flag "u" to RegExp function when evaluating "pattern" and "patternProperties". See [RegExp.prototype.unicode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode) . - -Option values: - -- `true` (default) - add unicode flag "u". -- `false` - remove unicode flag "u". - ## Advanced options ### meta diff --git a/lib/core.ts b/lib/core.ts index 469bf05ee..090f0f2ea 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -93,11 +93,11 @@ export interface CurrentOptions { allowUnionTypes?: boolean validateFormats?: boolean // validation and reporting options: - unicodeRegExp?: boolean $data?: boolean allErrors?: boolean verbose?: boolean discriminator?: boolean + unicodeRegExp?: boolean $comment?: | true | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown) diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 5ade451a1..bfb511ce5 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -60,11 +60,7 @@ const def: CodeKeywordDefinition & AddedKeywordDefinition = { definedProp = nil } if (patProps.length) { - const regExpFlags = it.opts.unicodeRegExp ? "u" : "" - definedProp = or( - definedProp, - ...patProps.map((p) => _`${usePattern(gen, p, regExpFlags)}.test(${key})`) - ) + definedProp = or(definedProp, ...patProps.map((p) => _`${usePattern(cxt, p)}.test(${key})`)) } return not(definedProp) } diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index fcfd66943..525535438 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -50,8 +50,7 @@ const def: CodeKeywordDefinition = { function validateProperties(pat: string): void { gen.forIn("key", data, (key) => { - const regExpFlags = it.opts.unicodeRegExp ? "u" : "" - gen.if(_`${usePattern(gen, pat, regExpFlags)}.test(${key})`, () => { + gen.if(_`${usePattern(cxt, pat)}.test(${key})`, () => { cxt.subschema( { keyword: "patternProperties", diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index c880d85c8..ca1cf5f0a 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -90,11 +90,12 @@ export function callValidateCode( return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` } -export function usePattern(gen: CodeGen, pattern: string, regExpFlags = "u"): Name { +export function usePattern({gen, it: {opts}}: KeywordCxt, pattern: string): Name { + const u = opts.unicodeRegExp ? "u" : "" return gen.scopeValue("pattern", { key: pattern, - ref: new RegExp(pattern, regExpFlags), - code: _`new RegExp(${pattern}, ${regExpFlags})`, + ref: new RegExp(pattern, u), + code: _`new RegExp(${pattern}, ${u})`, }) } diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index e6aea18d4..7b27b7d3c 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -17,12 +17,10 @@ const def: CodeKeywordDefinition = { $data: true, error, code(cxt: KeywordCxt) { - const {gen, data, $data, schema, schemaCode, it} = cxt - const regExpFlags = it.opts.unicodeRegExp ? "u" : "" + const {data, $data, schema, schemaCode, it} = cxt // TODO regexp should be wrapped in try/catchs - const regExp = $data - ? _`(new RegExp(${schemaCode}, ${regExpFlags}))` - : usePattern(gen, schema, regExpFlags) + const u = it.opts.unicodeRegExp ? "u" : "" + const regExp = $data ? _`(new RegExp(${schemaCode}, ${u}))` : usePattern(cxt, schema) cxt.fail$data(_`!${regExp}.test(${data})`) }, }