Add compatibility with AdGuard's `#%#//scriptlet(...)` syntax

Related issue:
- https://github.com/AdguardTeam/Scriptlets/issues/332

Additionally, uBO's own scriplet syntax now also accept quoting
the parameters with either `'` or `"`. This can be used to avoid
having to escape commas when they are present in a parameter.
This commit is contained in:
Raymond Hill 2023-06-28 19:35:22 -04:00
parent e0b3b44080
commit fd036a51ee
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
8 changed files with 292 additions and 183 deletions

View File

@ -763,7 +763,11 @@ function setCookieHelper(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'abort-current-script.js', name: 'abort-current-script.js',
aliases: [ 'acs.js', 'abort-current-inline-script.js', 'acis.js' ], aliases: [
'acs.js',
'abort-current-inline-script.js',
'acis.js',
],
fn: abortCurrentScript, fn: abortCurrentScript,
dependencies: [ dependencies: [
'abort-current-script-core.fn', 'abort-current-script-core.fn',
@ -786,7 +790,9 @@ function abortCurrentScript(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'abort-on-property-read.js', name: 'abort-on-property-read.js',
aliases: [ 'aopr.js' ], aliases: [
'aopr.js',
],
fn: abortOnPropertyRead, fn: abortOnPropertyRead,
dependencies: [ dependencies: [
'get-exception-token.fn', 'get-exception-token.fn',
@ -840,7 +846,9 @@ function abortOnPropertyRead(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'abort-on-property-write.js', name: 'abort-on-property-write.js',
aliases: [ 'aopw.js' ], aliases: [
'aopw.js',
],
fn: abortOnPropertyWrite, fn: abortOnPropertyWrite,
dependencies: [ dependencies: [
'get-exception-token.fn', 'get-exception-token.fn',
@ -872,7 +880,9 @@ function abortOnPropertyWrite(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'abort-on-stack-trace.js', name: 'abort-on-stack-trace.js',
aliases: [ 'aost.js' ], aliases: [
'aost.js',
],
fn: abortOnStackTrace, fn: abortOnStackTrace,
dependencies: [ dependencies: [
'get-exception-token.fn', 'get-exception-token.fn',
@ -978,7 +988,10 @@ function abortOnStackTrace(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'addEventListener-defuser.js', name: 'addEventListener-defuser.js',
aliases: [ 'aeld.js' ], aliases: [
'aeld.js',
'prevent-addEventListener.js',
],
fn: addEventListenerDefuser, fn: addEventListenerDefuser,
dependencies: [ dependencies: [
'get-extra-args.fn', 'get-extra-args.fn',
@ -1106,7 +1119,9 @@ function evaldataPrune(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'nano-setInterval-booster.js', name: 'nano-setInterval-booster.js',
aliases: [ 'nano-sib.js' ], aliases: [
'nano-sib.js',
],
fn: nanoSetIntervalBooster, fn: nanoSetIntervalBooster,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1155,7 +1170,9 @@ function nanoSetIntervalBooster(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'nano-setTimeout-booster.js', name: 'nano-setTimeout-booster.js',
aliases: [ 'nano-stb.js' ], aliases: [
'nano-stb.js',
],
fn: nanoSetTimeoutBooster, fn: nanoSetTimeoutBooster,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1205,6 +1222,9 @@ function nanoSetTimeoutBooster(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'noeval-if.js', name: 'noeval-if.js',
aliases: [
'prevent-eval-if.js',
],
fn: noEvalIf, fn: noEvalIf,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1228,6 +1248,9 @@ function noEvalIf(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'no-fetch-if.js', name: 'no-fetch-if.js',
aliases: [
'prevent-fetch.js',
],
fn: noFetchIf, fn: noFetchIf,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1330,7 +1353,9 @@ function refreshDefuser(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'remove-attr.js', name: 'remove-attr.js',
aliases: [ 'ra.js' ], aliases: [
'ra.js',
],
fn: removeAttr, fn: removeAttr,
dependencies: [ dependencies: [
'run-at.fn', 'run-at.fn',
@ -1396,7 +1421,9 @@ function removeAttr(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'remove-class.js', name: 'remove-class.js',
aliases: [ 'rc.js' ], aliases: [
'rc.js',
],
fn: removeClass, fn: removeClass,
dependencies: [ dependencies: [
'run-at.fn', 'run-at.fn',
@ -1460,7 +1487,10 @@ function removeClass(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'no-requestAnimationFrame-if.js', name: 'no-requestAnimationFrame-if.js',
aliases: [ 'norafif.js' ], aliases: [
'norafif.js',
'prevent-requestAnimationFrame.js',
],
fn: noRequestAnimationFrameIf, fn: noRequestAnimationFrameIf,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1495,7 +1525,9 @@ function noRequestAnimationFrameIf(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'set-constant.js', name: 'set-constant.js',
aliases: [ 'set.js' ], aliases: [
'set.js',
],
fn: setConstant, fn: setConstant,
dependencies: [ dependencies: [
'set-constant-core.fn' 'set-constant-core.fn'
@ -1511,7 +1543,10 @@ function setConstant(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'no-setInterval-if.js', name: 'no-setInterval-if.js',
aliases: [ 'nosiif.js' ], aliases: [
'nosiif.js',
'prevent-setInterval.js',
],
fn: noSetIntervalIf, fn: noSetIntervalIf,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1568,7 +1603,11 @@ function noSetIntervalIf(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'no-setTimeout-if.js', name: 'no-setTimeout-if.js',
aliases: [ 'nostif.js', 'setTimeout-defuser.js' ], aliases: [
'nostif.js',
'prevent-setTimeout.js',
'setTimeout-defuser.js',
],
fn: noSetTimeoutIf, fn: noSetTimeoutIf,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1692,6 +1731,9 @@ function webrtcIf(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'no-xhr-if.js', name: 'no-xhr-if.js',
aliases: [
'prevent-xhr.js',
],
fn: noXhrIf, fn: noXhrIf,
dependencies: [ dependencies: [
'pattern-to-regex.fn', 'pattern-to-regex.fn',
@ -1765,7 +1807,9 @@ function noXhrIf(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'no-window-open-if.js', name: 'no-window-open-if.js',
aliases: [ 'nowoif.js' ], aliases: [
'nowoif.js',
],
fn: noWindowOpenIf, fn: noWindowOpenIf,
dependencies: [ dependencies: [
'get-extra-args.fn', 'get-extra-args.fn',
@ -2699,7 +2743,9 @@ function spoofCSS(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'remove-node-text.js', name: 'remove-node-text.js',
aliases: [ 'rmnt.js' ], aliases: [
'rmnt.js',
],
fn: removeNodeText, fn: removeNodeText,
world: 'ISOLATED', world: 'ISOLATED',
dependencies: [ dependencies: [
@ -2786,9 +2832,9 @@ function setLocalStorageItem(
value = '' value = ''
) { ) {
if ( key === '' ) { return; } if ( key === '' ) { return; }
if ( value === '' ) { return; }
const validValues = [ const validValues = [
'',
'undefined', 'null', 'undefined', 'null',
'false', 'true', 'false', 'true',
'yes', 'no', 'yes', 'no',
@ -2805,7 +2851,11 @@ function setLocalStorageItem(
} }
try { try {
if ( actualValue !== undefined ) {
self.localStorage.setItem(key, `${actualValue}`); self.localStorage.setItem(key, `${actualValue}`);
} else {
self.localStorage.removeItem(key);
}
} catch(ex) { } catch(ex) {
} }
} }
@ -2849,7 +2899,9 @@ function setLocalStorageItem(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'replace-node-text.js', name: 'replace-node-text.js',
requiresTrust: true, requiresTrust: true,
aliases: [ 'rpnt.js', 'sed.js' /* to be removed */ ], aliases: [
'rpnt.js',
],
fn: replaceNodeText, fn: replaceNodeText,
world: 'ISOLATED', world: 'ISOLATED',
dependencies: [ dependencies: [
@ -2877,7 +2929,9 @@ function replaceNodeText(
builtinScriptlets.push({ builtinScriptlets.push({
name: 'trusted-set-constant.js', name: 'trusted-set-constant.js',
requiresTrust: true, requiresTrust: true,
aliases: [ 'trusted-set.js' ], aliases: [
'trusted-set.js',
],
fn: trustedSetConstant, fn: trustedSetConstant,
dependencies: [ dependencies: [
'set-constant-core.fn' 'set-constant-core.fn'

View File

@ -176,8 +176,8 @@ const µBlock = { // jshint ignore:line
// Read-only // Read-only
systemSettings: { systemSettings: {
compiledMagic: 55, // Increase when compiled format changes compiledMagic: 56, // Increase when compiled format changes
selfieMagic: 55, // Increase when selfie format changes selfieMagic: 56, // Increase when selfie format changes
}, },
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501 // https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501

View File

@ -237,7 +237,7 @@ const fromExtendedFilter = function(details) {
// Scriptlet injection // Scriptlet injection
case 32: case 32:
if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; } if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; }
if ( fargs[3] !== selector ) { break; } if ( fargs[3] !== details.compiled ) { break; }
if ( hostnameMatches(fargs[1]) ) { if ( hostnameMatches(fargs[1]) ) {
found = fargs[1] + prefix + selector; found = fargs[1] + prefix + selector;
} }

View File

@ -167,6 +167,16 @@ const fromExtendedFilter = async function(details) {
const id = messageId++; const id = messageId++;
const hostname = hostnameFromURI(details.url); const hostname = hostnameFromURI(details.url);
const parser = new sfp.AstFilterParser({
expertMode: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
});
parser.parse(details.rawFilter);
let compiled;
if ( parser.isScriptletFilter() ) {
compiled = JSON.stringify(parser.getScripletArgs());
}
worker.postMessage({ worker.postMessage({
what: 'fromExtendedFilter', what: 'fromExtendedFilter',
id, id,
@ -182,7 +192,8 @@ const fromExtendedFilter = async function(details) {
'specifichide', 'specifichide',
details.url details.url
) === 2, ) === 2,
rawFilter: details.rawFilter rawFilter: details.rawFilter,
compiled,
}); });
return new Promise(resolve => { return new Promise(resolve => {

View File

@ -27,7 +27,6 @@ import µb from './background.js';
import { redirectEngine as reng } from './redirect-engine.js'; import { redirectEngine as reng } from './redirect-engine.js';
import { sessionFirewall } from './filtering-engines.js'; import { sessionFirewall } from './filtering-engines.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js'; import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import * as sfp from './static-filtering-parser.js';
import { import {
domainFromHostname, domainFromHostname,
@ -37,11 +36,13 @@ import {
/******************************************************************************/ /******************************************************************************/
// Increment when internal representation changes
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); const scriptletDB = new StaticExtFilteringHostnameDB(1, VERSION);
let acceptedCount = 0; let acceptedCount = 0;
let discardedCount = 0; let discardedCount = 0;
@ -156,24 +157,8 @@ const isolatedWorldInjector = (( ) => {
}; };
})(); })();
// TODO: Probably should move this into StaticFilteringParser
// https://github.com/uBlockOrigin/uBlock-issues/issues/1031
// Normalize scriptlet name to its canonical, unaliased name.
const normalizeRawFilter = function(parser, sourceIsTrusted = false) { const normalizeRawFilter = function(parser, sourceIsTrusted = false) {
const root = parser.getBranchFromType(sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET); const args = parser.getScripletArgs();
const walker = parser.getWalker(root);
const args = [];
for ( let node = walker.next(); node !== 0; node = walker.next() ) {
switch ( parser.getNodeType(node) ) {
case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN:
case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG:
args.push(parser.getNodeString(node));
break;
default:
break;
}
}
walker.dispose();
if ( args.length !== 0 ) { if ( args.length !== 0 ) {
let token = `${args[0]}.js`; let token = `${args[0]}.js`;
if ( reng.aliases.has(token) ) { if ( reng.aliases.has(token) ) {
@ -184,28 +169,17 @@ const normalizeRawFilter = function(parser, sourceIsTrusted = false) {
} }
args[0] = token.slice(0, -3); args[0] = token.slice(0, -3);
} }
return `+js(${args.join(', ')})`; return JSON.stringify(args);
}; };
const lookupScriptlet = function(rawToken, mainMap, isolatedMap) { const lookupScriptlet = function(rawToken, mainMap, isolatedMap) {
if ( mainMap.has(rawToken) || isolatedMap.has(rawToken) ) { return; } if ( mainMap.has(rawToken) || isolatedMap.has(rawToken) ) { return; }
const pos = rawToken.indexOf(','); const args = JSON.parse(rawToken);
let token, args = ''; const token = `${args[0]}.js`;
if ( pos === -1 ) {
token = rawToken;
} else {
token = rawToken.slice(0, pos).trim();
args = rawToken.slice(pos + 1).trim();
}
if ( reng.aliases.has(token) ) {
token = reng.aliases.get(token);
} else {
token = `${token}.js`;
}
const details = reng.contentFromName(token, 'text/javascript'); const details = reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { return; } if ( details === undefined ) { return; }
const targetWorldMap = details.world !== 'ISOLATED' ? mainMap : isolatedMap; const targetWorldMap = details.world !== 'ISOLATED' ? mainMap : isolatedMap;
const content = patchScriptlet(details.js, args); const content = patchScriptlet(details.js, args.slice(1));
const dependencies = details.dependencies || []; const dependencies = details.dependencies || [];
while ( dependencies.length !== 0 ) { while ( dependencies.length !== 0 ) {
const token = dependencies.shift(); const token = dependencies.shift();
@ -227,43 +201,34 @@ const lookupScriptlet = function(rawToken, mainMap, isolatedMap) {
}; };
// Fill-in scriptlet argument placeholders. // Fill-in scriptlet argument placeholders.
const patchScriptlet = function(content, args) { const patchScriptlet = function(content, arglist) {
if ( content.startsWith('function') && content.endsWith('}') ) { if ( content.startsWith('function') && content.endsWith('}') ) {
content = `(${content})({{args}});`; content = `(${content})({{args}});`;
} }
if ( args.startsWith('{') && args.endsWith('}') ) { if ( arglist.length === 0 ) {
return content.replace('{{args}}', args);
}
if ( args === '' ) {
return content.replace('{{args}}', ''); return content.replace('{{args}}', '');
} }
const arglist = []; if ( arglist.length === 1 ) {
let s = args; if ( arglist[0].startsWith('{') && arglist[0].endsWith('}') ) {
let len = s.length; return content.replace('{{args}}', arglist[0]);
let beg = 0, pos = 0;
let i = 1;
while ( beg < len ) {
pos = s.indexOf(',', pos);
// Escaped comma? If so, skip.
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
s = s.slice(0, pos - 1) + s.slice(pos);
len -= 1;
continue;
} }
if ( pos === -1 ) { pos = len; }
arglist.push(s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&'));
beg = pos = pos + 1;
i++;
} }
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( return content.replace('{{args}}',
'{{args}}',
arglist.map(a => `'${a}'`).join(', ').replace(/\$/g, '$$$') arglist.map(a => `'${a}'`).join(', ').replace(/\$/g, '$$$')
); );
}; };
const decompile = function(json) {
const args = JSON.parse(json).map(s => s.replace(/,/g, '\\,'));
if ( args.length === 0 ) { return '+js()'; }
return `+js(${args.join(', ')})`;
};
/******************************************************************************/
scriptletFilteringEngine.logFilters = function(tabId, url, filters) { scriptletFilteringEngine.logFilters = function(tabId, url, filters) {
if ( typeof filters !== 'string' ) { return; } if ( typeof filters !== 'string' ) { return; }
const fctxt = µb.filteringContext const fctxt = µb.filteringContext
@ -303,7 +268,7 @@ scriptletFilteringEngine.compile = function(parser, writer) {
if ( normalized === undefined ) { return; } if ( normalized === undefined ) { return; }
// Tokenless is meaningful only for exception filters. // Tokenless is meaningful only for exception filters.
if ( normalized === '+js()' && isException === false ) { return; } if ( normalized === '[]' && isException === false ) { return; }
if ( parser.hasOptions() === false ) { if ( parser.hasOptions() === false ) {
if ( isException ) { if ( isException ) {
@ -329,11 +294,6 @@ scriptletFilteringEngine.compile = function(parser, writer) {
} }
}; };
// 01234567890123456789
// +js(token[, arg[, ...]])
// ^ ^
// 4 -1
scriptletFilteringEngine.fromCompiledContent = function(reader) { scriptletFilteringEngine.fromCompiledContent = function(reader) {
reader.select('SCRIPTLET_FILTERS'); reader.select('SCRIPTLET_FILTERS');
@ -347,7 +307,7 @@ scriptletFilteringEngine.fromCompiledContent = function(reader) {
duplicates.add(fingerprint); duplicates.add(fingerprint);
const args = reader.args(); const args = reader.args();
if ( args.length < 4 ) { continue; } if ( args.length < 4 ) { continue; }
scriptletDB.store(args[1], args[2], args[3].slice(4, -1)); scriptletDB.store(args[1], args[2], args[3]);
} }
}; };
@ -387,7 +347,7 @@ scriptletFilteringEngine.retrieve = function(request) {
if ( $scriptlets.size === 0 ) { return; } if ( $scriptlets.size === 0 ) { return; }
// Wholly disable scriptlet injection? // Wholly disable scriptlet injection?
if ( $exceptions.has('') ) { if ( $exceptions.has('[]') ) {
return { return {
filters: [ filters: [
{ tabId: request.tabId, url: request.url, filter: '#@#+js()' } { tabId: request.tabId, url: request.url, filter: '#@#+js()' }
@ -417,8 +377,8 @@ scriptletFilteringEngine.retrieve = function(request) {
mainWorld: mainWorldCode.join('\n\n'), mainWorld: mainWorldCode.join('\n\n'),
isolatedWorld: isolatedWorldCode.join('\n\n'), isolatedWorld: isolatedWorldCode.join('\n\n'),
filters: [ filters: [
...Array.from($scriptlets).map(s => `##+js(${s})`), ...Array.from($scriptlets).map(s => `##${decompile(s)}`),
...Array.from($exceptions).map(s => `#@#+js(${s})`), ...Array.from($exceptions).map(s => `#@#${decompile(s)}`),
].join('\n'), ].join('\n'),
}; };
scriptletCache.add(hostname, cacheDetails); scriptletCache.add(hostname, cacheDetails);
@ -519,7 +479,10 @@ scriptletFilteringEngine.toSelfie = function() {
}; };
scriptletFilteringEngine.fromSelfie = function(selfie) { scriptletFilteringEngine.fromSelfie = function(selfie) {
if ( selfie instanceof Object === false ) { return false; }
if ( selfie.version !== VERSION ) { return false; }
scriptletDB.fromSelfie(selfie); scriptletDB.fromSelfie(selfie);
return true;
}; };
/******************************************************************************/ /******************************************************************************/

View File

@ -24,7 +24,8 @@
/******************************************************************************/ /******************************************************************************/
const StaticExtFilteringHostnameDB = class { const StaticExtFilteringHostnameDB = class {
constructor(nBits, selfie = undefined) { constructor(nBits, version = 0) {
this.version = version;
this.nBits = nBits; this.nBits = nBits;
this.strToIdMap = new Map(); this.strToIdMap = new Map();
this.hostnameToSlotIdMap = new Map(); this.hostnameToSlotIdMap = new Map();
@ -35,9 +36,6 @@ const StaticExtFilteringHostnameDB = class {
// Array of strings (selectors and pseudo-selectors) // Array of strings (selectors and pseudo-selectors)
this.strSlots = []; this.strSlots = [];
this.size = 0; this.size = 0;
if ( selfie !== undefined ) {
this.fromSelfie(selfie);
}
this.cleanupTimer = vAPI.defer.create(( ) => { this.cleanupTimer = vAPI.defer.create(( ) => {
this.strToIdMap.clear(); this.strToIdMap.clear();
}); });
@ -142,6 +140,7 @@ const StaticExtFilteringHostnameDB = class {
toSelfie() { toSelfie() {
return { return {
version: this.version,
hostnameToSlotIdMap: Array.from(this.hostnameToSlotIdMap), hostnameToSlotIdMap: Array.from(this.hostnameToSlotIdMap),
regexToSlotIdMap: Array.from(this.regexToSlotIdMap), regexToSlotIdMap: Array.from(this.regexToSlotIdMap),
hostnameSlots: this.hostnameSlots, hostnameSlots: this.hostnameSlots,

View File

@ -168,9 +168,11 @@ staticExtFilteringEngine.fromSelfie = function(path) {
} }
if ( selfie instanceof Object === false ) { return false; } if ( selfie instanceof Object === false ) { return false; }
cosmeticFilteringEngine.fromSelfie(selfie.cosmetic); cosmeticFilteringEngine.fromSelfie(selfie.cosmetic);
scriptletFilteringEngine.fromSelfie(selfie.scriptlets);
httpheaderFilteringEngine.fromSelfie(selfie.httpHeaders); httpheaderFilteringEngine.fromSelfie(selfie.httpHeaders);
htmlFilteringEngine.fromSelfie(selfie.html); htmlFilteringEngine.fromSelfie(selfie.html);
if ( scriptletFilteringEngine.fromSelfie(selfie.scriptlets) === false ) {
return false;
}
return true; return true;
}); });
}; };

View File

@ -85,6 +85,7 @@ export const AST_FLAG_HAS_ERROR = 1 << iota++;
export const AST_FLAG_IS_EXCEPTION = 1 << iota++; export const AST_FLAG_IS_EXCEPTION = 1 << iota++;
export const AST_FLAG_EXT_STRONG = 1 << iota++; export const AST_FLAG_EXT_STRONG = 1 << iota++;
export const AST_FLAG_EXT_STYLE = 1 << iota++; export const AST_FLAG_EXT_STYLE = 1 << iota++;
export const AST_FLAG_EXT_SCRIPTLET_ADG = 1 << iota++;
export const AST_FLAG_NET_PATTERN_LEFT_HNANCHOR = 1 << iota++; export const AST_FLAG_NET_PATTERN_LEFT_HNANCHOR = 1 << iota++;
export const AST_FLAG_NET_PATTERN_RIGHT_PATHANCHOR = 1 << iota++; export const AST_FLAG_NET_PATTERN_RIGHT_PATHANCHOR = 1 << iota++;
export const AST_FLAG_NET_PATTERN_LEFT_ANCHOR = 1 << iota++; export const AST_FLAG_NET_PATTERN_LEFT_ANCHOR = 1 << iota++;
@ -793,6 +794,10 @@ export class AstFilterParser {
// TODO: mind maxTokenLength // TODO: mind maxTokenLength
this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/; this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/;
this.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/; this.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/;
this.reOddTrailingEscape = /(?:^|[^\\])(?:\\\\)*\\$/;
this.reUnescapeCommas = /((?:^|[^\\])(?:\\\\)*)\\,/g;
this.reUnescapeSingleQuotes = /((?:^|[^\\])(?:\\\\)*)\\'/g;
this.reUnescapeDoubleQuotes = /((?:^|[^\\])(?:\\\\)*)\\"/g;
} }
parse(raw) { parse(raw) {
@ -2070,7 +2075,13 @@ export class AstFilterParser {
parentEnd parentEnd
); );
this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_RAW, next); this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_RAW, next);
this.linkDown(next, this.parseExtPattern(next)); const down = this.parseExtPattern(next);
if ( down !== 0 ) {
this.linkDown(next, down);
} else {
this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR);
}
this.linkRight(prev, next); this.linkRight(prev, next);
this.validateExt(); this.validateExt();
return this.throwHeadNode(head); return this.throwHeadNode(head);
@ -2079,6 +2090,7 @@ export class AstFilterParser {
extFlagsFromAnchor(anchorBeg) { extFlagsFromAnchor(anchorBeg) {
let c = this.charCodeAt(anchorBeg+1) ; let c = this.charCodeAt(anchorBeg+1) ;
if ( c === 0x23 /* # */ ) { return 0; } if ( c === 0x23 /* # */ ) { return 0; }
if ( c === 0x25 /* % */ ) { return AST_FLAG_EXT_SCRIPTLET_ADG; }
if ( c === 0x3F /* ? */ ) { return AST_FLAG_EXT_STRONG; } if ( c === 0x3F /* ? */ ) { return AST_FLAG_EXT_STRONG; }
if ( c === 0x24 /* $ */ ) { if ( c === 0x24 /* $ */ ) {
c = this.charCodeAt(anchorBeg+2); c = this.charCodeAt(anchorBeg+2);
@ -2123,15 +2135,24 @@ export class AstFilterParser {
parseExtPattern(parent) { parseExtPattern(parent) {
const c = this.charCodeAt(this.nodes[parent+NODE_BEG_INDEX]); const c = this.charCodeAt(this.nodes[parent+NODE_BEG_INDEX]);
// ##+js(...) // ##+js(...)
if ( c === 0x2B /* '+' */ ) { if ( c === 0x2B /* + */ ) {
const s = this.getNodeString(parent); const s = this.getNodeString(parent);
if ( /^\+js\(.*\)$/.exec(s) !== null ) { if ( /^\+js\(.*\)$/.exec(s) !== null ) {
this.astTypeFlavor = AST_TYPE_EXTENDED_SCRIPTLET; this.astTypeFlavor = AST_TYPE_EXTENDED_SCRIPTLET;
return this.parseExtPatternScriptlet(parent); return this.parseExtPatternScriptlet(parent);
} }
} }
// #%#//scriptlet(...)
if ( this.getFlags(AST_FLAG_EXT_SCRIPTLET_ADG) ) {
const s = this.getNodeString(parent);
if ( /^\/\/scriptlet\(.*\)$/.exec(s) !== null ) {
this.astTypeFlavor = AST_TYPE_EXTENDED_SCRIPTLET;
return this.parseExtPatternScriptlet(parent);
}
return 0;
}
// ##^... | ##^responseheader(...) // ##^... | ##^responseheader(...)
if ( c === 0x5E /* '^' */ ) { if ( c === 0x5E /* ^ */ ) {
const s = this.getNodeString(parent); const s = this.getNodeString(parent);
if ( this.reResponseheaderPattern.test(s) ) { if ( this.reResponseheaderPattern.test(s) ) {
this.astTypeFlavor = AST_TYPE_EXTENDED_RESPONSEHEADER; this.astTypeFlavor = AST_TYPE_EXTENDED_RESPONSEHEADER;
@ -2149,24 +2170,14 @@ export class AstFilterParser {
const beg = this.nodes[parent+NODE_BEG_INDEX]; const beg = this.nodes[parent+NODE_BEG_INDEX];
const end = this.nodes[parent+NODE_END_INDEX]; const end = this.nodes[parent+NODE_END_INDEX];
const s = this.getNodeString(parent); const s = this.getNodeString(parent);
const rawArg0 = beg + 4; const rawArg0 = beg + (s.startsWith('+js') ? 4 : 12);
const rawArg1 = end - 1; const rawArg1 = end - 1;
const head = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, beg, rawArg0); const head = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, beg, rawArg0);
let prev = head, next = 0; let prev = head, next = 0;
const trimmedArg0 = rawArg0 + this.leftWhitespaceCount(s); next = this.allocTypedNode(NODE_TYPE_EXT_PATTERN_SCRIPTLET, rawArg0, rawArg1);
const trimmedArg1 = rawArg1 - this.rightWhitespaceCount(s);
if ( trimmedArg0 !== rawArg0 ) {
next = this.allocTypedNode(NODE_TYPE_WHITESPACE, rawArg0, trimmedArg0);
prev = this.linkRight(prev, next);
}
next = this.allocTypedNode(NODE_TYPE_EXT_PATTERN_SCRIPTLET, trimmedArg0, trimmedArg1);
this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_SCRIPTLET, next); this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_SCRIPTLET, next);
this.linkDown(next, this.parseExtPatternScriptletArgs(next)); this.linkDown(next, this.parseExtPatternScriptletArgs(next));
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
if ( trimmedArg1 !== rawArg1 ) {
next = this.allocTypedNode(NODE_TYPE_WHITESPACE, trimmedArg1, rawArg1);
prev = this.linkRight(prev, next);
}
next = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, rawArg1, end); next = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, rawArg1, end);
this.linkRight(prev, next); this.linkRight(prev, next);
return head; return head;
@ -2181,65 +2192,51 @@ export class AstFilterParser {
const s = this.getNodeString(parent); const s = this.getNodeString(parent);
const argsEnd = s.length; const argsEnd = s.length;
// token // token
let argEnd = this.indexOfNextScriptletArgSeparator(s, 0); const details = this.parseExtPatternScriptletArg(s, 0);
let rawArg = s.slice(0, argEnd); if ( details.argBeg > 0 ) {
let argBodyBeg = this.leftWhitespaceCount(rawArg);
if ( argBodyBeg !== 0 ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION, NODE_TYPE_EXT_DECORATION,
parentBeg, parentBeg,
parentBeg + argBodyBeg parentBeg + details.argBeg
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
} }
let argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg); const token = s.slice(details.argBeg, details.argEnd);
rawArg = s.slice(argBodyBeg, argBodyEnd); const tokenEnd = details.argEnd - (token.endsWith('.js') ? 3 : 0);
const tokenEnd = rawArg.endsWith('.js')
? argBodyEnd - 3
: argBodyEnd;
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN,
parentBeg + argBodyBeg, parentBeg + details.argBeg,
parentBeg + tokenEnd parentBeg + tokenEnd
); );
if ( details.failed ) {
this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR);
}
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
// ignore pointless `.js` if ( tokenEnd < details.argEnd ) {
if ( tokenEnd !== argBodyEnd ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_IGNORE, NODE_TYPE_IGNORE,
parentBeg + argBodyEnd - 3, parentBeg + tokenEnd,
parentBeg + argBodyEnd parentBeg + details.argEnd
);
prev = this.linkRight(prev, next);
}
if ( details.quoteEnd < argsEnd ) {
next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION,
parentBeg + details.argEnd,
parentBeg + details.separatorEnd
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
} }
// all args // all args
argBodyBeg = argEnd + 1;
const rawArgs = s.slice(argBodyBeg, argsEnd);
argBodyBeg += this.leftWhitespaceCount(rawArgs);
next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd,
parentBeg + argBodyBeg
);
prev = this.linkRight(prev, next);
argBodyEnd = argsEnd - this.rightWhitespaceCount(rawArgs);
if ( argBodyBeg !== argBodyEnd ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS,
parentBeg + argBodyBeg, parentBeg + details.separatorEnd,
parentBeg + argBodyEnd parentBeg + argsEnd
); );
this.linkDown(next, this.parseExtPatternScriptletArglist(next)); this.linkDown(next, this.parseExtPatternScriptletArglist(next));
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
}
if ( argBodyEnd !== argsEnd ) {
next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd,
parentBeg + argsEnd
);
prev = this.linkRight(prev, next);
}
return this.throwHeadNode(head); return this.throwHeadNode(head);
} }
@ -2269,48 +2266,131 @@ export class AstFilterParser {
const head = this.allocHeadNode(); const head = this.allocHeadNode();
const argsEnd = s.length; const argsEnd = s.length;
let prev = head; let prev = head;
let argBodyBeg = 0, argBodyEnd = 0, argEnd = 0; let decorationBeg = 0;
let t = ''; let i = 0;
while ( argBodyBeg < argsEnd ) { for (;;) {
argEnd = this.indexOfNextScriptletArgSeparator(s, argBodyBeg); const details = this.parseExtPatternScriptletArg(s, i);
t = s.slice(argBodyBeg, argEnd); if ( decorationBeg < details.argBeg ) {
argBodyEnd = argEnd - this.rightWhitespaceCount(t);
next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG,
parentBeg + argBodyBeg,
parentBeg + argBodyEnd
);
prev = this.linkRight(prev, next);
if ( argEnd === argsEnd ) { break; }
t = s.slice(argEnd + 1);
argBodyBeg = argEnd + 1 + this.leftWhitespaceCount(t);
if ( argBodyEnd !== argBodyBeg ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION, NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd, parentBeg + decorationBeg,
parentBeg + argBodyBeg parentBeg + details.argBeg
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
} }
if ( i === argsEnd ) { break; }
next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG,
parentBeg + details.argBeg,
parentBeg + details.argEnd
);
if ( details.transform ) {
this.setNodeTransform(next, this.normalizeScriptletArg(
s.slice(details.argBeg, details.argEnd),
details.separatorCode
));
}
prev = this.linkRight(prev, next);
if ( details.failed ) {
this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR);
}
decorationBeg = details.argEnd;
i = details.separatorEnd;
} }
return this.throwHeadNode(head); return this.throwHeadNode(head);
} }
indexOfNextScriptletArgSeparator(pattern, beg = 0) { parseExtPatternScriptletArg(pattern, beg = 0) {
const patternEnd = pattern.length; if ( this.parseExtPatternScriptletArg.details === undefined ) {
if ( beg >= patternEnd ) { return patternEnd; } this.parseExtPatternScriptletArg.details = {
const nextComma = pattern.indexOf(',', beg); quoteBeg: 0, argBeg: 0, argEnd: 0, quoteEnd: 0,
if ( nextComma === -1 ) { return patternEnd; } separatorCode: 0, separatorBeg: 0, separatorEnd: 0,
// An odd number of backslashes immediately before the comma means transform: false, failed: false,
// it's being escaped };
let backslashCount = 0;
for ( let i = nextComma; i > beg; i-- ) {
if ( pattern.charCodeAt(i-1) !== 0x5C /* \ */ ) { break; }
backslashCount += 1;
} }
return (backslashCount & 1) === 0 const details = this.parseExtPatternScriptletArg.details;
? nextComma const len = pattern.length;
: this.indexOfNextScriptletArgSeparator(pattern, nextComma + 1); details.quoteBeg = beg + this.leftWhitespaceCount(pattern.slice(beg));
details.failed = false;
const qc = pattern.charCodeAt(details.quoteBeg);
if ( qc === 0x22 /* " */ || qc === 0x27 /* ' */ ) {
details.separatorCode = qc;
details.argBeg = details.argEnd = details.quoteBeg + 1;
details.transform = false;
this.indexOfNextScriptletArgSeparator(pattern, details);
if ( details.argEnd !== len ) {
details.quoteEnd = details.argEnd + 1;
details.separatorBeg = details.separatorEnd = details.quoteEnd;
details.separatorEnd += this.leftWhitespaceCount(pattern.slice(details.quoteEnd));
if ( details.separatorEnd === len ) { return details; }
if ( pattern.charCodeAt(details.separatorEnd) === 0x2C ) {
details.separatorEnd += 1;
return details;
}
}
}
details.separatorCode = 0x2C /* , */;
details.argBeg = details.argEnd = details.quoteBeg;
details.transform = false;
this.indexOfNextScriptletArgSeparator(pattern, details);
details.separatorBeg = details.separatorEnd = details.argEnd;
if ( details.separatorBeg < len ) {
details.separatorEnd += 1;
}
details.argEnd -= this.rightWhitespaceCount(pattern.slice(0, details.separatorBeg));
details.quoteEnd = details.argEnd;
if ( this.getFlags(AST_FLAG_EXT_SCRIPTLET_ADG) ) {
details.failed = true;
}
return details;
}
indexOfNextScriptletArgSeparator(pattern, details) {
const separatorChar = String.fromCharCode(details.separatorCode);
while ( details.argEnd < pattern.length ) {
const pos = pattern.indexOf(separatorChar, details.argEnd);
if ( pos === -1 ) {
return (details.argEnd = pattern.length);
}
if ( this.reOddTrailingEscape.test(pattern.slice(0, pos)) === false ) {
return (details.argEnd = pos);
}
details.transform = true;
details.argEnd = pos + 1;
}
}
normalizeScriptletArg(arg, separatorCode) {
if ( separatorCode === 0x22 /* " */ ) {
if ( arg.includes('"') === false ) { return; }
return arg.replace(this.reUnescapeDoubleQuotes, '$1"');
}
if ( separatorCode === 0x27 /* ' */ ) {
if ( arg.includes("'") === false ) { return; }
return arg.replace(this.reUnescapeSingleQuotes, "$1'");
}
if ( arg.includes(',') === false ) { return; }
return arg.replace(this.reUnescapeCommas, '$1,');
}
getScripletArgs() {
const args = [];
if ( this.isScriptletFilter() === false ) { return args; }
const root = this.getBranchFromType(NODE_TYPE_EXT_PATTERN_SCRIPTLET);
const walker = this.getWalker(root);
for ( let node = walker.next(); node !== 0; node = walker.next() ) {
switch ( this.getNodeType(node) ) {
case NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN:
case NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG:
args.push(this.getNodeTransform(node));
break;
default:
break;
}
}
walker.dispose();
return args;
} }
parseExtPatternResponseheader(parent) { parseExtPatternResponseheader(parent) {