|
| 1 | +/******************************************************************************* |
| 2 | +
|
| 3 | + uBlock Origin - a comprehensive, efficient content blocker |
| 4 | + Copyright (C) 2025-present Raymond Hill |
| 5 | +
|
| 6 | + This program is free software: you can redistribute it and/or modify |
| 7 | + it under the terms of the GNU General Public License as published by |
| 8 | + the Free Software Foundation, either version 3 of the License, or |
| 9 | + (at your option) any later version. |
| 10 | +
|
| 11 | + This program is distributed in the hope that it will be useful, |
| 12 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | + GNU General Public License for more details. |
| 15 | +
|
| 16 | + You should have received a copy of the GNU General Public License |
| 17 | + along with this program. If not, see {http://www.gnu.org/licenses/}. |
| 18 | +
|
| 19 | + Home: https://github.com/gorhill/uBlock |
| 20 | +
|
| 21 | +*/ |
| 22 | + |
| 23 | +/******************************************************************************/ |
| 24 | + |
| 25 | +export class JSONPath { |
| 26 | + static create(query) { |
| 27 | + const jsonp = new JSONPath(); |
| 28 | + jsonp.compile(query); |
| 29 | + return jsonp; |
| 30 | + } |
| 31 | + compile(query) { |
| 32 | + this.#compiled = this.#compile(query, 0); |
| 33 | + return this.#compiled ? this.#compiled.i : 0; |
| 34 | + } |
| 35 | + evaluate(root) { |
| 36 | + if ( this.#compiled === undefined ) { return []; } |
| 37 | + this.root = root; |
| 38 | + return this.#evaluate(this.#compiled.steps, []); |
| 39 | + } |
| 40 | + resolvePath(path) { |
| 41 | + if ( path.length === 0 ) { return { value: this.root }; } |
| 42 | + const key = path.at(-1); |
| 43 | + let obj = this.root |
| 44 | + for ( let i = 0, n = path.length-1; i < n; i++ ) { |
| 45 | + obj = obj[path[i]]; |
| 46 | + } |
| 47 | + return { obj, key, value: obj[key] }; |
| 48 | + } |
| 49 | + toString() { |
| 50 | + return JSON.stringify(this.#compiled); |
| 51 | + } |
| 52 | + #UNDEFINED = 0; |
| 53 | + #ROOT = 1; |
| 54 | + #CURRENT = 2; |
| 55 | + #CHILDREN = 3; |
| 56 | + #DESCENDANTS = 4; |
| 57 | + #reUnquotedIdentifier = /^[A-Za-z_][\w]*|^\*/; |
| 58 | + #reExpr = /^([!=^$*]=|[<>]=?)(.+?)\)\]/; |
| 59 | + #reIndice = /^\[-?\d+\]/; |
| 60 | + #compiled; |
| 61 | + #compile(query, i) { |
| 62 | + if ( query.length === 0 ) { return; } |
| 63 | + const steps = []; |
| 64 | + let c = query.charCodeAt(i); |
| 65 | + steps.push({ mv: c === 0x24 /* $ */ ? this.#ROOT : this.#CURRENT }); |
| 66 | + if ( c === 0x24 /* $ */ || c === 0x40 /* @ */ ) { i += 1; } |
| 67 | + let mv = this.#UNDEFINED; |
| 68 | + for (;;) { |
| 69 | + if ( i === query.length ) { break; } |
| 70 | + c = query.charCodeAt(i); |
| 71 | + if ( c === 0x20 /* whitespace */ ) { |
| 72 | + i += 1; |
| 73 | + continue; |
| 74 | + } |
| 75 | + // Dot accessor syntax |
| 76 | + if ( c === 0x2E /* . */ ) { |
| 77 | + if ( mv !== this.#UNDEFINED ) { return; } |
| 78 | + if ( query.startsWith('..', i) ) { |
| 79 | + mv = this.#DESCENDANTS; |
| 80 | + i += 2; |
| 81 | + } else { |
| 82 | + mv = this.#CHILDREN; |
| 83 | + i += 1; |
| 84 | + } |
| 85 | + continue; |
| 86 | + } |
| 87 | + if ( c !== 0x5B /* [ */ ) { |
| 88 | + if ( mv === this.#UNDEFINED ) { |
| 89 | + const step = steps.at(-1); |
| 90 | + if ( step === undefined ) { return; } |
| 91 | + i = this.#compileExpr(step, query, i); |
| 92 | + break; |
| 93 | + } |
| 94 | + const s = this.#consumeUnquotedIdentifier(query, i); |
| 95 | + if ( s === undefined ) { return; } |
| 96 | + steps.push({ mv, k: s }); |
| 97 | + i += s.length; |
| 98 | + mv = this.#UNDEFINED; |
| 99 | + continue; |
| 100 | + } |
| 101 | + // Bracket accessor syntax |
| 102 | + if ( query.startsWith('[*]', i) ) { |
| 103 | + mv ||= this.#CHILDREN; |
| 104 | + steps.push({ mv, k: '*' }); |
| 105 | + i += 3; |
| 106 | + mv = this.#UNDEFINED; |
| 107 | + continue; |
| 108 | + } |
| 109 | + if ( query.startsWith("['", i) ) { |
| 110 | + const r = this.#consumeQuotedIdentifier(query, i+2); |
| 111 | + if ( r === undefined ) { return; } |
| 112 | + mv ||= this.#CHILDREN; |
| 113 | + steps.push({ mv, k: r.s }); |
| 114 | + i = r.i; |
| 115 | + mv = this.#UNDEFINED; |
| 116 | + continue; |
| 117 | + } |
| 118 | + if ( query.startsWith('[?(', i) ) { |
| 119 | + const not = query.charCodeAt(i+3) === 0x21 /* ! */; |
| 120 | + const j = i + 3 + (not ? 1 : 0); |
| 121 | + const r = this.#compile(query, j); |
| 122 | + if ( r === undefined ) { return; } |
| 123 | + if ( query.startsWith(')]', r.i) === false ) { return; } |
| 124 | + if ( not ) { r.steps.at(-1).not = true; } |
| 125 | + steps.push({ mv: mv || this.#CHILDREN, steps: r.steps }); |
| 126 | + i = r.i + 2; |
| 127 | + mv = this.#UNDEFINED; |
| 128 | + continue; |
| 129 | + } |
| 130 | + if ( this.#reIndice.test(query.slice(i)) ) { |
| 131 | + const match = this.#reIndice.exec(query.slice(i)); |
| 132 | + const indice = parseInt(query.slice(i+1), 10); |
| 133 | + mv ||= this.CHILDREN; |
| 134 | + steps.push({ mv, k: indice }); |
| 135 | + i += match[0].length; |
| 136 | + mv = this.#UNDEFINED; |
| 137 | + continue; |
| 138 | + } |
| 139 | + return; |
| 140 | + } |
| 141 | + if ( steps.length <= 1 ) { return; } |
| 142 | + return { steps, i }; |
| 143 | + } |
| 144 | + #evaluate(steps, pathin) { |
| 145 | + let resultset = []; |
| 146 | + if ( Array.isArray(steps) === false ) { return resultset; } |
| 147 | + for ( const step of steps ) { |
| 148 | + switch ( step.mv ) { |
| 149 | + case this.#ROOT: |
| 150 | + resultset = [ [] ]; |
| 151 | + break; |
| 152 | + case this.#CURRENT: |
| 153 | + resultset = [ pathin ]; |
| 154 | + break; |
| 155 | + case this.#CHILDREN: |
| 156 | + case this.#DESCENDANTS: |
| 157 | + resultset = this.#getMatches(resultset, step); |
| 158 | + break; |
| 159 | + default: |
| 160 | + break; |
| 161 | + } |
| 162 | + } |
| 163 | + return resultset; |
| 164 | + } |
| 165 | + #getMatches(listin, step) { |
| 166 | + const listout = []; |
| 167 | + const recursive = step.mv === this.#DESCENDANTS; |
| 168 | + for ( const pathin of listin ) { |
| 169 | + const { value: v } = this.resolvePath(pathin); |
| 170 | + if ( v === null ) { continue; } |
| 171 | + if ( v === undefined ) { continue; } |
| 172 | + const { steps, k } = step; |
| 173 | + if ( k ) { |
| 174 | + if ( k === '*' ) { |
| 175 | + const entries = Array.from(this.#getDescendants(v, recursive)); |
| 176 | + for ( const { path } of entries ) { |
| 177 | + this.#evaluateExpr(step, [ ...pathin, ...path ], listout); |
| 178 | + } |
| 179 | + continue; |
| 180 | + } |
| 181 | + if ( typeof k === 'number' ) { |
| 182 | + if ( Array.isArray(v) === false ) { continue; } |
| 183 | + const n = v.length; |
| 184 | + const i = k >= 0 ? k : n + k; |
| 185 | + if ( i < 0 ) { continue; } |
| 186 | + if ( i >= n ) { continue; } |
| 187 | + this.#evaluateExpr(step, [ ...pathin, i ], listout); |
| 188 | + } else if ( Array.isArray(k) ) { |
| 189 | + for ( const l of k ) { |
| 190 | + this.#evaluateExpr(step, [ ...pathin, l ], listout); |
| 191 | + } |
| 192 | + } else { |
| 193 | + this.#evaluateExpr(step, [ ...pathin, k ], listout); |
| 194 | + } |
| 195 | + if ( recursive !== true ) { continue; } |
| 196 | + for ( const { obj, key, path } of this.#getDescendants(v, recursive) ) { |
| 197 | + const w = obj[key]; |
| 198 | + if ( w instanceof Object === false ) { continue; } |
| 199 | + if ( Object.hasOwn(w, k) === false ) { continue; } |
| 200 | + this.#evaluateExpr(step, [ ...pathin, ...path, k ], listout); |
| 201 | + } |
| 202 | + continue; |
| 203 | + } |
| 204 | + if ( steps ) { |
| 205 | + const isArray = Array.isArray(v); |
| 206 | + if ( isArray === false ) { |
| 207 | + const r = this.#evaluate(steps, pathin); |
| 208 | + if ( r.length !== 0 ) { |
| 209 | + listout.push(pathin); |
| 210 | + } |
| 211 | + if ( recursive !== true ) { continue; } |
| 212 | + } |
| 213 | + for ( const { obj, key, path } of this.#getDescendants(v, recursive) ) { |
| 214 | + const w = obj[key]; |
| 215 | + if ( Array.isArray(w) ) { continue; } |
| 216 | + const x = [ ...pathin, ...path ]; |
| 217 | + const r = this.#evaluate(steps, x); |
| 218 | + if ( r.length !== 0 ) { |
| 219 | + listout.push(x); |
| 220 | + } |
| 221 | + } |
| 222 | + } |
| 223 | + } |
| 224 | + return listout; |
| 225 | + } |
| 226 | + #getDescendants(v, recursive) { |
| 227 | + const iterator = { |
| 228 | + next() { |
| 229 | + const n = this.stack.length; |
| 230 | + if ( n === 0 ) { |
| 231 | + this.value = undefined; |
| 232 | + this.done = true; |
| 233 | + return this; |
| 234 | + } |
| 235 | + const details = this.stack[n-1]; |
| 236 | + const entry = details.keys.next(); |
| 237 | + if ( entry.done ) { |
| 238 | + this.stack.pop(); |
| 239 | + this.path.pop(); |
| 240 | + return this.next(); |
| 241 | + } |
| 242 | + this.path[n-1] = entry.value; |
| 243 | + this.value = { |
| 244 | + obj: details.obj, |
| 245 | + key: entry.value, |
| 246 | + path: this.path.slice(), |
| 247 | + }; |
| 248 | + const v = this.value.obj[this.value.key]; |
| 249 | + if ( recursive ) { |
| 250 | + if ( Array.isArray(v) ) { |
| 251 | + this.stack.push({ obj: v, keys: v.keys() }); |
| 252 | + } else if ( typeof v === 'object' && v !== null ) { |
| 253 | + this.stack.push({ obj: v, keys: Object.keys(v).values() }); |
| 254 | + } |
| 255 | + } |
| 256 | + return this; |
| 257 | + }, |
| 258 | + path: [], |
| 259 | + value: undefined, |
| 260 | + done: false, |
| 261 | + stack: [], |
| 262 | + [Symbol.iterator]() { return this; }, |
| 263 | + }; |
| 264 | + if ( Array.isArray(v) ) { |
| 265 | + iterator.stack.push({ obj: v, keys: v.keys() }); |
| 266 | + } else if ( typeof v === 'object' && v !== null ) { |
| 267 | + iterator.stack.push({ obj: v, keys: Object.keys(v).values() }); |
| 268 | + } |
| 269 | + return iterator; |
| 270 | + } |
| 271 | + #consumeQuotedIdentifier(query, i) { |
| 272 | + const len = query.length; |
| 273 | + const parts = []; |
| 274 | + let beg = i, end = i; |
| 275 | + for (;;) { |
| 276 | + if ( end === len ) { return; } |
| 277 | + const c = query.charCodeAt(end); |
| 278 | + if ( c === 0x27 /* ' */ ) { |
| 279 | + if ( query.startsWith("']", end) === false ) { return; } |
| 280 | + parts.push(query.slice(beg, end)); |
| 281 | + end += 2; |
| 282 | + break; |
| 283 | + } |
| 284 | + if ( c === 0x5C /* \ */ && (end+1) < len ) { |
| 285 | + parts.push(query.slice(beg, end)); |
| 286 | + const d = query.chatCodeAt(end+1); |
| 287 | + if ( d === 0x27 || d === 0x5C ) { |
| 288 | + end += 1; |
| 289 | + beg = end; |
| 290 | + } |
| 291 | + } |
| 292 | + end += 1; |
| 293 | + } |
| 294 | + return { s: parts.join(''), i: end }; |
| 295 | + } |
| 296 | + #consumeUnquotedIdentifier(query, i) { |
| 297 | + const match = this.#reUnquotedIdentifier.exec(query.slice(i)); |
| 298 | + if ( match === null ) { return; } |
| 299 | + return match[0]; |
| 300 | + } |
| 301 | + #compileExpr(step, query, i) { |
| 302 | + const match = this.#reExpr.exec(query.slice(i)); |
| 303 | + if ( match === null ) { return i; } |
| 304 | + try { |
| 305 | + step.rval = JSON.parse(match[2]); |
| 306 | + step.op = match[1]; |
| 307 | + } catch { |
| 308 | + } |
| 309 | + return i + match[1].length + match[2].length; |
| 310 | + } |
| 311 | + #evaluateExpr(step, path, out) { |
| 312 | + const { obj: o, key: k } = this.resolvePath(path); |
| 313 | + const hasOwn = o instanceof Object && Object.hasOwn(o, k); |
| 314 | + const v = o[k]; |
| 315 | + let outcome = true; |
| 316 | + if ( step.op !== undefined && hasOwn === false ) { return; } |
| 317 | + switch ( step.op ) { |
| 318 | + case '==': outcome = v === step.rval; break; |
| 319 | + case '!=': outcome = v !== step.rval; break; |
| 320 | + case '<': outcome = v < step.rval; break; |
| 321 | + case '<=': outcome = v <= step.rval; break; |
| 322 | + case '>': outcome = v > step.rval; break; |
| 323 | + case '>=': outcome = v >= step.rval; break; |
| 324 | + case '^=': outcome = `${v}`.startsWith(step.rval); break; |
| 325 | + case '$=': outcome = `${v}`.endsWith(step.rval); break; |
| 326 | + case '*=': outcome = `${v}`.includes(step.rval); break; |
| 327 | + default: outcome = hasOwn; break; |
| 328 | + } |
| 329 | + if ( outcome === (step.not === true) ) { return; } |
| 330 | + out.push(path); |
| 331 | + } |
| 332 | +} |
0 commit comments