Support negated pattern for stack test in scriptlets

Prepend pattern with `!` to test for unmatched patterns in
stack trace. This applies to sctiplet parameters which purpose
is to test against the stack, i.e. `aost` and `json-prune`.

Additionally, dropped support for JSON notation in favor of
optional variable arguments notation.

Related discussion:
- https://github.com/uBlockOrigin/uBlock-discussions/discussions/789#discussioncomment-6520330
This commit is contained in:
Raymond Hill 2023-07-31 09:38:04 -04:00
parent bb7779ba75
commit 84cc69aa10
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 48 additions and 45 deletions

View File

@ -59,6 +59,36 @@ function safeSelf() {
if ( `${args[0]}` === '' ) { return; } if ( `${args[0]}` === '' ) { return; }
this.log('[uBO]', ...args); this.log('[uBO]', ...args);
}, },
'initPattern': function(pattern, options = {}) {
if ( pattern === '' ) {
return { matchAll: true };
}
const expect = (options.canNegate && pattern.startsWith('!') === false);
if ( expect === false ) {
pattern = pattern.slice(1);
}
const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
if ( match !== null ) {
return {
re: new this.RegExp(
match[1],
match[2] || options.flags
),
expect,
};
}
return {
re: new this.RegExp(pattern.replace(
/[.*+?^${}()|[\]\\]/g, '\\$&'),
options.flags
),
expect,
};
},
'testPattern': function(details, haystack) {
if ( details.matchAll ) { return true; }
return this.RegExp_test.call(details.re, haystack) === details.expect;
},
}; };
scriptletGlobals.set('safeSelf', safe); scriptletGlobals.set('safeSelf', safe);
return safe; return safe;
@ -636,7 +666,7 @@ function objectPrune(
obj, obj,
rawPrunePaths, rawPrunePaths,
rawNeedlePaths, rawNeedlePaths,
stackNeedle = '' stackNeedleDetails = { matchAll: true }
) { ) {
if ( typeof rawPrunePaths !== 'string' ) { return obj; } if ( typeof rawPrunePaths !== 'string' ) { return obj; }
const prunePaths = rawPrunePaths !== '' const prunePaths = rawPrunePaths !== ''
@ -657,9 +687,8 @@ function objectPrune(
log = console.log.bind(console); log = console.log.bind(console);
reLogNeedle = patternToRegex(rawNeedlePaths); reLogNeedle = patternToRegex(rawNeedlePaths);
} }
if ( stackNeedle !== '' ) { if ( stackNeedleDetails.matchAll !== true ) {
const reStackNeedle = patternToRegex(stackNeedle); if ( matchesStackTrace(stackNeedleDetails, extraArgs.logstack) === false ) {
if ( matchesStackTrace(reStackNeedle, extraArgs.logstack) === false ) {
return obj; return obj;
} }
} }
@ -828,10 +857,9 @@ builtinScriptlets.push({
], ],
}); });
function matchesStackTrace( function matchesStackTrace(
reNeedle, needleDetails,
logLevel = 0 logLevel = 0
) { ) {
if ( reNeedle === undefined ) { return false; }
const safe = safeSelf(); const safe = safeSelf();
const exceptionToken = getExceptionToken(); const exceptionToken = getExceptionToken();
const error = new safe.Error(exceptionToken); const error = new safe.Error(exceptionToken);
@ -861,7 +889,7 @@ function matchesStackTrace(
} }
lines[0] = `stackDepth:${lines.length-1}`; lines[0] = `stackDepth:${lines.length-1}`;
const stack = lines.join('\t'); const stack = lines.join('\t');
const r = safe.RegExp_test.call(reNeedle, stack); const r = safe.testPattern(needleDetails, stack);
if ( if (
logLevel === 1 || logLevel === 1 ||
logLevel === 2 && r || logLevel === 2 && r ||
@ -1004,15 +1032,16 @@ builtinScriptlets.push({
'get-extra-args.fn', 'get-extra-args.fn',
'matches-stack-trace.fn', 'matches-stack-trace.fn',
'pattern-to-regex.fn', 'pattern-to-regex.fn',
'safe-self.fn',
], ],
}); });
// Status is currently experimental
function abortOnStackTrace( function abortOnStackTrace(
chain = '', chain = '',
needle = '' needle = ''
) { ) {
if ( typeof chain !== 'string' ) { return; } if ( typeof chain !== 'string' ) { return; }
const reNeedle = patternToRegex(needle); const safe = safeSelf();
const needleDetails = safe.initPattern(needle, { canNegate: true });
const extraArgs = getExtraArgs(Array.from(arguments), 2); const extraArgs = getExtraArgs(Array.from(arguments), 2);
const makeProxy = function(owner, chain) { const makeProxy = function(owner, chain) {
const pos = chain.indexOf('.'); const pos = chain.indexOf('.');
@ -1020,13 +1049,13 @@ function abortOnStackTrace(
let v = owner[chain]; let v = owner[chain];
Object.defineProperty(owner, chain, { Object.defineProperty(owner, chain, {
get: function() { get: function() {
if ( matchesStackTrace(reNeedle, extraArgs.log) ) { if ( matchesStackTrace(needleDetails, extraArgs.log) ) {
throw new ReferenceError(getExceptionToken()); throw new ReferenceError(getExceptionToken());
} }
return v; return v;
}, },
set: function(a) { set: function(a) {
if ( matchesStackTrace(reNeedle, extraArgs.log) ) { if ( matchesStackTrace(needleDetails, extraArgs.log) ) {
throw new ReferenceError(getExceptionToken()); throw new ReferenceError(getExceptionToken());
} }
v = a; v = a;
@ -1132,6 +1161,7 @@ builtinScriptlets.push({
fn: jsonPrune, fn: jsonPrune,
dependencies: [ dependencies: [
'object-prune.fn', 'object-prune.fn',
'safe-self.fn',
], ],
}); });
// When no "prune paths" argument is provided, the scriptlet is // When no "prune paths" argument is provided, the scriptlet is
@ -1145,6 +1175,8 @@ function jsonPrune(
rawNeedlePaths = '', rawNeedlePaths = '',
stackNeedle = '' stackNeedle = ''
) { ) {
const safe = safeSelf();
const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true });
const extraArgs = Array.from(arguments).slice(3); const extraArgs = Array.from(arguments).slice(3);
JSON.parse = new Proxy(JSON.parse, { JSON.parse = new Proxy(JSON.parse, {
apply: function(target, thisArg, args) { apply: function(target, thisArg, args) {
@ -1152,7 +1184,7 @@ function jsonPrune(
Reflect.apply(target, thisArg, args), Reflect.apply(target, thisArg, args),
rawPrunePaths, rawPrunePaths,
rawNeedlePaths, rawNeedlePaths,
stackNeedle, stackNeedleDetails,
...extraArgs ...extraArgs
); );
}, },
@ -1164,7 +1196,7 @@ function jsonPrune(
o, o,
rawPrunePaths, rawPrunePaths,
rawNeedlePaths, rawNeedlePaths,
stackNeedle, stackNeedleDetails,
...extraArgs ...extraArgs
) )
); );

View File

@ -41,7 +41,6 @@ const VERSION = 1;
const duplicates = new Set(); const duplicates = new Set();
const scriptletCache = new µb.MRUCache(32); const scriptletCache = new µb.MRUCache(32);
const reEscapeScriptArg = /[\\'"]/g;
const scriptletDB = new StaticExtFilteringHostnameDB(1, VERSION); const scriptletDB = new StaticExtFilteringHostnameDB(1, VERSION);
@ -206,21 +205,11 @@ const patchScriptlet = function(content, arglist) {
if ( content.startsWith('function') && content.endsWith('}') ) { if ( content.startsWith('function') && content.endsWith('}') ) {
content = `(${content})({{args}});`; content = `(${content})({{args}});`;
} }
if ( arglist.length === 0 ) {
return content.replace('{{args}}', '');
}
if ( arglist.length === 1 ) {
if ( arglist[0].startsWith('{') && arglist[0].endsWith('}') ) {
return content.replace('{{args}}', arglist[0]);
}
}
for ( let i = 0; i < arglist.length; i++ ) { for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]); content = content.replace(`{{${i+1}}}`, arglist[i]);
} }
return content.replace('{{args}}', return content.replace('{{args}}',
arglist.map(a => `'${a.replace(reEscapeScriptArg, '\\$&')}'`) JSON.stringify(arglist).slice(1,-1).replace(/\$/g, '$$$')
.join(', ')
.replace(/\$/g, '$$$')
); );
}; };

View File

@ -2266,27 +2266,9 @@ export class AstFilterParser {
const parentEnd = this.nodes[parent+NODE_END_INDEX]; const parentEnd = this.nodes[parent+NODE_END_INDEX];
if ( parentEnd === parentBeg ) { return 0; } if ( parentEnd === parentBeg ) { return 0; }
const s = this.getNodeString(parent); const s = this.getNodeString(parent);
let next = 0;
// json-based arg?
const match = this.rePatternScriptletJsonArgs.exec(s);
if ( match !== null ) {
next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG,
parentBeg,
parentEnd
);
try {
void JSON.parse(s);
} catch(ex) {
this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR);
}
return next;
}
// positional args
const head = this.allocHeadNode();
const argsEnd = s.length; const argsEnd = s.length;
let prev = head; const head = this.allocHeadNode();
let prev = head, next = 0;
let decorationBeg = 0; let decorationBeg = 0;
let i = 0; let i = 0;
for (;;) { for (;;) {