Skip to content

Commit

Permalink
Check for valid I-Regexp syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed May 15, 2024
1 parent 26c5ea9 commit a257c75
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 8 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src/path/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
39 changes: 36 additions & 3 deletions src/path/functions/match.ts
Original file line number Diff line number Diff line change
@@ -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 = {
/**
Expand All @@ -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 {
Expand All @@ -26,11 +39,13 @@ export class Match implements FilterFunction {

readonly cacheSize: number;
readonly throwErrors: boolean;
readonly iRegexpCheck: boolean;
#cache: LRUCache<string, RegExp>;

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);
}

Expand All @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions src/path/functions/search.ts
Original file line number Diff line number Diff line change
@@ -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 = {
/**
Expand All @@ -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 {
Expand All @@ -27,11 +40,13 @@ export class Search implements FilterFunction {

readonly cacheSize: number;
readonly throwErrors: boolean;
readonly iRegexpCheck: boolean;
#cache: LRUCache<string, RegExp>;

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);
}

Expand All @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions tests/path/regex_filters.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -101,6 +102,6 @@ describe("search filter", () => {
);
expect(() =>
env.query("$[?search(@.a, 'a.*(')]", [{ a: "the end is ab" }]),
).toThrow(SyntaxError);
).toThrow(IRegexpError);
});
});

0 comments on commit a257c75

Please sign in to comment.