From f36d2b8496f32b4f52bfc3cf9a1c3248af777f4e Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Wed, 1 Oct 2025 11:35:15 -0400 Subject: [PATCH] Add ability to test against regex in `JSONPath` expressions Related discussion: https://github.com/uBlockOrigin/uAssets/issues/30157#issuecomment-3356048999 --- src/js/jsonpath.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/js/jsonpath.js b/src/js/jsonpath.js index 020aab0bf..1126f8fc1 100644 --- a/src/js/jsonpath.js +++ b/src/js/jsonpath.js @@ -49,6 +49,7 @@ * - ^=: stringified value starts with * - $=: stringified value ends with * - *=: stringified value includes + * - =/.../: regular expression matches * * - Examples (from "JSONPath examples" at reference link) * - .store.book[*].author @@ -376,7 +377,7 @@ export class JSONPath { continue; } if ( c0 === 0x27 /* ' */ ) { - const r = this.#consumeQuotedIdentifier(query, i+1); + const r = this.#untilChar(query, 0x27 /* ' */, i+1) if ( r === undefined ) { return; } keys.push(r.s); i = r.i; @@ -397,22 +398,27 @@ export class JSONPath { } return { s: keys.length === 1 ? keys[0] : keys, i }; } - #consumeQuotedIdentifier(query, i) { + #consumeUnquotedIdentifier(query, i) { + const match = this.#reUnquotedIdentifier.exec(query.slice(i)); + if ( match === null ) { return; } + return match[0]; + } + #untilChar(query, targetCharCode, i) { const len = query.length; const parts = []; let beg = i, end = i; for (;;) { if ( end === len ) { return; } const c = query.charCodeAt(end); - if ( c === 0x27 /* ' */ ) { + if ( c === targetCharCode ) { parts.push(query.slice(beg, end)); end += 1; break; } if ( c === 0x5C /* \ */ && (end+1) < len ) { parts.push(query.slice(beg, end)); - const d = query.chatCodeAt(end+1); - if ( d === 0x27 || d === 0x5C ) { + const d = query.charCodeAt(end+1); + if ( d === targetCharCode || d === 0x5C ) { end += 1; beg = end; } @@ -421,12 +427,20 @@ export class JSONPath { } return { s: parts.join(''), i: end }; } - #consumeUnquotedIdentifier(query, i) { - const match = this.#reUnquotedIdentifier.exec(query.slice(i)); - if ( match === null ) { return; } - return match[0]; - } #compileExpr(query, step, i) { + if ( query.startsWith('=/', i) ) { + const r = this.#untilChar(query, 0x2F /* / */, i+2); + if ( r === undefined ) { return i; } + const match = /^[i]/.exec(query.slice(r.i)); + try { + step.rval = new RegExp(r.s, match && match[0] || undefined); + } catch { + return i; + } + step.op = 're'; + if ( match ) { r.i += match[0].length; } + return r.i; + } const match = this.#reExpr.exec(query.slice(i)); if ( match === null ) { return i; } try { @@ -466,6 +480,7 @@ export class JSONPath { case '^=': outcome = `${v}`.startsWith(step.rval) === target; break; case '$=': outcome = `${v}`.endsWith(step.rval) === target; break; case '*=': outcome = `${v}`.includes(step.rval) === target; break; + case 're': outcome = step.rval.test(`${v}`); break; default: outcome = hasOwn === target; break; } if ( outcome ) { return k; }