1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-09-15 15:32:28 +02:00

Add support for logical expressions to !#if directive

Reference:
https://adguard.com/kb/general/ad-filtering/create-own-filters/#conditions-directive

This commit should make uBO fully compatible with the `!#if`
directives found throughout AdGuard's filter lists.

Additionally, added the new `!#else` directive for convenience
to filter list authors:

    !#if cap_html_filtering
    example.com##^script:has-text(fakeAd)
    !#else
    example.com##+js(rmnt, script, fakeAd)
    !#endif
This commit is contained in:
Raymond Hill 2023-06-29 14:50:42 -04:00
parent 9433b218f7
commit 194354cd5d
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 74 additions and 40 deletions

View File

@ -32,7 +32,7 @@ import { dom, qs$ } from '../dom.js';
const redirectNames = new Map(); const redirectNames = new Map();
const scriptletNames = new Map(); const scriptletNames = new Map();
const preparseDirectiveTokens = new Map(); const preparseDirectiveEnv = [];
const preparseDirectiveHints = []; const preparseDirectiveHints = [];
const originHints = []; const originHints = [];
let hintHelperRegistered = false; let hintHelperRegistered = false;
@ -88,15 +88,9 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_VALUE: case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_VALUE:
return 'directive'; return 'directive';
case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE: { case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE: {
if ( preparseDirectiveTokens.size === 0 ) {
return 'positive strong';
}
const raw = astParser.getNodeString(currentWalkerNode); const raw = astParser.getNodeString(currentWalkerNode);
const not = raw.startsWith('!'); const state = sfp.utils.preparser.evaluateExpr(raw, preparseDirectiveEnv);
const token = not ? raw.slice(1) : raw; return state ? 'positive strong' : 'negative strong';
return not === preparseDirectiveTokens.get(token)
? 'negative strong'
: 'positive strong';
} }
case sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR: case sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR:
return astParser.getFlags(sfp.AST_FLAG_IS_EXCEPTION) return astParser.getFlags(sfp.AST_FLAG_IS_EXCEPTION)
@ -287,10 +281,9 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
} }
} }
} }
if ( Array.isArray(details.preparseDirectiveTokens)) { if ( Array.isArray(details.preparseDirectiveEnv)) {
details.preparseDirectiveTokens.forEach(([ a, b ]) => { preparseDirectiveEnv.length = 0;
preparseDirectiveTokens.set(a, b); preparseDirectiveEnv.push(...details.preparseDirectiveEnv);
});
} }
if ( Array.isArray(details.preparseDirectiveHints)) { if ( Array.isArray(details.preparseDirectiveHints)) {
preparseDirectiveHints.push(...details.preparseDirectiveHints); preparseDirectiveHints.push(...details.preparseDirectiveHints);

View File

@ -1593,8 +1593,7 @@ const onMessage = function(request, sender, callback) {
response = {}; response = {};
if ( (request.hintUpdateToken || 0) === 0 ) { if ( (request.hintUpdateToken || 0) === 0 ) {
response.redirectResources = redirectEngine.getResourceDetails(); response.redirectResources = redirectEngine.getResourceDetails();
response.preparseDirectiveTokens = response.preparseDirectiveEnv = vAPI.webextFlavor.env.slice();
sfp.utils.preparser.getTokens(vAPI.webextFlavor.env);
response.preparseDirectiveHints = response.preparseDirectiveHints =
sfp.utils.preparser.getHints(); sfp.utils.preparser.getHints();
response.expertMode = µb.hiddenSettings.filterAuthorMode; response.expertMode = µb.hiddenSettings.filterAuthorMode;

View File

@ -780,7 +780,7 @@ export class AstFilterParser {
this.reBadHostnameChars = /[\x00-\x24\x26-\x29\x2b\x2c\x2f\x3b-\x40\x5c\x5e\x60\x7b-\x7f]/; this.reBadHostnameChars = /[\x00-\x24\x26-\x29\x2b\x2c\x2f\x3b-\x40\x5c\x5e\x60\x7b-\x7f]/;
this.reIsEntity = /^[^*]+\.\*$/; this.reIsEntity = /^[^*]+\.\*$/;
this.rePreparseDirectiveIf = /^!#if /; this.rePreparseDirectiveIf = /^!#if /;
this.rePreparseDirectiveAny = /^!#(?:endif|if |include )/; this.rePreparseDirectiveAny = /^!#(?:else|endif|if |include )/;
this.reURL = /\bhttps?:\/\/\S+/; this.reURL = /\bhttps?:\/\/\S+/;
this.reHasPatternSpecialChars = /[\*\^]/; this.reHasPatternSpecialChars = /[\*\^]/;
this.rePatternAllSpecialChars = /[\*\^]+|[^\x00-\x7f]+/g; this.rePatternAllSpecialChars = /[\*\^]+|[^\x00-\x7f]+/g;
@ -1122,10 +1122,7 @@ export class AstFilterParser {
this.linkRight(head, next); this.linkRight(head, next);
if ( type === NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE ) { if ( type === NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE ) {
const rawToken = this.getNodeString(next).trim(); const rawToken = this.getNodeString(next).trim();
const token = rawToken.charCodeAt(0) === 0x21 /* ! */ if ( utils.preparser.evaluateExpr(rawToken) === undefined ) {
? rawToken.slice(1)
: rawToken;
if ( preparserIfTokens.has(token) === false ) {
this.addNodeFlags(next, NODE_FLAG_ERROR); this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR); this.addFlags(AST_FLAG_HAS_ERROR);
this.astError = AST_ERROR_IF_TOKEN_UNKNOWN; this.astError = AST_ERROR_IF_TOKEN_UNKNOWN;
@ -4137,43 +4134,88 @@ export const utils = (( ) => {
} }
}; };
// Useful reference:
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#conditions-directive
class preparser { class preparser {
static evaluateExprToken(token, env = []) {
const not = token.charCodeAt(0) === 0x21 /* ! */;
if ( not ) { token = token.slice(1); }
const state = preparserTokens.get(token);
if ( state === undefined ) { return; }
return state === 'false' && not || env.includes(state) !== not;
}
static evaluateExpr(expr, env = []) {
if ( expr.startsWith('(') && expr.endsWith(')') ) {
expr = expr.slice(1, -1);
}
const matches = Array.from(expr.matchAll(/(?:(?:&&|\|\|)\s+)?\S+/g));
if ( matches.length === 0 ) { return; }
if ( matches[0][0].startsWith('|') || matches[0][0].startsWith('&') ) { return; }
let result = this.evaluateExprToken(matches[0][0], env);
for ( let i = 1; i < matches.length; i++ ) {
const parts = matches[i][0].split(/ +/);
if ( parts.length !== 2 ) { return; }
const state = this.evaluateExprToken(parts[1], env);
if ( state === undefined ) { return; }
if ( parts[0] === '||' ) {
result = result || state;
} else if ( parts[0] === '&&' ) {
result = result && state;
} else {
return;
}
}
return result;
}
// This method returns an array of indices, corresponding to position in // This method returns an array of indices, corresponding to position in
// the content string which should alternatively be parsed and discarded. // the content string which should alternatively be parsed and discarded.
static splitter(content, env = []) { static splitter(content, env = []) {
const reIf = /^!#(if|endif)\b([^\n]*)(?:[\n\r]+|$)/gm; const reIf = /^!#(if|else|endif)\b([^\n]*)(?:[\n\r]+|$)/gm;
const stack = []; const stack = [];
const shouldDiscard = ( ) => stack.some(v => v);
const parts = [ 0 ]; const parts = [ 0 ];
let discard = false; let discard = false;
const shouldDiscard = ( ) => stack.some(v => v);
const begif = (startDiscard, match) => {
if ( discard === false && startDiscard ) {
parts.push(match.index);
discard = true;
}
stack.push(startDiscard);
};
const endif = match => {
stack.pop();
const stopDiscard = shouldDiscard() === false;
if ( discard && stopDiscard ) {
parts.push(match.index + match[0].length);
discard = false;
}
};
for (;;) { for (;;) {
const match = reIf.exec(content); const match = reIf.exec(content);
if ( match === null ) { break; } if ( match === null ) { break; }
switch ( match[1] ) { switch ( match[1] ) {
case 'if': { case 'if': {
let expr = match[2].trim(); const startDiscard = this.evaluateExpr(match[2].trim(), env) === false;
const target = expr.charCodeAt(0) === 0x21 /* '!' */; begif(startDiscard, match);
if ( target ) { expr = expr.slice(1); } break;
const token = preparserTokens.get(expr); }
const startDiscard = case 'else': {
token === 'false' && target === false || if ( stack.length === 0 ) { break; }
token !== undefined && env.includes(token) === target; const startDiscard = stack[stack.length-1] === false;
if ( discard === false && startDiscard ) { endif(match);
parts.push(match.index); begif(startDiscard, match);
discard = true;
}
stack.push(startDiscard);
break; break;
} }
case 'endif': { case 'endif': {
stack.pop(); endif(match);
const stopDiscard = shouldDiscard() === false;
if ( discard && stopDiscard ) {
parts.push(match.index + match[0].length);
discard = false;
}
break; break;
} }
default: default: