diff --git a/CHANGELOG.md b/CHANGELOG.md index 2282ecb..81add73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,20 @@ # JSON P3 Change Log +## Version 1.3.2 + +**Fixes** + +- Fixed more I-Regex to RegExp pattern mapping. See [jsonpath-compliance-test-suite#77](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite/pull/77). + +**Compliance** + +- We now check that regular expression patterns passed to `match` and `search` are valid according to RFC 9485. The standard behavior is to silently return `false` from these filter function if the pattern is invalid. The `throwErrors` option can be passed to `Match` and/or `Search` to throw an error instead, and the `iRegexpCheck` option can be set to `false` to disable I-Regexp checks. + ## Version 1.3.1 **Fixes** -- Fixed RegExp to I-Regex pattern mapping with the `match` and `search` filter functions. We now correctly match the special `.` character to everything other than `\r` and `\n`. +- Fixed I-Regex to RegExp pattern mapping with the `match` and `search` filter functions. We now correctly match the special `.` character to everything other than `\r` and `\n`. ## Version 1.3.0 diff --git a/package-lock.json b/package-lock.json index 74216a9..cf892ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "json-p3", - "version": "1.2.1", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "json-p3", - "version": "1.2.1", + "version": "1.3.1", "license": "MIT", "devDependencies": { "@babel/cli": "^7.23.4", @@ -39,6 +39,7 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-sonarjs": "^0.23.0", "eslint-plugin-tsdoc": "^0.2.17", + "iregexp-check": "^0.1.1", "jest": "^29.7.0", "prettier": "^3.1.1", "rollup": "^4.9.2", @@ -7050,6 +7051,12 @@ "node": ">= 0.4" } }, + "node_modules/iregexp-check": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/iregexp-check/-/iregexp-check-0.1.1.tgz", + "integrity": "sha512-uIFoJ9UV96yhZY3Gp9PAg2UJ5iNGH9+695QqXq/vab2u4cTSur+4EAmxIY2ZafIJc8wRaQe27N3TxQ1yxcJitQ==", + "dev": true + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", diff --git a/package.json b/package.json index 0b187ee..75f07d1 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-sonarjs": "^0.23.0", "eslint-plugin-tsdoc": "^0.2.17", + "iregexp-check": "^0.1.1", "jest": "^29.7.0", "prettier": "^3.1.1", "rollup": "^4.9.2", diff --git a/src/path/errors.ts b/src/path/errors.ts index 76a98e0..c0b38e1 100644 --- a/src/path/errors.ts +++ b/src/path/errors.ts @@ -126,3 +126,14 @@ export class JSONPathRecursionLimitError extends JSONPathError { this.message = withErrorContext(message, token); } } + +/** + * Error thrown due to invalid I-Regexp syntax. + */ +export class IRegexpError extends Error { + constructor(readonly message: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = "IRegexpError"; + } +} diff --git a/src/path/functions/match.ts b/src/path/functions/match.ts index 6f29078..11b9bff 100644 --- a/src/path/functions/match.ts +++ b/src/path/functions/match.ts @@ -1,6 +1,9 @@ +import { isString } from "../../types"; +import { IRegexpError } from "../errors"; import { LRUCache } from "../lru_cache"; import { FilterFunction, FunctionExpressionType } from "./function"; import { mapRegexp } from "./pattern"; +import { check } from "iregexp-check"; export type MatchFilterFunctionOptions = { /** @@ -9,11 +12,21 @@ export type MatchFilterFunctionOptions = { cacheSize?: number; /** - * If _true_, throw errors from regex construction and matching. - * The standard and default behavior is to ignore these errors - * and return _false_. + * If _true_, throw errors from regex checking, construction and matching. + * The standard and default behavior is to ignore these errors and return + * _false_. */ throwErrors?: boolean; + + /** + * If _true_, check that regexp patterns are valid according to I-Regexp. + * The standard and default behavior is to silently return _false_ if a + * pattern is invalid. + * + * If `iRegexpCheck` is _true_ and `throwErrors` is _true_, a `IRegexpError` + * will be thrown. + */ + iRegexpCheck?: boolean; }; export class Match implements FilterFunction { @@ -26,11 +39,13 @@ export class Match implements FilterFunction { readonly cacheSize: number; readonly throwErrors: boolean; + readonly iRegexpCheck: boolean; #cache: LRUCache; constructor(readonly options: MatchFilterFunctionOptions = {}) { this.cacheSize = options.cacheSize ?? 10; this.throwErrors = options.throwErrors ?? false; + this.iRegexpCheck = options.iRegexpCheck ?? true; this.#cache = new LRUCache(this.cacheSize); } @@ -47,6 +62,24 @@ export class Match implements FilterFunction { } } + if (!isString(pattern)) { + if (this.throwErrors) { + throw new IRegexpError( + `match() expected a string pattern, found ${pattern}`, + ); + } + return false; + } + + if (this.iRegexpCheck && !check(pattern)) { + if (this.throwErrors) { + throw new IRegexpError( + `pattern ${pattern} is not a valid I-Regexp pattern`, + ); + } + return false; + } + try { const re = new RegExp(this.fullMatch(pattern), "u"); if (this.cacheSize > 0) this.#cache.set(pattern, re); diff --git a/src/path/functions/search.ts b/src/path/functions/search.ts index af4dfbe..9673591 100644 --- a/src/path/functions/search.ts +++ b/src/path/functions/search.ts @@ -1,6 +1,9 @@ +import { check } from "iregexp-check"; import { LRUCache } from "../lru_cache"; import { FilterFunction, FunctionExpressionType } from "./function"; import { mapRegexp } from "./pattern"; +import { IRegexpError } from "../errors"; +import { isString } from "../../types"; export type SearchFilterFunctionOptions = { /** @@ -15,6 +18,16 @@ export type SearchFilterFunctionOptions = { * and return _false_. */ throwErrors?: boolean; + + /** + * If _true_, check that regexp patterns are valid according to I-Regexp. + * The standard and default behavior is to silently return _false_ if a + * pattern is invalid. + * + * If `iRegexpCheck` is _true_ and `throwErrors` is _true_, a `IRegexpError` + * will be thrown. + */ + iRegexpCheck?: boolean; }; export class Search implements FilterFunction { @@ -27,11 +40,13 @@ export class Search implements FilterFunction { readonly cacheSize: number; readonly throwErrors: boolean; + readonly iRegexpCheck: boolean; #cache: LRUCache; constructor(readonly options: SearchFilterFunctionOptions = {}) { this.cacheSize = options.cacheSize ?? 10; this.throwErrors = options.throwErrors ?? false; + this.iRegexpCheck = options.iRegexpCheck ?? true; this.#cache = new LRUCache(this.cacheSize); } @@ -48,6 +63,24 @@ export class Search implements FilterFunction { } } + if (!isString(pattern)) { + if (this.throwErrors) { + throw new IRegexpError( + `match() expected a string pattern, found ${pattern}`, + ); + } + return false; + } + + if (this.iRegexpCheck && !check(pattern)) { + if (this.throwErrors) { + throw new IRegexpError( + `pattern ${pattern} is not a valid I-Regexp pattern`, + ); + } + return false; + } + try { const re = new RegExp(mapRegexp(pattern), "u"); if (this.cacheSize > 0) this.#cache.set(pattern, re); diff --git a/tests/path/regex_filters.test.ts b/tests/path/regex_filters.test.ts index 423e90a..f0b5891 100644 --- a/tests/path/regex_filters.test.ts +++ b/tests/path/regex_filters.test.ts @@ -1,4 +1,5 @@ import { JSONPathEnvironment } from "../../src/path"; +import { IRegexpError } from "../../src/path/errors"; import { Match, Search } from "../../src/path/functions"; describe("match filter", () => { @@ -23,7 +24,7 @@ describe("match filter", () => { new Match({ cacheSize: 0, throwErrors: true }), ); expect(() => env.query("$[?match(@.a, 'a.*(')]", [{ a: "ab" }])).toThrow( - SyntaxError, + IRegexpError, ); }); test("don't replace dot in character group", () => { @@ -101,6 +102,6 @@ describe("search filter", () => { ); expect(() => env.query("$[?search(@.a, 'a.*(')]", [{ a: "the end is ab" }]), - ).toThrow(SyntaxError); + ).toThrow(IRegexpError); }); });