Skip to content

Commit 194354c

Browse files
committed
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
1 parent 9433b21 commit 194354c

File tree

3 files changed

+74
-40
lines changed

3 files changed

+74
-40
lines changed

‎src/js/codemirror/ubo-static-filtering.js‎

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { dom, qs$ } from '../dom.js';
3232

3333
const redirectNames = new Map();
3434
const scriptletNames = new Map();
35-
const preparseDirectiveTokens = new Map();
35+
const preparseDirectiveEnv = [];
3636
const preparseDirectiveHints = [];
3737
const originHints = [];
3838
let hintHelperRegistered = false;
@@ -88,15 +88,9 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
8888
case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_VALUE:
8989
return 'directive';
9090
case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE: {
91-
if ( preparseDirectiveTokens.size === 0 ) {
92-
return 'positive strong';
93-
}
9491
const raw = astParser.getNodeString(currentWalkerNode);
95-
const not = raw.startsWith('!');
96-
const token = not ? raw.slice(1) : raw;
97-
return not === preparseDirectiveTokens.get(token)
98-
? 'negative strong'
99-
: 'positive strong';
92+
const state = sfp.utils.preparser.evaluateExpr(raw, preparseDirectiveEnv);
93+
return state ? 'positive strong' : 'negative strong';
10094
}
10195
case sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR:
10296
return astParser.getFlags(sfp.AST_FLAG_IS_EXCEPTION)
@@ -287,10 +281,9 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
287281
}
288282
}
289283
}
290-
if ( Array.isArray(details.preparseDirectiveTokens)) {
291-
details.preparseDirectiveTokens.forEach(([ a, b ]) => {
292-
preparseDirectiveTokens.set(a, b);
293-
});
284+
if ( Array.isArray(details.preparseDirectiveEnv)) {
285+
preparseDirectiveEnv.length = 0;
286+
preparseDirectiveEnv.push(...details.preparseDirectiveEnv);
294287
}
295288
if ( Array.isArray(details.preparseDirectiveHints)) {
296289
preparseDirectiveHints.push(...details.preparseDirectiveHints);

‎src/js/messaging.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,8 +1593,7 @@ const onMessage = function(request, sender, callback) {
15931593
response = {};
15941594
if ( (request.hintUpdateToken || 0) === 0 ) {
15951595
response.redirectResources = redirectEngine.getResourceDetails();
1596-
response.preparseDirectiveTokens =
1597-
sfp.utils.preparser.getTokens(vAPI.webextFlavor.env);
1596+
response.preparseDirectiveEnv = vAPI.webextFlavor.env.slice();
15981597
response.preparseDirectiveHints =
15991598
sfp.utils.preparser.getHints();
16001599
response.expertMode = µb.hiddenSettings.filterAuthorMode;

‎src/js/static-filtering-parser.js‎

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ export class AstFilterParser {
780780
this.reBadHostnameChars = /[\x00-\x24\x26-\x29\x2b\x2c\x2f\x3b-\x40\x5c\x5e\x60\x7b-\x7f]/;
781781
this.reIsEntity = /^[^*]+\.\*$/;
782782
this.rePreparseDirectiveIf = /^!#if /;
783-
this.rePreparseDirectiveAny = /^!#(?:endif|if |include )/;
783+
this.rePreparseDirectiveAny = /^!#(?:else|endif|if |include )/;
784784
this.reURL = /\bhttps?:\/\/\S+/;
785785
this.reHasPatternSpecialChars = /[\*\^]/;
786786
this.rePatternAllSpecialChars = /[\*\^]+|[^\x00-\x7f]+/g;
@@ -1122,10 +1122,7 @@ export class AstFilterParser {
11221122
this.linkRight(head, next);
11231123
if ( type === NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE ) {
11241124
const rawToken = this.getNodeString(next).trim();
1125-
const token = rawToken.charCodeAt(0) === 0x21 /* ! */
1126-
? rawToken.slice(1)
1127-
: rawToken;
1128-
if ( preparserIfTokens.has(token) === false ) {
1125+
if ( utils.preparser.evaluateExpr(rawToken) === undefined ) {
11291126
this.addNodeFlags(next, NODE_FLAG_ERROR);
11301127
this.addFlags(AST_FLAG_HAS_ERROR);
11311128
this.astError = AST_ERROR_IF_TOKEN_UNKNOWN;
@@ -4137,43 +4134,88 @@ export const utils = (( ) => {
41374134
}
41384135
};
41394136

4137+
// Useful reference:
4138+
// https://adguard.com/kb/general/ad-filtering/create-own-filters/#conditions-directive
4139+
41404140
class preparser {
4141+
static evaluateExprToken(token, env = []) {
4142+
const not = token.charCodeAt(0) === 0x21 /* ! */;
4143+
if ( not ) { token = token.slice(1); }
4144+
const state = preparserTokens.get(token);
4145+
if ( state === undefined ) { return; }
4146+
return state === 'false' && not || env.includes(state) !== not;
4147+
}
4148+
4149+
static evaluateExpr(expr, env = []) {
4150+
if ( expr.startsWith('(') && expr.endsWith(')') ) {
4151+
expr = expr.slice(1, -1);
4152+
}
4153+
const matches = Array.from(expr.matchAll(/(?:(?:&&|\|\|)\s+)?\S+/g));
4154+
if ( matches.length === 0 ) { return; }
4155+
if ( matches[0][0].startsWith('|') || matches[0][0].startsWith('&') ) { return; }
4156+
let result = this.evaluateExprToken(matches[0][0], env);
4157+
for ( let i = 1; i < matches.length; i++ ) {
4158+
const parts = matches[i][0].split(/ +/);
4159+
if ( parts.length !== 2 ) { return; }
4160+
const state = this.evaluateExprToken(parts[1], env);
4161+
if ( state === undefined ) { return; }
4162+
if ( parts[0] === '||' ) {
4163+
result = result || state;
4164+
} else if ( parts[0] === '&&' ) {
4165+
result = result && state;
4166+
} else {
4167+
return;
4168+
}
4169+
}
4170+
return result;
4171+
}
4172+
41414173
// This method returns an array of indices, corresponding to position in
41424174
// the content string which should alternatively be parsed and discarded.
41434175
static splitter(content, env = []) {
4144-
const reIf = /^!#(if|endif)\b([^\n]*)(?:[\n\r]+|$)/gm;
4176+
const reIf = /^!#(if|else|endif)\b([^\n]*)(?:[\n\r]+|$)/gm;
41454177
const stack = [];
4146-
const shouldDiscard = ( ) => stack.some(v => v);
41474178
const parts = [ 0 ];
41484179
let discard = false;
41494180

4181+
const shouldDiscard = ( ) => stack.some(v => v);
4182+
4183+
const begif = (startDiscard, match) => {
4184+
if ( discard === false && startDiscard ) {
4185+
parts.push(match.index);
4186+
discard = true;
4187+
}
4188+
stack.push(startDiscard);
4189+
};
4190+
4191+
const endif = match => {
4192+
stack.pop();
4193+
const stopDiscard = shouldDiscard() === false;
4194+
if ( discard && stopDiscard ) {
4195+
parts.push(match.index + match[0].length);
4196+
discard = false;
4197+
}
4198+
};
4199+
41504200
for (;;) {
41514201
const match = reIf.exec(content);
41524202
if ( match === null ) { break; }
41534203

41544204
switch ( match[1] ) {
41554205
case 'if': {
4156-
let expr = match[2].trim();
4157-
const target = expr.charCodeAt(0) === 0x21 /* '!' */;
4158-
if ( target ) { expr = expr.slice(1); }
4159-
const token = preparserTokens.get(expr);
4160-
const startDiscard =
4161-
token === 'false' && target === false ||
4162-
token !== undefined && env.includes(token) === target;
4163-
if ( discard === false && startDiscard ) {
4164-
parts.push(match.index);
4165-
discard = true;
4166-
}
4167-
stack.push(startDiscard);
4206+
const startDiscard = this.evaluateExpr(match[2].trim(), env) === false;
4207+
begif(startDiscard, match);
4208+
break;
4209+
}
4210+
case 'else': {
4211+
if ( stack.length === 0 ) { break; }
4212+
const startDiscard = stack[stack.length-1] === false;
4213+
endif(match);
4214+
begif(startDiscard, match);
41684215
break;
41694216
}
41704217
case 'endif': {
4171-
stack.pop();
4172-
const stopDiscard = shouldDiscard() === false;
4173-
if ( discard && stopDiscard ) {
4174-
parts.push(match.index + match[0].length);
4175-
discard = false;
4176-
}
4218+
endif(match);
41774219
break;
41784220
}
41794221
default:

0 commit comments

Comments
 (0)