2414 lines
86 KiB
JavaScript
2414 lines
86 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin - a browser extension to block requests.
|
|
Copyright (C) 2020-present Raymond Hill
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
Home: https://github.com/gorhill/uBlock
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/*******************************************************************************
|
|
|
|
The goal is for the static filtering parser to avoid external
|
|
dependencies to other code in the project.
|
|
|
|
Roughly, this is how things work: each input string (passed to analyze())
|
|
is decomposed into a minimal set of distinct slices. Each slice is a
|
|
triplet of integers consisiting of:
|
|
|
|
- a bit vector describing the characters inside the slice
|
|
- an index of where in the origin string the slice starts
|
|
- a length for the number of character in the slice
|
|
|
|
Slice descriptor are all flatly stored in an array of integer so as to
|
|
avoid the need for a secondary data structure. Example:
|
|
|
|
raw string: toto.com
|
|
toto . com
|
|
| | |
|
|
slices: [ 65536, 0, 4, 1024, 4, 1, 65536, 5, 3 ]
|
|
^ ^ ^
|
|
| | |
|
|
| | +---- number of characters
|
|
| +---- index in raw string
|
|
+---- bit vector
|
|
|
|
Thus the number of slices to describe the `toto.com` string is made of
|
|
three slices, encoded into nine integers.
|
|
|
|
Once a string has been encoded into slices, the parser will only work
|
|
with those slices in order to parse the filter represented by the
|
|
string, rather than performing string operations on the original string.
|
|
The result is that parsing is essentially number-crunching operations
|
|
rather than string operations, for the most part (potentially opening
|
|
the door for WASM code in the future to parse static filters).
|
|
|
|
The array used to hold the slices is reused across string analysis, in
|
|
order to eliminate memory churning.
|
|
|
|
Above the slices, there are various span objects used to describe
|
|
consecutive sequences of slices and which are filled in as a result
|
|
of parsing.
|
|
|
|
**/
|
|
|
|
{
|
|
// >>>>> start of local scope
|
|
|
|
/******************************************************************************/
|
|
|
|
const Parser = class {
|
|
constructor(options = {}) {
|
|
this.interactive = options.interactive === true;
|
|
this.raw = '';
|
|
this.slices = [];
|
|
this.leftSpaceSpan = new Span();
|
|
this.exceptionSpan = new Span();
|
|
this.patternLeftAnchorSpan = new Span();
|
|
this.patternSpan = new Span();
|
|
this.patternRightAnchorSpan = new Span();
|
|
this.optionsAnchorSpan = new Span();
|
|
this.optionsSpan = new Span();
|
|
this.commentSpan = new Span();
|
|
this.rightSpaceSpan = new Span();
|
|
this.eolSpan = new Span();
|
|
this.spans = [
|
|
this.leftSpaceSpan,
|
|
this.exceptionSpan,
|
|
this.patternLeftAnchorSpan,
|
|
this.patternSpan,
|
|
this.patternRightAnchorSpan,
|
|
this.optionsAnchorSpan,
|
|
this.optionsSpan,
|
|
this.commentSpan,
|
|
this.rightSpaceSpan,
|
|
this.eolSpan,
|
|
];
|
|
this.patternTokenIterator = new PatternTokenIterator(this);
|
|
this.netOptionsIterator = new NetOptionsIterator(this);
|
|
this.extOptionsIterator = new ExtOptionsIterator(this);
|
|
this.maxTokenLength = Number.MAX_SAFE_INTEGER;
|
|
this.reIsLocalhostRedirect = /(?:0\.0\.0\.0|(?:broadcast|local)host|local|ip6-\w+)\b/;
|
|
this.reHostname = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x5E\x60\x7B-\x7F]+/;
|
|
this.reUnicodeChar = /[^\x00-\x7F]/;
|
|
this.reUnicodeChars = /[^\x00-\x7F]/g;
|
|
this.punycoder = new URL(self.location);
|
|
this.selectorCompiler = new this.SelectorCompiler(this);
|
|
// TODO: reuse for network filtering analysis
|
|
this.result = {
|
|
exception: false,
|
|
raw: '',
|
|
compiled: '',
|
|
pseudoclass: false,
|
|
};
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
this.sliceWritePtr = 0;
|
|
this.category = CATNone;
|
|
this.allBits = 0; // bits found in any slices
|
|
this.patternBits = 0; // bits found in any pattern slices
|
|
this.optionsBits = 0; // bits found in any option slices
|
|
this.flavorBits = 0;
|
|
for ( const span of this.spans ) { span.reset(); }
|
|
this.pattern = '';
|
|
}
|
|
|
|
analyze(raw) {
|
|
this.slice(raw);
|
|
let slot = this.leftSpaceSpan.len;
|
|
if ( slot === this.rightSpaceSpan.i ) { return; }
|
|
|
|
// test for `!`, `#`, or `[`
|
|
if ( hasBits(this.slices[slot], BITLineComment) ) {
|
|
// static extended filter?
|
|
if ( hasBits(this.slices[slot], BITHash) ) {
|
|
this.analyzeExt(slot);
|
|
if ( this.category === CATStaticExtFilter ) { return; }
|
|
}
|
|
// if not `#`, no ambiguity
|
|
this.category = CATComment;
|
|
return;
|
|
}
|
|
|
|
// assume no inline comment
|
|
this.commentSpan.i = this.rightSpaceSpan.i;
|
|
|
|
// extended filtering with options?
|
|
if ( hasBits(this.allBits, BITHash) ) {
|
|
let hashSlot = this.findFirstMatch(slot, BITHash);
|
|
if ( hashSlot !== -1 ) {
|
|
this.analyzeExt(hashSlot);
|
|
if ( this.category === CATStaticExtFilter ) { return; }
|
|
// inline comment? (a space followed by a hash)
|
|
if ( (this.allBits & BITSpace) !== 0 ) {
|
|
for (;;) {
|
|
if ( hasBits(this.slices[hashSlot-3], BITSpace) ) {
|
|
this.commentSpan.i = hashSlot-3;
|
|
this.commentSpan.len = this.rightSpaceSpan.i - hashSlot;
|
|
break;
|
|
}
|
|
hashSlot = this.findFirstMatch(hashSlot + 6, BITHash);
|
|
if ( hashSlot === -1 ) { break; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// assume network filtering
|
|
this.analyzeNet();
|
|
}
|
|
|
|
// Use in syntax highlighting contexts
|
|
analyzeExtra() {
|
|
if ( this.category === CATStaticExtFilter ) {
|
|
this.analyzeExtExtra();
|
|
} else if ( this.category === CATStaticNetFilter ) {
|
|
this.analyzeNetExtra();
|
|
}
|
|
}
|
|
|
|
// Static extended filters are all of the form:
|
|
//
|
|
// 1. options (optional): a comma-separated list of hostnames
|
|
// 2. anchor: regex equivalent => /^#@?[\$\??|%|\?)?#$/
|
|
// 3. pattern
|
|
//
|
|
// Return true if a valid extended filter is found, otherwise false.
|
|
// When a valid extended filter is found:
|
|
// optionsSpan: first slot which contains options
|
|
// optionsAnchorSpan: first slot to anchor
|
|
// patternSpan: first slot to pattern
|
|
analyzeExt(from) {
|
|
let end = this.rightSpaceSpan.i;
|
|
// Number of consecutive #s.
|
|
const len = this.slices[from+2];
|
|
// More than 3 #s is likely to be a comment in a hosts file.
|
|
if ( len > 3 ) { return; }
|
|
if ( len !== 1 ) {
|
|
// If a space immediately follows 2 #s, assume a comment.
|
|
if ( len === 2 ) {
|
|
if ( from+3 === end || hasBits(this.slices[from+3], BITSpace) ) {
|
|
return;
|
|
}
|
|
} else /* len === 3 */ {
|
|
this.splitSlot(from, 2);
|
|
end = this.rightSpaceSpan.i;
|
|
}
|
|
this.optionsSpan.i = this.leftSpaceSpan.i + this.leftSpaceSpan.len;
|
|
this.optionsSpan.len = from - this.optionsSpan.i;
|
|
this.optionsAnchorSpan.i = from;
|
|
this.optionsAnchorSpan.len = 3;
|
|
this.patternSpan.i = from + 3;
|
|
this.patternSpan.len = this.rightSpaceSpan.i - this.patternSpan.i;
|
|
this.category = CATStaticExtFilter;
|
|
this.analyzeExtPattern();
|
|
return;
|
|
}
|
|
let flavorBits = 0;
|
|
let to = from + 3;
|
|
if ( to === end ) { return; }
|
|
// #@...
|
|
// ^
|
|
if ( hasBits(this.slices[to], BITAt) ) {
|
|
if ( this.slices[to+2] !== 1 ) { return; }
|
|
flavorBits |= BITFlavorException;
|
|
to += 3; if ( to === end ) { return; }
|
|
}
|
|
// #$...
|
|
// ^
|
|
if ( hasBits(this.slices[to], BITDollar) ) {
|
|
if ( this.slices[to+2] !== 1 ) { return; }
|
|
flavorBits |= BITFlavorExtStyle;
|
|
to += 3; if ( to === end ) { return; }
|
|
// #$?...
|
|
// ^
|
|
if ( hasBits(this.slices[to], BITQuestion) ) {
|
|
if ( this.slices[to+2] !== 1 ) { return; }
|
|
flavorBits |= BITFlavorExtStrong;
|
|
to += 3; if ( to === end ) { return; }
|
|
}
|
|
}
|
|
// #[%?]...
|
|
// ^^
|
|
else if ( hasBits(this.slices[to], BITPercent | BITQuestion) ) {
|
|
if ( this.slices[to+2] !== 1 ) { return; }
|
|
flavorBits |= hasBits(this.slices[to], BITQuestion)
|
|
? BITFlavorExtStrong
|
|
: BITFlavorUnsupported;
|
|
to += 3; if ( to === end ) { return; }
|
|
}
|
|
// ##...
|
|
// ^
|
|
if ( hasNoBits(this.slices[to], BITHash) ) { return; }
|
|
if ( this.slices[to+2] > 1 ) {
|
|
this.splitSlot(to, 1);
|
|
}
|
|
to += 3;
|
|
this.optionsSpan.i = this.leftSpaceSpan.i + this.leftSpaceSpan.len;
|
|
this.optionsSpan.len = from - this.optionsSpan.i;
|
|
this.optionsAnchorSpan.i = from;
|
|
this.optionsAnchorSpan.len = to - this.optionsAnchorSpan.i;
|
|
this.patternSpan.i = to;
|
|
this.patternSpan.len = this.rightSpaceSpan.i - to;
|
|
this.flavorBits = flavorBits;
|
|
this.category = CATStaticExtFilter;
|
|
this.analyzeExtPattern();
|
|
}
|
|
|
|
analyzeExtPattern() {
|
|
this.result.exception = this.isException();
|
|
this.result.compiled = undefined;
|
|
this.result.pseudoclass = false;
|
|
|
|
let selector = this.strFromSpan(this.patternSpan);
|
|
if ( selector === '' ) {
|
|
this.flavorBits |= BITFlavorUnsupported;
|
|
this.result.raw = '';
|
|
return;
|
|
}
|
|
const { i } = this.patternSpan;
|
|
// ##+js(...)
|
|
if (
|
|
hasBits(this.slices[i], BITPlus) &&
|
|
selector.startsWith('+js(') && selector.endsWith(')')
|
|
) {
|
|
this.flavorBits |= BITFlavorExtScriptlet;
|
|
this.result.raw = selector;
|
|
this.result.compiled = selector.slice(4, -1);
|
|
return;
|
|
}
|
|
// ##^...
|
|
if ( hasBits(this.slices[i], BITCaret) ) {
|
|
this.flavorBits |= BITFlavorExtHTML;
|
|
selector = selector.slice(1);
|
|
}
|
|
// ##...
|
|
else {
|
|
this.flavorBits |= BITFlavorExtCosmetic;
|
|
}
|
|
this.result.raw = selector;
|
|
if ( this.selectorCompiler.compile(selector, this.result) === false ) {
|
|
this.flavorBits |= BITFlavorUnsupported;
|
|
}
|
|
}
|
|
|
|
// Use in syntax highlighting contexts
|
|
analyzeExtExtra() {
|
|
if ( this.hasOptions() ) {
|
|
const { i, len } = this.optionsSpan;
|
|
this.analyzeDomainList(i, i + len, BITComma, 0b11);
|
|
}
|
|
if ( hasBits(this.flavorBits, BITFlavorUnsupported) ) {
|
|
this.markSpan(this.patternSpan, BITError);
|
|
}
|
|
}
|
|
|
|
// Static network filters are all of the form:
|
|
//
|
|
// 1. exception declarator (optional): `@@`
|
|
// 2. left-hand pattern anchor (optional): `||` or `|`
|
|
// 3. pattern: a valid pattern, one of
|
|
// a regex, starting and ending with `/`
|
|
// a sequence of characters with optional wildcard characters
|
|
// wildcard `*` : regex equivalent => /./
|
|
// wildcard `^` : regex equivalent => /[^%.0-9a-z_-]|$/
|
|
// 4. right-hand anchor (optional): `|`
|
|
// 5. options declarator (optional): `$`
|
|
// options: one or more options
|
|
// 6. inline comment (optional): ` #`
|
|
//
|
|
// When a valid static filter is found:
|
|
// exceptionSpan: first slice of exception declarator
|
|
// patternLeftAnchorSpan: first slice to left-hand pattern anchor
|
|
// patternSpan: all slices belonging to pattern
|
|
// patternRightAnchorSpan: first slice to right-hand pattern anchor
|
|
// optionsAnchorSpan: first slice to options anchor
|
|
// optionsSpan: first slice to options
|
|
analyzeNet() {
|
|
let islice = this.leftSpaceSpan.len;
|
|
|
|
// Assume no exception
|
|
this.exceptionSpan.i = this.leftSpaceSpan.len;
|
|
// Exception?
|
|
if (
|
|
islice < this.commentSpan.i &&
|
|
hasBits(this.slices[islice], BITAt)
|
|
) {
|
|
const len = this.slices[islice+2];
|
|
// @@@*, ... => @@, @*, ...
|
|
if ( len >= 2 ) {
|
|
if ( len > 2 ) {
|
|
this.splitSlot(islice, 2);
|
|
}
|
|
this.exceptionSpan.len = 3;
|
|
islice += 3;
|
|
this.flavorBits |= BITFlavorException;
|
|
}
|
|
}
|
|
|
|
// Assume no options
|
|
this.optionsAnchorSpan.i = this.optionsSpan.i = this.commentSpan.i;
|
|
|
|
// Assume all is part of pattern
|
|
this.patternSpan.i = islice;
|
|
this.patternSpan.len = this.optionsAnchorSpan.i - islice;
|
|
|
|
let patternStartIsRegex =
|
|
islice < this.optionsAnchorSpan.i &&
|
|
hasBits(this.slices[islice], BITSlash);
|
|
let patternIsRegex = patternStartIsRegex;
|
|
if ( patternStartIsRegex ) {
|
|
const { i, len } = this.patternSpan;
|
|
patternIsRegex = (
|
|
len === 3 && this.slices[i+2] > 2 ||
|
|
len > 3 && hasBits(this.slices[i+len-3], BITSlash)
|
|
);
|
|
}
|
|
|
|
// If the pattern is not a regex, there might be options.
|
|
if ( patternIsRegex === false ) {
|
|
let optionsBits = 0;
|
|
let i = this.optionsAnchorSpan.i;
|
|
for (;;) {
|
|
i -= 3;
|
|
if ( i < islice ) { break; }
|
|
const bits = this.slices[i];
|
|
if ( hasBits(bits, BITDollar) ) { break; }
|
|
optionsBits |= bits;
|
|
}
|
|
if ( i >= islice ) {
|
|
const len = this.slices[i+2];
|
|
if ( len > 1 ) {
|
|
// https://github.com/gorhill/uBlock/issues/952
|
|
// AdGuard-specific `$$` filters => unsupported.
|
|
if ( this.findFirstOdd(0, BITHostname | BITComma | BITAsterisk) === i ) {
|
|
this.flavorBits |= BITFlavorError;
|
|
if ( this.interactive ) {
|
|
this.markSlices(i, i+3, BITError);
|
|
}
|
|
} else {
|
|
this.splitSlot(i, len - 1);
|
|
i += 3;
|
|
}
|
|
}
|
|
this.patternSpan.len = i - this.patternSpan.i;
|
|
this.optionsAnchorSpan.i = i;
|
|
this.optionsAnchorSpan.len = 3;
|
|
i += 3;
|
|
this.optionsSpan.i = i;
|
|
this.optionsSpan.len = this.commentSpan.i - i;
|
|
this.optionsBits = optionsBits;
|
|
if ( patternStartIsRegex ) {
|
|
const { i, len } = this.patternSpan;
|
|
patternIsRegex = (
|
|
len === 3 && this.slices[i+2] > 2 ||
|
|
len > 3 && hasBits(this.slices[i+len-3], BITSlash)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the pattern is a regex, remember this.
|
|
if ( patternIsRegex ) {
|
|
this.flavorBits |= BITFlavorNetRegex;
|
|
}
|
|
|
|
// Refine by processing pattern anchors.
|
|
//
|
|
// Assume no anchors.
|
|
this.patternLeftAnchorSpan.i = this.patternSpan.i;
|
|
this.patternRightAnchorSpan.i = this.optionsAnchorSpan.i;
|
|
// Not a regex, there might be anchors.
|
|
if ( patternIsRegex === false ) {
|
|
// Left anchor?
|
|
// `|`: anchor to start of URL
|
|
// `||`: anchor to left of a hostname label
|
|
if (
|
|
this.patternSpan.len !== 0 &&
|
|
hasBits(this.slices[this.patternSpan.i], BITPipe)
|
|
) {
|
|
this.patternLeftAnchorSpan.len = 3;
|
|
const len = this.slices[this.patternSpan.i+2];
|
|
// |||*, ... => ||, |*, ...
|
|
if ( len > 2 ) {
|
|
this.splitSlot(this.patternSpan.i, 2);
|
|
} else {
|
|
this.patternSpan.len -= 3;
|
|
}
|
|
this.patternSpan.i += 3;
|
|
this.flavorBits |= len === 1
|
|
? BITFlavorNetLeftURLAnchor
|
|
: BITFlavorNetLeftHnAnchor;
|
|
}
|
|
// Right anchor?
|
|
// `|`: anchor to end of URL
|
|
// `^`: anchor to end of hostname, when other conditions are
|
|
// fulfilled:
|
|
// the pattern is hostname-anchored on the left
|
|
// the pattern is made only of hostname characters
|
|
if ( this.patternSpan.len !== 0 ) {
|
|
const lastPatternSlice = this.patternSpan.len > 3
|
|
? this.patternRightAnchorSpan.i - 3
|
|
: this.patternSpan.i;
|
|
const bits = this.slices[lastPatternSlice];
|
|
if ( (bits & BITPipe) !== 0 ) {
|
|
this.patternRightAnchorSpan.i = lastPatternSlice;
|
|
this.patternRightAnchorSpan.len = 3;
|
|
const len = this.slices[this.patternRightAnchorSpan.i+2];
|
|
// ..., ||* => ..., |*, |
|
|
if ( len > 1 ) {
|
|
this.splitSlot(this.patternRightAnchorSpan.i, len - 1);
|
|
this.patternRightAnchorSpan.i += 3;
|
|
} else {
|
|
this.patternSpan.len -= 3;
|
|
}
|
|
this.flavorBits |= BITFlavorNetRightURLAnchor;
|
|
} else if (
|
|
hasBits(bits, BITCaret) &&
|
|
this.slices[lastPatternSlice+2] === 1 &&
|
|
hasBits(this.flavorBits, BITFlavorNetLeftHnAnchor) &&
|
|
this.skipUntilNot(
|
|
this.patternSpan.i,
|
|
lastPatternSlice,
|
|
BITHostname
|
|
) === lastPatternSlice
|
|
) {
|
|
this.patternRightAnchorSpan.i = lastPatternSlice;
|
|
this.patternRightAnchorSpan.len = 3;
|
|
this.patternSpan.len -= 3;
|
|
this.flavorBits |= BITFlavorNetRightHnAnchor;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collate useful pattern bits information for further use.
|
|
//
|
|
// https://github.com/gorhill/httpswitchboard/issues/15
|
|
// When parsing a hosts file, ensure localhost et al. don't end up
|
|
// in the pattern. To accomplish this we establish the rule that
|
|
// if a pattern contains a space character, the pattern will be only
|
|
// the part following the space character.
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1118
|
|
// Patterns with more than one space are dubious.
|
|
{
|
|
const { i, len } = this.patternSpan;
|
|
let j = len;
|
|
for (;;) {
|
|
if ( j === 0 ) { break; }
|
|
j -= 3;
|
|
const bits = this.slices[i+j];
|
|
if ( hasBits(bits, BITSpace) ) { break; }
|
|
this.patternBits |= bits;
|
|
}
|
|
if ( j !== 0 ) {
|
|
let dubious = false;
|
|
for ( let k = this.patternSpan.i; k < j; k += 3 ) {
|
|
if ( hasNoBits(this.slices[k], BITSpace) ) { continue; }
|
|
this.patternBits |= BITSpace;
|
|
if ( this.interactive ) {
|
|
this.markSlices(this.patternSpan.i, j, BITError);
|
|
}
|
|
dubious = true;
|
|
break;
|
|
}
|
|
if ( dubious === false ) {
|
|
this.patternSpan.i += j + 3;
|
|
this.patternSpan.len -= j + 3;
|
|
if ( this.reIsLocalhostRedirect.test(this.getNetPattern()) ) {
|
|
this.flavorBits |= BITFlavorIgnore;
|
|
}
|
|
if ( this.interactive ) {
|
|
this.markSlices(0, this.patternSpan.i, BITIgnore);
|
|
}
|
|
}
|
|
// TODO: test again for regex?
|
|
}
|
|
}
|
|
|
|
// Pointless wildcards and anchoring:
|
|
// - Eliminate leading wildcard not followed by a pattern token slice
|
|
// - Eliminate trailing wildcard not preceded by a pattern token slice
|
|
// - Eliminate pattern anchoring when irrelevant
|
|
//
|
|
// Leading wildcard history:
|
|
// https://github.com/gorhill/uBlock/issues/1669#issuecomment-224822448
|
|
// Remove pointless leading *.
|
|
// https://github.com/gorhill/uBlock/issues/3034
|
|
// We can remove anchoring if we need to match all at the start.
|
|
//
|
|
// Trailing wildcard history:
|
|
// https://github.com/gorhill/uBlock/issues/3034
|
|
// We can remove anchoring if we need to match all at the end.
|
|
{
|
|
let { i, len } = this.patternSpan;
|
|
// Pointless leading wildcard
|
|
if (
|
|
len > 3 &&
|
|
hasBits(this.slices[i], BITAsterisk) &&
|
|
hasNoBits(this.slices[i+3], BITPatternToken)
|
|
) {
|
|
this.slices[i] |= BITIgnore;
|
|
i += 3; len -= 3;
|
|
this.patternSpan.i = i;
|
|
this.patternSpan.len = len;
|
|
// We can ignore left-hand pattern anchor
|
|
if ( this.patternLeftAnchorSpan.len !== 0 ) {
|
|
this.slices[this.patternLeftAnchorSpan.i] |= BITIgnore;
|
|
this.flavorBits &= ~BITFlavorNetLeftAnchor;
|
|
}
|
|
}
|
|
// Pointless trailing wildcard
|
|
if (
|
|
len > 3 &&
|
|
hasBits(this.slices[i+len-3], BITAsterisk) &&
|
|
hasNoBits(this.slices[i+len-6], BITPatternToken)
|
|
) {
|
|
// Ignore only if the pattern would not end up looking like
|
|
// a regex.
|
|
if (
|
|
hasNoBits(this.slices[i], BITSlash) ||
|
|
hasNoBits(this.slices[i+len-6], BITSlash)
|
|
) {
|
|
this.slices[i+len-3] |= BITIgnore;
|
|
}
|
|
len -= 3;
|
|
this.patternSpan.len = len;
|
|
// We can ignore right-hand pattern anchor
|
|
if ( this.patternRightAnchorSpan.len !== 0 ) {
|
|
this.slices[this.patternRightAnchorSpan.i] |= BITIgnore;
|
|
this.flavorBits &= ~BITFlavorNetRightAnchor;
|
|
}
|
|
}
|
|
// Pointless left-hand pattern anchoring
|
|
if (
|
|
(
|
|
len === 0 ||
|
|
len !== 0 && hasBits(this.slices[i], BITAsterisk)
|
|
) &&
|
|
hasBits(this.flavorBits, BITFlavorNetLeftAnchor)
|
|
) {
|
|
this.slices[this.patternLeftAnchorSpan.i] |= BITIgnore;
|
|
this.flavorBits &= ~BITFlavorNetLeftAnchor;
|
|
}
|
|
// Pointless right-hand pattern anchoring
|
|
if (
|
|
(
|
|
len === 0 ||
|
|
len !== 0 && hasBits(this.slices[i+len-3], BITAsterisk)
|
|
) &&
|
|
hasBits(this.flavorBits, BITFlavorNetRightAnchor)
|
|
) {
|
|
this.slices[this.patternRightAnchorSpan.i] |= BITIgnore;
|
|
this.flavorBits &= ~BITFlavorNetRightAnchor;
|
|
}
|
|
}
|
|
|
|
this.category = CATStaticNetFilter;
|
|
}
|
|
|
|
analyzeNetExtra() {
|
|
// Validate regex
|
|
if ( this.patternIsRegex() ) {
|
|
try {
|
|
void new RegExp(this.getNetPattern());
|
|
}
|
|
catch (ex) {
|
|
this.markSpan(this.patternSpan, BITError);
|
|
}
|
|
} else if (
|
|
this.patternIsDubious() || (
|
|
this.patternHasUnicode() &&
|
|
this.toASCII(true) === false
|
|
)
|
|
) {
|
|
this.markSpan(this.patternSpan, BITError);
|
|
}
|
|
this.netOptionsIterator.init();
|
|
}
|
|
|
|
analyzeDomainList(from, to, bitSeparator, optionBits) {
|
|
if ( from >= to ) { return; }
|
|
let beg = from;
|
|
// Dangling leading separator?
|
|
if ( hasBits(this.slices[beg], bitSeparator) ) {
|
|
this.markSlices(beg, beg + 3, BITError);
|
|
beg += 3;
|
|
}
|
|
while ( beg < to ) {
|
|
let end = this.skipUntil(beg, to, bitSeparator);
|
|
if ( end < to && this.slices[end+2] !== 1 ) {
|
|
this.markSlices(end, end + 3, BITError);
|
|
}
|
|
if ( this.analyzeDomain(beg, end, optionBits) === false ) {
|
|
this.markSlices(beg, end, BITError);
|
|
}
|
|
beg = end + 3;
|
|
}
|
|
// Dangling trailing separator?
|
|
if ( hasBits(this.slices[to-3], bitSeparator) ) {
|
|
this.markSlices(to - 3, to, BITError);
|
|
}
|
|
}
|
|
|
|
// bits:
|
|
// 0: can use entity-based hostnames
|
|
// 1: can use single wildcard
|
|
analyzeDomain(from, to, optionBits) {
|
|
const { slices } = this;
|
|
let len = to - from;
|
|
if ( len === 0 ) { return false; }
|
|
const not = hasBits(slices[from], BITTilde);
|
|
if ( not ) {
|
|
if ( (optionBits & 0b01) === 0 || slices[from+2] > 1 ) { return false; }
|
|
from += 3;
|
|
len -= 3;
|
|
}
|
|
if ( len === 0 ) { return false; }
|
|
// One slice only, check for single asterisk
|
|
if (
|
|
len === 3 &&
|
|
not === false &&
|
|
(optionBits & 0b10) !== 0 &&
|
|
hasBits(slices[from], BITAsterisk)
|
|
) {
|
|
return slices[from+2] === 1;
|
|
}
|
|
// First slice must be regex-equivalent of `\w`
|
|
if ( hasNoBits(slices[from], BITRegexWord | BITUnicode) ) { return false; }
|
|
// Last slice
|
|
if ( len > 3 ) {
|
|
const last = to - 3;
|
|
if ( hasBits(slices[last], BITAsterisk) ) {
|
|
if (
|
|
(optionBits & 0b01) === 0 ||
|
|
len < 9 ||
|
|
slices[last+2] > 1 ||
|
|
hasNoBits(slices[last-3], BITPeriod)
|
|
) {
|
|
return false;
|
|
}
|
|
} else if ( hasNoBits(slices[to-3], BITAlphaNum | BITUnicode) ) {
|
|
return false;
|
|
}
|
|
}
|
|
// Middle slices
|
|
if ( len > 6 ) {
|
|
for ( let i = from + 3; i < to - 3; i += 3 ) {
|
|
const bits = slices[i];
|
|
if ( hasNoBits(bits, BITHostname) ) { return false; }
|
|
if ( hasBits(bits, BITPeriod) && slices[i+2] > 1 ) {
|
|
return false;
|
|
}
|
|
if (
|
|
hasBits(bits, BITDash) && (
|
|
hasNoBits(slices[i-3], BITRegexWord | BITUnicode) ||
|
|
hasNoBits(slices[i+3], BITRegexWord | BITUnicode)
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
slice(raw) {
|
|
this.reset();
|
|
this.raw = raw;
|
|
const rawEnd = raw.length;
|
|
if ( rawEnd === 0 ) { return; }
|
|
// All unicode characters are allowed in hostname
|
|
const unicodeBits = BITUnicode | BITAlpha;
|
|
// Create raw slices
|
|
const slices = this.slices;
|
|
let ptr = this.sliceWritePtr;
|
|
let c = raw.charCodeAt(0);
|
|
let aBits = c < 0x80 ? charDescBits[c] : unicodeBits;
|
|
slices[ptr+0] = aBits;
|
|
slices[ptr+1] = 0;
|
|
ptr += 2;
|
|
let allBits = aBits;
|
|
let i = 0, j = 1;
|
|
while ( j < rawEnd ) {
|
|
c = raw.charCodeAt(j);
|
|
const bBits = c < 0x80 ? charDescBits[c] : unicodeBits;
|
|
if ( bBits !== aBits ) {
|
|
slices[ptr+0] = j - i;
|
|
slices[ptr+1] = bBits;
|
|
slices[ptr+2] = j;
|
|
ptr += 3;
|
|
allBits |= bBits;
|
|
aBits = bBits;
|
|
i = j;
|
|
}
|
|
j += 1;
|
|
}
|
|
slices[ptr+0] = j - i;
|
|
ptr += 1;
|
|
// End-of-line slice
|
|
this.eolSpan.i = ptr;
|
|
slices[ptr+0] = 0;
|
|
slices[ptr+1] = rawEnd;
|
|
slices[ptr+2] = 0;
|
|
ptr += 3;
|
|
// Trim left
|
|
if ( (slices[0] & BITSpace) !== 0 ) {
|
|
this.leftSpaceSpan.len = 3;
|
|
} else {
|
|
this.leftSpaceSpan.len = 0;
|
|
}
|
|
// Trim right
|
|
const lastSlice = this.eolSpan.i - 3;
|
|
if (
|
|
(lastSlice > this.leftSpaceSpan.i) &&
|
|
(slices[lastSlice] & BITSpace) !== 0
|
|
) {
|
|
this.rightSpaceSpan.i = lastSlice;
|
|
this.rightSpaceSpan.len = 3;
|
|
} else {
|
|
this.rightSpaceSpan.i = this.eolSpan.i;
|
|
this.rightSpaceSpan.len = 0;
|
|
}
|
|
// Quit cleanly
|
|
this.sliceWritePtr = ptr;
|
|
this.allBits = allBits;
|
|
}
|
|
|
|
splitSlot(slot, len) {
|
|
this.sliceWritePtr += 3;
|
|
if ( this.sliceWritePtr > this.slices.length ) {
|
|
this.slices.push(0, 0, 0);
|
|
}
|
|
this.slices.copyWithin(slot + 3, slot, this.sliceWritePtr - 3);
|
|
this.slices[slot+3+1] = this.slices[slot+1] + len;
|
|
this.slices[slot+3+2] = this.slices[slot+2] - len;
|
|
this.slices[slot+2] = len;
|
|
for ( const span of this.spans ) {
|
|
if ( span.i > slot ) {
|
|
span.i += 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
markSlices(beg, end, bits) {
|
|
while ( beg < end ) {
|
|
this.slices[beg] |= bits;
|
|
beg += 3;
|
|
}
|
|
}
|
|
|
|
markSpan(span, bits) {
|
|
const { i, len } = span;
|
|
this.markSlices(i, i + len, bits);
|
|
}
|
|
|
|
unmarkSlices(beg, end, bits) {
|
|
while ( beg < end ) {
|
|
this.slices[beg] &= ~bits;
|
|
beg += 3;
|
|
}
|
|
}
|
|
|
|
findFirstMatch(from, bits) {
|
|
let to = from;
|
|
while ( to < this.sliceWritePtr ) {
|
|
if ( (this.slices[to] & bits) !== 0 ) { return to; }
|
|
to += 3;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
findFirstOdd(from, bits) {
|
|
let to = from;
|
|
while ( to < this.sliceWritePtr ) {
|
|
if ( (this.slices[to] & bits) === 0 ) { return to; }
|
|
to += 3;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
skipUntil(from, to, bits) {
|
|
let i = from;
|
|
while ( i < to ) {
|
|
if ( (this.slices[i] & bits) !== 0 ) { break; }
|
|
i += 3;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
skipUntilNot(from, to, bits) {
|
|
let i = from;
|
|
while ( i < to ) {
|
|
if ( (this.slices[i] & bits) === 0 ) { break; }
|
|
i += 3;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
strFromSlices(from, to) {
|
|
return this.raw.slice(
|
|
this.slices[from+1],
|
|
this.slices[to+1] + this.slices[to+2]
|
|
);
|
|
}
|
|
|
|
strFromSpan(span) {
|
|
if ( span.len === 0 ) { return ''; }
|
|
const beg = span.i;
|
|
return this.strFromSlices(beg, beg + span.len - 3);
|
|
}
|
|
|
|
isBlank() {
|
|
return this.allBits === BITSpace;
|
|
}
|
|
|
|
hasOptions() {
|
|
return this.optionsSpan.len !== 0;
|
|
}
|
|
|
|
getPattern() {
|
|
if ( this.pattern !== '' ) { return this.pattern; }
|
|
const { i, len } = this.patternSpan;
|
|
if ( len === 0 ) { return ''; }
|
|
let beg = this.slices[i+1];
|
|
let end = this.slices[i+len+1];
|
|
this.pattern = this.raw.slice(beg, end);
|
|
return this.pattern;
|
|
}
|
|
|
|
getNetPattern() {
|
|
if ( this.pattern !== '' ) { return this.pattern; }
|
|
const { i, len } = this.patternSpan;
|
|
if ( len === 0 ) { return ''; }
|
|
let beg = this.slices[i+1];
|
|
let end = this.slices[i+len+1];
|
|
if ( hasBits(this.flavorBits, BITFlavorNetRegex) ) {
|
|
beg += 1; end -= 1;
|
|
}
|
|
this.pattern = this.raw.slice(beg, end);
|
|
return this.pattern;
|
|
}
|
|
|
|
// https://github.com/chrisaljoudi/uBlock/issues/1096
|
|
// Examples of dubious filter content:
|
|
// - Spaces characters
|
|
// - Single character other than `*` wildcard
|
|
patternIsDubious() {
|
|
return hasBits(this.patternBits, BITSpace) || (
|
|
this.patternBits !== BITAsterisk &&
|
|
this.optionsSpan.len === 0 &&
|
|
this.patternSpan.len === 3 &&
|
|
this.slices[this.patternSpan.i+2] === 1
|
|
);
|
|
}
|
|
|
|
patternIsMatchAll() {
|
|
const { len } = this.patternSpan;
|
|
return len === 0 ||
|
|
len === 3 && hasBits(this.patternBits, BITAsterisk);
|
|
}
|
|
|
|
patternIsPlainHostname() {
|
|
if (
|
|
hasBits(this.patternBits, ~BITHostname) || (
|
|
hasBits(this.flavorBits, BITFlavorNetAnchor) &&
|
|
hasNotAllBits(this.flavorBits, BITFlavorNetHnAnchor)
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
const { i, len } = this.patternSpan;
|
|
return hasBits(this.slices[i], BITAlphaNum) &&
|
|
hasBits(this.slices[i+len-3], BITAlphaNum);
|
|
}
|
|
|
|
patternIsLeftHostnameAnchored() {
|
|
return hasBits(this.flavorBits, BITFlavorNetLeftHnAnchor);
|
|
}
|
|
|
|
patternIsRightHostnameAnchored() {
|
|
return hasBits(this.flavorBits, BITFlavorNetRightHnAnchor);
|
|
}
|
|
|
|
patternIsLeftAnchored() {
|
|
return hasBits(this.flavorBits, BITFlavorNetLeftURLAnchor);
|
|
}
|
|
|
|
patternIsRightAnchored() {
|
|
return hasBits(this.flavorBits, BITFlavorNetRightURLAnchor);
|
|
}
|
|
|
|
patternIsRegex() {
|
|
return (this.flavorBits & BITFlavorNetRegex) !== 0;
|
|
}
|
|
|
|
patternHasWildcard() {
|
|
return hasBits(this.patternBits, BITAsterisk);
|
|
}
|
|
|
|
patternHasCaret() {
|
|
return hasBits(this.patternBits, BITCaret);
|
|
}
|
|
|
|
patternHasUnicode() {
|
|
return hasBits(this.patternBits, BITUnicode);
|
|
}
|
|
|
|
patternHasUppercase() {
|
|
return hasBits(this.patternBits, BITUppercase);
|
|
}
|
|
|
|
patternToLowercase() {
|
|
const hasUpper = this.patternHasUppercase();
|
|
if ( hasUpper === false && this.pattern !== '' ) {
|
|
return this.pattern;
|
|
}
|
|
const { i, len } = this.patternSpan;
|
|
if ( len === 0 ) { return ''; }
|
|
const beg = this.slices[i+1];
|
|
const end = this.slices[i+len+1];
|
|
this.pattern = this.pattern || this.raw.slice(beg, end);
|
|
if ( hasUpper === false ) { return this.pattern; }
|
|
this.pattern = this.pattern.toLowerCase();
|
|
this.raw = this.raw.slice(0, beg) +
|
|
this.pattern +
|
|
this.raw.slice(end);
|
|
this.unmarkSlices(i, i + len, BITUppercase);
|
|
this.patternBits &= ~BITUppercase;
|
|
return this.pattern;
|
|
}
|
|
|
|
patternHasSpace() {
|
|
return hasBits(this.flavorBits, BITFlavorNetSpaceInPattern);
|
|
}
|
|
|
|
patternHasLeadingWildcard() {
|
|
if ( hasBits(this.patternBits, BITAsterisk) === false ) {
|
|
return false;
|
|
}
|
|
const { i, len } = this.patternSpan;
|
|
return len !== 0 && hasBits(this.slices[i], BITAsterisk);
|
|
}
|
|
|
|
patternHasTrailingWildcard() {
|
|
if ( hasBits(this.patternBits, BITAsterisk) === false ) {
|
|
return false;
|
|
}
|
|
const { i, len } = this.patternSpan;
|
|
return len !== 0 && hasBits(this.slices[i+len-1], BITAsterisk);
|
|
}
|
|
|
|
optionHasUnicode() {
|
|
return hasBits(this.optionsBits, BITUnicode);
|
|
}
|
|
|
|
netOptions() {
|
|
return this.netOptionsIterator;
|
|
}
|
|
|
|
extOptions() {
|
|
return this.extOptionsIterator;
|
|
}
|
|
|
|
patternTokens() {
|
|
if ( this.category === CATStaticNetFilter ) {
|
|
return this.patternTokenIterator;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
setMaxTokenLength(len) {
|
|
this.maxTokenLength = len;
|
|
}
|
|
|
|
hasUnicode() {
|
|
return hasBits(this.allBits, BITUnicode);
|
|
}
|
|
|
|
toLowerCase() {
|
|
if ( hasBits(this.allBits, BITUppercase) ) {
|
|
this.raw = this.raw.toLowerCase();
|
|
}
|
|
return this.raw;
|
|
}
|
|
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1118#issuecomment-650730158
|
|
// Be ready to deal with non-punycode-able Unicode characters.
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/772
|
|
// Encode Unicode characters beyond the hostname part.
|
|
toASCII(dryrun = false) {
|
|
if ( this.patternHasUnicode() === false ) { return true; }
|
|
const { i, len } = this.patternSpan;
|
|
if ( len === 0 ) { return true; }
|
|
const patternIsRegex = this.patternIsRegex();
|
|
let pattern = this.getNetPattern();
|
|
// Punycode hostname part of the pattern.
|
|
if ( patternIsRegex === false ) {
|
|
const match = this.reHostname.exec(pattern);
|
|
if ( match === null ) { return true; }
|
|
try {
|
|
this.punycoder.hostname = match[0].replace(/\*/g, '__asterisk__');
|
|
} catch(ex) {
|
|
return false;
|
|
}
|
|
const hn = this.punycoder.hostname;
|
|
if ( hn === '' ) { return false; }
|
|
const punycoded = hn.replace(/__asterisk__/g, '*');
|
|
pattern = punycoded + pattern.slice(match.index + match[0].length);
|
|
}
|
|
// Percent-encode remaining Unicode characters.
|
|
if ( this.reUnicodeChar.test(pattern) ) {
|
|
try {
|
|
pattern = pattern.replace(
|
|
this.reUnicodeChars,
|
|
s => encodeURIComponent(s)
|
|
);
|
|
} catch (ex) {
|
|
return false;
|
|
}
|
|
}
|
|
if ( dryrun ) { return true; }
|
|
if ( patternIsRegex ) {
|
|
pattern = `/${pattern}/`;
|
|
}
|
|
const beg = this.slices[i+1];
|
|
const end = this.slices[i+len+1];
|
|
const raw = this.raw.slice(0, beg) + pattern + this.raw.slice(end);
|
|
this.analyze(raw);
|
|
return true;
|
|
}
|
|
|
|
hasFlavor(bits) {
|
|
return hasBits(this.flavorBits, bits);
|
|
}
|
|
|
|
isException() {
|
|
return hasBits(this.flavorBits, BITFlavorException);
|
|
}
|
|
|
|
shouldIgnore() {
|
|
return hasBits(this.flavorBits, BITFlavorIgnore);
|
|
}
|
|
|
|
hasError() {
|
|
return hasBits(this.flavorBits, BITFlavorError);
|
|
}
|
|
|
|
shouldDiscard() {
|
|
return hasBits(
|
|
this.flavorBits,
|
|
BITFlavorError | BITFlavorUnsupported | BITFlavorIgnore
|
|
);
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// https://github.com/chrisaljoudi/uBlock/issues/1004
|
|
// Detect and report invalid CSS selectors.
|
|
|
|
// Discard new ABP's `-abp-properties` directive until it is
|
|
// implemented (if ever). Unlikely, see:
|
|
// https://github.com/gorhill/uBlock/issues/1752
|
|
|
|
// https://github.com/gorhill/uBlock/issues/2624
|
|
// Convert Adguard's `-ext-has='...'` into uBO's `:has(...)`.
|
|
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/89
|
|
// Do not discard unknown pseudo-elements.
|
|
|
|
Parser.prototype.SelectorCompiler = class {
|
|
constructor(parser) {
|
|
this.parser = parser;
|
|
this.reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/;
|
|
this.reExtendedSyntaxParser = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/;
|
|
this.reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/;
|
|
this.normalizedExtendedSyntaxOperators = new Map([
|
|
[ 'contains', ':has-text' ],
|
|
[ 'has', ':has' ],
|
|
[ 'matches-css', ':matches-css' ],
|
|
[ 'matches-css-after', ':matches-css-after' ],
|
|
[ 'matches-css-before', ':matches-css-before' ],
|
|
]);
|
|
this.reSimpleSelector = /^[#.][A-Za-z_][\w-]*$/;
|
|
this.div = document.createElement('div');
|
|
this.rePseudoClass = /:(?::?after|:?before|:[a-z][a-z-]*[a-z])$/;
|
|
this.reProceduralOperator = new RegExp([
|
|
'^(?:',
|
|
Array.from(parser.proceduralOperatorTokens.keys()).join('|'),
|
|
')\\('
|
|
].join(''));
|
|
this.reEatBackslashes = /\\([()])/g;
|
|
this.reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
|
|
this.reNeedScope = /^\s*>/;
|
|
this.reIsDanglingSelector = /[+>~\s]\s*$/;
|
|
this.reIsSiblingSelector = /^\s*[+~]/;
|
|
this.regexToRawValue = new Map();
|
|
// https://github.com/gorhill/uBlock/issues/2793
|
|
this.normalizedOperators = new Map([
|
|
[ ':-abp-contains', ':has-text' ],
|
|
[ ':-abp-has', ':has' ],
|
|
[ ':contains', ':has-text' ],
|
|
[ ':nth-ancestor', ':upward' ],
|
|
[ ':watch-attrs', ':watch-attr' ],
|
|
]);
|
|
this.actionOperators = new Set([
|
|
':remove',
|
|
':style',
|
|
]);
|
|
}
|
|
|
|
compile(raw, out) {
|
|
// https://github.com/gorhill/uBlock/issues/952
|
|
// Find out whether we are dealing with an Adguard-specific cosmetic
|
|
// filter, and if so, translate it if supported, or discard it if not
|
|
// supported.
|
|
// We have an Adguard/ABP cosmetic filter if and only if the
|
|
// character is `$`, `%` or `?`, otherwise it's not a cosmetic
|
|
// filter.
|
|
// Adguard's style injection: translate to uBO's format.
|
|
if ( hasBits(this.parser.flavorBits, BITFlavorExtStyle) ) {
|
|
raw = this.translateAdguardCSSInjectionFilter(raw);
|
|
if ( raw === '' ) { return false; }
|
|
out.raw = raw;
|
|
}
|
|
|
|
let extendedSyntax = false;
|
|
const selectorType = this.cssSelectorType(raw);
|
|
if ( selectorType !== 0 ) {
|
|
extendedSyntax = this.reExtendedSyntax.test(raw);
|
|
if ( extendedSyntax === false ) {
|
|
out.pseudoclass = selectorType === 3;
|
|
out.compiled = raw;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// We rarely reach this point -- majority of selectors are plain
|
|
// CSS selectors.
|
|
|
|
// Supported Adguard/ABP advanced selector syntax: will translate
|
|
// into uBO's syntax before further processing.
|
|
// Mind unsupported advanced selector syntax, such as ABP's
|
|
// `-abp-properties`.
|
|
// Note: extended selector syntax has been deprecated in ABP, in
|
|
// favor of the procedural one (i.e. `:operator(...)`).
|
|
// See https://issues.adblockplus.org/ticket/5287
|
|
if ( extendedSyntax ) {
|
|
let matches;
|
|
while ( (matches = this.reExtendedSyntaxParser.exec(raw)) !== null ) {
|
|
const operator = this.normalizedExtendedSyntaxOperators.get(matches[1]);
|
|
if ( operator === undefined ) { return false; }
|
|
raw = raw.slice(0, matches.index) +
|
|
operator + '(' + matches[3] + ')' +
|
|
raw.slice(matches.index + matches[0].length);
|
|
}
|
|
return this.compile(raw, out);
|
|
}
|
|
|
|
// Procedural selector?
|
|
const compiled = this.compileProceduralSelector(raw);
|
|
if ( compiled === undefined ) { return false; }
|
|
|
|
if ( compiled.pseudo !== undefined ) {
|
|
out.pseudoclass = compiled.pseudo;
|
|
}
|
|
|
|
out.compiled = JSON.stringify(compiled);
|
|
return true;
|
|
}
|
|
|
|
translateAdguardCSSInjectionFilter(suffix) {
|
|
const matches = /^([^{]+)\{([^}]+)\}\s*$/.exec(suffix);
|
|
if ( matches === null ) { return ''; }
|
|
const selector = matches[1].trim();
|
|
const style = matches[2].trim();
|
|
// Special style directive `remove: true` is converted into a
|
|
// `:remove()` operator.
|
|
if ( /^\s*remove:\s*true[; ]*$/.test(style) ) {
|
|
return `${selector}:remove()`;
|
|
}
|
|
// For some reasons, many of Adguard's plain cosmetic filters are
|
|
// "disguised" as style-based cosmetic filters: convert such filters
|
|
// to plain cosmetic filters.
|
|
return /display\s*:\s*none\s*!important;?$/.test(style)
|
|
? selector
|
|
: `${selector}:style(${style})`;
|
|
}
|
|
|
|
// Return value:
|
|
// 0b00 (0) = not a valid CSS selector
|
|
// 0b01 (1) = valid CSS selector, without pseudo-element
|
|
// 0b11 (3) = valid CSS selector, with pseudo element
|
|
//
|
|
// Quick regex-based validation -- most cosmetic filters are of the
|
|
// simple form and in such case a regex is much faster.
|
|
// Keep in mind:
|
|
// https://github.com/gorhill/uBlock/issues/693
|
|
// https://github.com/gorhill/uBlock/issues/1955
|
|
// https://github.com/gorhill/uBlock/issues/3111
|
|
// Workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1406817
|
|
// is fixed.
|
|
cssSelectorType(s) {
|
|
if ( this.reSimpleSelector.test(s) ) { return 1; }
|
|
const pos = this.cssPseudoSelector(s);
|
|
if ( pos !== -1 ) {
|
|
return this.cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
|
|
}
|
|
try {
|
|
this.div.matches(`${s}, ${s}:not(#foo)`);
|
|
} catch (ex) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
cssPseudoSelector(s) {
|
|
if ( s.lastIndexOf(':') === -1 ) { return -1; }
|
|
const match = this.rePseudoClass.exec(s);
|
|
return match !== null ? match.index : -1;
|
|
}
|
|
|
|
compileProceduralSelector(raw) {
|
|
const compiled = this.compileProcedural(raw, true);
|
|
if ( compiled !== undefined ) {
|
|
compiled.raw = this.decompileProcedural(compiled);
|
|
}
|
|
return compiled;
|
|
}
|
|
|
|
isBadRegex(s) {
|
|
try {
|
|
void new RegExp(s);
|
|
} catch (ex) {
|
|
this.isBadRegex.message = ex.toString();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// When dealing with literal text, we must first eat _some_
|
|
// backslash characters.
|
|
compileText(s) {
|
|
const match = this.reParseRegexLiteral.exec(s);
|
|
let regexDetails;
|
|
if ( match !== null ) {
|
|
regexDetails = match[1];
|
|
if ( this.isBadRegex(regexDetails) ) { return; }
|
|
if ( match[2] ) {
|
|
regexDetails = [ regexDetails, match[2] ];
|
|
}
|
|
} else {
|
|
regexDetails = s.replace(this.reEatBackslashes, '$1')
|
|
.replace(this.reEscapeRegex, '\\$&');
|
|
this.regexToRawValue.set(regexDetails, s);
|
|
}
|
|
return regexDetails;
|
|
}
|
|
|
|
compileCSSDeclaration(s) {
|
|
const pos = s.indexOf(':');
|
|
if ( pos === -1 ) { return; }
|
|
const name = s.slice(0, pos).trim();
|
|
const value = s.slice(pos + 1).trim();
|
|
const match = this.reParseRegexLiteral.exec(value);
|
|
let regexDetails;
|
|
if ( match !== null ) {
|
|
regexDetails = match[1];
|
|
if ( this.isBadRegex(regexDetails) ) { return; }
|
|
if ( match[2] ) {
|
|
regexDetails = [ regexDetails, match[2] ];
|
|
}
|
|
} else {
|
|
regexDetails = '^' + value.replace(this.reEscapeRegex, '\\$&') + '$';
|
|
this.regexToRawValue.set(regexDetails, value);
|
|
}
|
|
return { name: name, value: regexDetails };
|
|
}
|
|
|
|
compileConditionalSelector(s) {
|
|
// https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277
|
|
// Prepend `:scope ` if needed.
|
|
if ( this.reNeedScope.test(s) ) {
|
|
s = `:scope ${s}`;
|
|
}
|
|
return this.compileProcedural(s);
|
|
}
|
|
|
|
compileInteger(s, min = 0, max = 0x7FFFFFFF) {
|
|
if ( /^\d+$/.test(s) === false ) { return; }
|
|
const n = parseInt(s, 10);
|
|
if ( n < min || n >= max ) { return; }
|
|
return n;
|
|
}
|
|
|
|
compileNotSelector(s) {
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
|
|
// Reject instances of :not() filters for which the argument is
|
|
// a valid CSS selector, otherwise we would be adversely
|
|
// changing the behavior of CSS4's :not().
|
|
if ( this.cssSelectorType(s) === 0 ) {
|
|
return this.compileConditionalSelector(s);
|
|
}
|
|
}
|
|
|
|
compileUpwardArgument(s) {
|
|
const i = this.compileInteger(s, 1, 256);
|
|
if ( i !== undefined ) { return i; }
|
|
if ( this.cssSelectorType(s) === 1 ) { return s; }
|
|
}
|
|
|
|
compileRemoveSelector(s) {
|
|
if ( s === '' ) { return s; }
|
|
}
|
|
|
|
compileSpathExpression(s) {
|
|
if ( this.cssSelectorType('*' + s) === 1 ) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/668
|
|
compileStyleProperties(s) {
|
|
if ( /url\(|\\/i.test(s) ) { return; }
|
|
this.div.style.cssText = s;
|
|
if ( this.div.style.cssText === '' ) { return; }
|
|
this.div.style.cssText = '';
|
|
return s;
|
|
}
|
|
|
|
compileAttrList(s) {
|
|
const attrs = s.split('\s*,\s*');
|
|
const out = [];
|
|
for ( const attr of attrs ) {
|
|
if ( attr !== '' ) {
|
|
out.push(attr);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
compileXpathExpression(s) {
|
|
try {
|
|
document.createExpression(s, null);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// https://github.com/gorhill/uBlock/issues/2793#issuecomment-333269387
|
|
// Normalize (somewhat) the stringified version of procedural
|
|
// cosmetic filters -- this increase the likelihood of detecting
|
|
// duplicates given that uBO is able to understand syntax specific
|
|
// to other blockers.
|
|
// The normalized string version is what is reported in the logger,
|
|
// by design.
|
|
decompileProcedural(compiled) {
|
|
const tasks = compiled.tasks;
|
|
if ( Array.isArray(tasks) === false ) {
|
|
return compiled.selector;
|
|
}
|
|
const raw = [ compiled.selector ];
|
|
let value;
|
|
for ( const task of tasks ) {
|
|
switch ( task[0] ) {
|
|
case ':has':
|
|
case ':if':
|
|
raw.push(`:has(${this.decompileProcedural(task[1])})`);
|
|
break;
|
|
case ':has-text':
|
|
if ( Array.isArray(task[1]) ) {
|
|
value = `/${task[1][0]}/${task[1][1]}`;
|
|
} else {
|
|
value = this.regexToRawValue.get(task[1]);
|
|
if ( value === undefined ) {
|
|
value = `/${task[1]}/`;
|
|
}
|
|
}
|
|
raw.push(`:has-text(${value})`);
|
|
break;
|
|
case ':matches-css':
|
|
case ':matches-css-after':
|
|
case ':matches-css-before':
|
|
if ( Array.isArray(task[1].value) ) {
|
|
value = `/${task[1].value[0]}/${task[1].value[1]}`;
|
|
} else {
|
|
value = this.regexToRawValue.get(task[1].value);
|
|
if ( value === undefined ) {
|
|
value = `/${task[1].value}/`;
|
|
}
|
|
}
|
|
raw.push(`${task[0]}(${task[1].name}: ${value})`);
|
|
break;
|
|
case ':not':
|
|
case ':if-not':
|
|
raw.push(`:not(${this.decompileProcedural(task[1])})`);
|
|
break;
|
|
case ':spath':
|
|
raw.push(task[1]);
|
|
break;
|
|
case ':min-text-length':
|
|
case ':remove':
|
|
case ':style':
|
|
case ':upward':
|
|
case ':watch-attr':
|
|
case ':xpath':
|
|
raw.push(`${task[0]}(${task[1]})`);
|
|
break;
|
|
}
|
|
}
|
|
return raw.join('');
|
|
}
|
|
|
|
compileProcedural(raw, root = false) {
|
|
if ( raw === '' ) { return; }
|
|
|
|
const tasks = [];
|
|
const n = raw.length;
|
|
let prefix = '';
|
|
let i = 0;
|
|
let opPrefixBeg = 0;
|
|
let action;
|
|
|
|
// TODO: use slices instead of charCodeAt()
|
|
for (;;) {
|
|
let c, match;
|
|
// Advance to next operator.
|
|
while ( i < n ) {
|
|
c = raw.charCodeAt(i++);
|
|
if ( c === 0x3A /* ':' */ ) {
|
|
match = this.reProceduralOperator.exec(raw.slice(i));
|
|
if ( match !== null ) { break; }
|
|
}
|
|
}
|
|
if ( i === n ) { break; }
|
|
const opNameBeg = i - 1;
|
|
const opNameEnd = i + match[0].length - 1;
|
|
i += match[0].length;
|
|
// Find end of argument: first balanced closing parenthesis.
|
|
// Note: unbalanced parenthesis can be used in a regex literal
|
|
// when they are escaped using `\`.
|
|
// TODO: need to handle quoted parentheses.
|
|
let pcnt = 1;
|
|
while ( i < n ) {
|
|
c = raw.charCodeAt(i++);
|
|
if ( c === 0x5C /* '\\' */ ) {
|
|
if ( i < n ) { i += 1; }
|
|
} else if ( c === 0x28 /* '(' */ ) {
|
|
pcnt +=1 ;
|
|
} else if ( c === 0x29 /* ')' */ ) {
|
|
pcnt -= 1;
|
|
if ( pcnt === 0 ) { break; }
|
|
}
|
|
}
|
|
// Unbalanced parenthesis? An unbalanced parenthesis is fine
|
|
// as long as the last character is a closing parenthesis.
|
|
if ( pcnt !== 0 && c !== 0x29 ) { return; }
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
|
|
// Maybe that one operator is a valid CSS selector and if so,
|
|
// then consider it to be part of the prefix.
|
|
if ( this.cssSelectorType(raw.slice(opNameBeg, i)) === 1 ) {
|
|
continue;
|
|
}
|
|
// Extract and remember operator details.
|
|
let operator = raw.slice(opNameBeg, opNameEnd);
|
|
operator = this.normalizedOperators.get(operator) || operator;
|
|
// Action operator can only be used as trailing operator in the
|
|
// root task list.
|
|
// Per-operator arguments validation
|
|
const args = this.compileArgument(
|
|
operator,
|
|
raw.slice(opNameEnd + 1, i - 1)
|
|
);
|
|
if ( args === undefined ) { return; }
|
|
if ( opPrefixBeg === 0 ) {
|
|
prefix = raw.slice(0, opNameBeg);
|
|
} else if ( opNameBeg !== opPrefixBeg ) {
|
|
if ( action !== undefined ) { return; }
|
|
const spath = this.compileSpathExpression(
|
|
raw.slice(opPrefixBeg, opNameBeg)
|
|
);
|
|
if ( spath === undefined ) { return; }
|
|
tasks.push([ ':spath', spath ]);
|
|
}
|
|
if ( action !== undefined ) { return; }
|
|
tasks.push([ operator, args ]);
|
|
if ( this.actionOperators.has(operator) ) {
|
|
if ( root === false ) { return; }
|
|
action = operator.slice(1);
|
|
}
|
|
opPrefixBeg = i;
|
|
if ( i === n ) { break; }
|
|
}
|
|
|
|
// No task found: then we have a CSS selector.
|
|
// At least one task found: nothing should be left to parse.
|
|
if ( tasks.length === 0 ) {
|
|
prefix = raw;
|
|
} else if ( opPrefixBeg < n ) {
|
|
if ( action !== undefined ) { return; }
|
|
const spath = this.compileSpathExpression(raw.slice(opPrefixBeg));
|
|
if ( spath === undefined ) { return; }
|
|
tasks.push([ ':spath', spath ]);
|
|
}
|
|
|
|
// https://github.com/NanoAdblocker/NanoCore/issues/1#issuecomment-354394894
|
|
// https://www.reddit.com/r/uBlockOrigin/comments/c6iem5/
|
|
// Convert sibling-selector prefix into :spath operator, but
|
|
// only if context is not the root.
|
|
if ( prefix !== '' ) {
|
|
if ( this.reIsDanglingSelector.test(prefix) && tasks.length !== 0 ) {
|
|
prefix += ' *';
|
|
}
|
|
if ( this.cssSelectorType(prefix) === 0 ) {
|
|
if (
|
|
root ||
|
|
this.reIsSiblingSelector.test(prefix) === false ||
|
|
this.compileSpathExpression(prefix) === undefined
|
|
) {
|
|
return;
|
|
}
|
|
tasks.unshift([ ':spath', prefix ]);
|
|
prefix = '';
|
|
}
|
|
}
|
|
|
|
const out = { selector: prefix };
|
|
|
|
if ( tasks.length !== 0 ) {
|
|
out.tasks = tasks;
|
|
}
|
|
|
|
// Expose action to take in root descriptor.
|
|
//
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/961
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/382
|
|
// For the time being, `style` action can't be used in a
|
|
// procedural selector.
|
|
if ( action !== undefined ) {
|
|
if ( tasks.length > 1 && action === 'style' ) { return; }
|
|
out.action = action;
|
|
}
|
|
|
|
// Pseudo-selectors are valid only when used in a root task list.
|
|
if ( prefix !== '' ) {
|
|
const pos = this.cssPseudoSelector(prefix);
|
|
if ( pos !== -1 ) {
|
|
if ( root === false ) { return; }
|
|
out.pseudo = pos;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
compileArgument(operator, args) {
|
|
switch ( operator ) {
|
|
case ':has':
|
|
return this.compileConditionalSelector(args);
|
|
case ':has-text':
|
|
return this.compileText(args);
|
|
case ':if':
|
|
return this.compileConditionalSelector(args);
|
|
case ':if-not':
|
|
return this.compileConditionalSelector(args);
|
|
case ':matches-css':
|
|
return this.compileCSSDeclaration(args);
|
|
case ':matches-css-after':
|
|
return this.compileCSSDeclaration(args);
|
|
case ':matches-css-before':
|
|
return this.compileCSSDeclaration(args);
|
|
case ':min-text-length':
|
|
return this.compileInteger(args);
|
|
case ':not':
|
|
return this.compileNotSelector(args);
|
|
case ':remove':
|
|
return this.compileRemoveSelector(args);
|
|
case ':spath':
|
|
return this.compileSpathExpression(args);
|
|
case ':style':
|
|
return this.compileStyleProperties(args);
|
|
case ':upward':
|
|
return this.compileUpwardArgument(args);
|
|
case ':watch-attr':
|
|
return this.compileAttrList(args);
|
|
case ':xpath':
|
|
return this.compileXpathExpression(args);
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
Parser.prototype.proceduralOperatorTokens = new Map([
|
|
[ '-abp-contains', 0b00 ],
|
|
[ '-abp-has', 0b00, ],
|
|
[ 'contains', 0b00, ],
|
|
[ 'has', 0b01 ],
|
|
[ 'has-text', 0b01 ],
|
|
[ 'if', 0b00 ],
|
|
[ 'if-not', 0b00 ],
|
|
[ 'matches-css', 0b11 ],
|
|
[ 'matches-css-after', 0b11 ],
|
|
[ 'matches-css-before', 0b11 ],
|
|
[ 'min-text-length', 0b01 ],
|
|
[ 'not', 0b01 ],
|
|
[ 'nth-ancestor', 0b00 ],
|
|
[ 'remove', 0b11 ],
|
|
[ 'style', 0b11 ],
|
|
[ 'upward', 0b01 ],
|
|
[ 'watch-attr', 0b11 ],
|
|
[ 'watch-attrs', 0b00 ],
|
|
[ 'xpath', 0b01 ],
|
|
]);
|
|
|
|
/******************************************************************************/
|
|
|
|
const hasNoBits = (v, bits) => (v & bits) === 0;
|
|
const hasBits = (v, bits) => (v & bits) !== 0;
|
|
const hasNotAllBits = (v, bits) => (v & bits) !== bits;
|
|
//const hasAllBits = (v, bits) => (v & bits) === bits;
|
|
|
|
/******************************************************************************/
|
|
|
|
const CATNone = 0;
|
|
const CATStaticExtFilter = 1;
|
|
const CATStaticNetFilter = 2;
|
|
const CATComment = 3;
|
|
|
|
const BITSpace = 1 << 0;
|
|
const BITGlyph = 1 << 1;
|
|
const BITExclamation = 1 << 2;
|
|
const BITHash = 1 << 3;
|
|
const BITDollar = 1 << 4;
|
|
const BITPercent = 1 << 5;
|
|
const BITParen = 1 << 6;
|
|
const BITAsterisk = 1 << 7;
|
|
const BITPlus = 1 << 8;
|
|
const BITComma = 1 << 9;
|
|
const BITDash = 1 << 10;
|
|
const BITPeriod = 1 << 11;
|
|
const BITSlash = 1 << 12;
|
|
const BITNum = 1 << 13;
|
|
const BITEqual = 1 << 14;
|
|
const BITQuestion = 1 << 15;
|
|
const BITAt = 1 << 16;
|
|
const BITAlpha = 1 << 17;
|
|
const BITUppercase = 1 << 18;
|
|
const BITSquareBracket = 1 << 19;
|
|
const BITBackslash = 1 << 20;
|
|
const BITCaret = 1 << 21;
|
|
const BITUnderscore = 1 << 22;
|
|
const BITBrace = 1 << 23;
|
|
const BITPipe = 1 << 24;
|
|
const BITTilde = 1 << 25;
|
|
const BITOpening = 1 << 27;
|
|
const BITClosing = 1 << 28;
|
|
const BITUnicode = 1 << 29;
|
|
// TODO: separate from character bits into a new slice slot.
|
|
const BITIgnore = 1 << 30;
|
|
const BITError = 1 << 31;
|
|
|
|
const BITAll = 0xFFFFFFFF;
|
|
const BITAlphaNum = BITNum | BITAlpha;
|
|
const BITRegexWord = BITAlphaNum | BITUnderscore;
|
|
const BITHostname = BITNum | BITAlpha | BITUppercase | BITDash | BITPeriod | BITUnderscore | BITUnicode;
|
|
const BITPatternToken = BITNum | BITAlpha | BITPercent;
|
|
const BITLineComment = BITExclamation | BITHash | BITSquareBracket;
|
|
|
|
// Important: it is expected that lines passed to the parser have been
|
|
// trimmed of new line characters. Given this, any newline characters found
|
|
// will be interpreted as normal white spaces.
|
|
|
|
const charDescBits = [
|
|
/* 0x00 - 0x08 */ 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
/* 0x09 */ BITSpace, // \t
|
|
/* 0x0A */ BITSpace, // \n
|
|
/* 0x0B - 0x0C */ 0, 0,
|
|
/* 0x0D */ BITSpace, // \r
|
|
/* 0x0E - 0x0F */ 0, 0,
|
|
/* 0x10 - 0x1F */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
/* 0x20 */ BITSpace,
|
|
/* 0x21 ! */ BITExclamation,
|
|
/* 0x22 " */ BITGlyph,
|
|
/* 0x23 # */ BITHash,
|
|
/* 0x24 $ */ BITDollar,
|
|
/* 0x25 % */ BITPercent,
|
|
/* 0x26 & */ BITGlyph,
|
|
/* 0x27 ' */ BITGlyph,
|
|
/* 0x28 ( */ BITParen | BITOpening,
|
|
/* 0x29 ) */ BITParen | BITClosing,
|
|
/* 0x2A * */ BITAsterisk,
|
|
/* 0x2B + */ BITPlus,
|
|
/* 0x2C , */ BITComma,
|
|
/* 0x2D - */ BITDash,
|
|
/* 0x2E . */ BITPeriod,
|
|
/* 0x2F / */ BITSlash,
|
|
/* 0x30 0 */ BITNum,
|
|
/* 0x31 1 */ BITNum,
|
|
/* 0x32 2 */ BITNum,
|
|
/* 0x33 3 */ BITNum,
|
|
/* 0x34 4 */ BITNum,
|
|
/* 0x35 5 */ BITNum,
|
|
/* 0x36 6 */ BITNum,
|
|
/* 0x37 7 */ BITNum,
|
|
/* 0x38 8 */ BITNum,
|
|
/* 0x39 9 */ BITNum,
|
|
/* 0x3A : */ BITGlyph,
|
|
/* 0x3B ; */ BITGlyph,
|
|
/* 0x3C < */ BITGlyph,
|
|
/* 0x3D = */ BITEqual,
|
|
/* 0x3E > */ BITGlyph,
|
|
/* 0x3F ? */ BITQuestion,
|
|
/* 0x40 @ */ BITAt,
|
|
/* 0x41 A */ BITAlpha | BITUppercase,
|
|
/* 0x42 B */ BITAlpha | BITUppercase,
|
|
/* 0x43 C */ BITAlpha | BITUppercase,
|
|
/* 0x44 D */ BITAlpha | BITUppercase,
|
|
/* 0x45 E */ BITAlpha | BITUppercase,
|
|
/* 0x46 F */ BITAlpha | BITUppercase,
|
|
/* 0x47 G */ BITAlpha | BITUppercase,
|
|
/* 0x48 H */ BITAlpha | BITUppercase,
|
|
/* 0x49 I */ BITAlpha | BITUppercase,
|
|
/* 0x4A J */ BITAlpha | BITUppercase,
|
|
/* 0x4B K */ BITAlpha | BITUppercase,
|
|
/* 0x4C L */ BITAlpha | BITUppercase,
|
|
/* 0x4D M */ BITAlpha | BITUppercase,
|
|
/* 0x4E N */ BITAlpha | BITUppercase,
|
|
/* 0x4F O */ BITAlpha | BITUppercase,
|
|
/* 0x50 P */ BITAlpha | BITUppercase,
|
|
/* 0x51 Q */ BITAlpha | BITUppercase,
|
|
/* 0x52 R */ BITAlpha | BITUppercase,
|
|
/* 0x53 S */ BITAlpha | BITUppercase,
|
|
/* 0x54 T */ BITAlpha | BITUppercase,
|
|
/* 0x55 U */ BITAlpha | BITUppercase,
|
|
/* 0x56 V */ BITAlpha | BITUppercase,
|
|
/* 0x57 W */ BITAlpha | BITUppercase,
|
|
/* 0x58 X */ BITAlpha | BITUppercase,
|
|
/* 0x59 Y */ BITAlpha | BITUppercase,
|
|
/* 0x5A Z */ BITAlpha | BITUppercase,
|
|
/* 0x5B [ */ BITSquareBracket | BITOpening,
|
|
/* 0x5C \ */ BITBackslash,
|
|
/* 0x5D ] */ BITSquareBracket | BITClosing,
|
|
/* 0x5E ^ */ BITCaret,
|
|
/* 0x5F _ */ BITUnderscore,
|
|
/* 0x60 ` */ BITGlyph,
|
|
/* 0x61 a */ BITAlpha,
|
|
/* 0x62 b */ BITAlpha,
|
|
/* 0x63 c */ BITAlpha,
|
|
/* 0x64 d */ BITAlpha,
|
|
/* 0x65 e */ BITAlpha,
|
|
/* 0x66 f */ BITAlpha,
|
|
/* 0x67 g */ BITAlpha,
|
|
/* 0x68 h */ BITAlpha,
|
|
/* 0x69 i */ BITAlpha,
|
|
/* 0x6A j */ BITAlpha,
|
|
/* 0x6B k */ BITAlpha,
|
|
/* 0x6C l */ BITAlpha,
|
|
/* 0x6D m */ BITAlpha,
|
|
/* 0x6E n */ BITAlpha,
|
|
/* 0x6F o */ BITAlpha,
|
|
/* 0x70 p */ BITAlpha,
|
|
/* 0x71 q */ BITAlpha,
|
|
/* 0x72 r */ BITAlpha,
|
|
/* 0x73 s */ BITAlpha,
|
|
/* 0x74 t */ BITAlpha,
|
|
/* 0x75 u */ BITAlpha,
|
|
/* 0x76 v */ BITAlpha,
|
|
/* 0x77 w */ BITAlpha,
|
|
/* 0x78 x */ BITAlpha,
|
|
/* 0x79 y */ BITAlpha,
|
|
/* 0x7A z */ BITAlpha,
|
|
/* 0x7B { */ BITBrace | BITOpening,
|
|
/* 0x7C | */ BITPipe,
|
|
/* 0x7D } */ BITBrace | BITClosing,
|
|
/* 0x7E ~ */ BITTilde,
|
|
/* 0x7F */ 0,
|
|
];
|
|
|
|
const BITFlavorException = 1 << 0;
|
|
const BITFlavorNetRegex = 1 << 1;
|
|
const BITFlavorNetLeftURLAnchor = 1 << 2;
|
|
const BITFlavorNetRightURLAnchor = 1 << 3;
|
|
const BITFlavorNetLeftHnAnchor = 1 << 4;
|
|
const BITFlavorNetRightHnAnchor = 1 << 5;
|
|
const BITFlavorNetSpaceInPattern = 1 << 6;
|
|
const BITFlavorExtStyle = 1 << 7;
|
|
const BITFlavorExtStrong = 1 << 8;
|
|
const BITFlavorExtCosmetic = 1 << 9;
|
|
const BITFlavorExtScriptlet = 1 << 10;
|
|
const BITFlavorExtHTML = 1 << 11;
|
|
const BITFlavorIgnore = 1 << 29;
|
|
const BITFlavorUnsupported = 1 << 30;
|
|
const BITFlavorError = 1 << 31;
|
|
|
|
const BITFlavorNetLeftAnchor = BITFlavorNetLeftURLAnchor | BITFlavorNetLeftHnAnchor;
|
|
const BITFlavorNetRightAnchor = BITFlavorNetRightURLAnchor | BITFlavorNetRightHnAnchor;
|
|
const BITFlavorNetHnAnchor = BITFlavorNetLeftHnAnchor | BITFlavorNetRightHnAnchor;
|
|
const BITFlavorNetAnchor = BITFlavorNetLeftAnchor | BITFlavorNetRightAnchor;
|
|
|
|
const OPTTokenInvalid = 0;
|
|
const OPTToken1p = 1;
|
|
const OPTToken3p = 2;
|
|
const OPTTokenAll = 3;
|
|
const OPTTokenBadfilter = 4;
|
|
const OPTTokenCname = 5;
|
|
const OPTTokenCsp = 6;
|
|
const OPTTokenCss = 7;
|
|
const OPTTokenDenyAllow = 8;
|
|
const OPTTokenDoc = 9;
|
|
const OPTTokenDomain = 10;
|
|
const OPTTokenEhide = 11;
|
|
const OPTTokenEmpty = 12;
|
|
const OPTTokenFont = 13;
|
|
const OPTTokenFrame = 14;
|
|
const OPTTokenGenericblock = 15;
|
|
const OPTTokenGhide = 16;
|
|
const OPTTokenImage = 17;
|
|
const OPTTokenImportant = 18;
|
|
const OPTTokenInlineFont = 19;
|
|
const OPTTokenInlineScript = 20;
|
|
const OPTTokenMedia = 21;
|
|
const OPTTokenMp4 = 22;
|
|
const OPTTokenObject = 23;
|
|
const OPTTokenOther = 24;
|
|
const OPTTokenPing = 25;
|
|
const OPTTokenPopunder = 26;
|
|
const OPTTokenPopup = 27;
|
|
const OPTTokenRedirect = 28;
|
|
const OPTTokenRedirectRule = 29;
|
|
const OPTTokenScript = 30;
|
|
const OPTTokenShide = 31;
|
|
const OPTTokenXhr = 32;
|
|
const OPTTokenWebrtc = 33;
|
|
const OPTTokenWebsocket = 34;
|
|
|
|
const OPTCanNegate = 1 << 8;
|
|
const OPTBlockOnly = 1 << 9;
|
|
const OPTAllowOnly = 1 << 10;
|
|
const OPTMustAssign = 1 << 11;
|
|
const OPTAllowMayAssign = 1 << 12;
|
|
const OPTDomainList = 1 << 13;
|
|
const OPTType = 1 << 14;
|
|
const OPTNetworkType = 1 << 15;
|
|
const OPTRedirectType = 1 << 16;
|
|
const OPTRedirectableType = 1 << 17;
|
|
const OPTNotSupported = 1 << 18;
|
|
|
|
/******************************************************************************/
|
|
|
|
Parser.prototype.CATNone = CATNone;
|
|
Parser.prototype.CATStaticExtFilter = CATStaticExtFilter;
|
|
Parser.prototype.CATStaticNetFilter = CATStaticNetFilter;
|
|
Parser.prototype.CATComment = CATComment;
|
|
|
|
Parser.prototype.BITSpace = BITSpace;
|
|
Parser.prototype.BITGlyph = BITGlyph;
|
|
Parser.prototype.BITComma = BITComma;
|
|
Parser.prototype.BITLineComment = BITLineComment;
|
|
Parser.prototype.BITPipe = BITPipe;
|
|
Parser.prototype.BITAsterisk = BITAsterisk;
|
|
Parser.prototype.BITCaret = BITCaret;
|
|
Parser.prototype.BITUppercase = BITUppercase;
|
|
Parser.prototype.BITHostname = BITHostname;
|
|
Parser.prototype.BITPeriod = BITPeriod;
|
|
Parser.prototype.BITDash = BITDash;
|
|
Parser.prototype.BITHash = BITHash;
|
|
Parser.prototype.BITEqual = BITEqual;
|
|
Parser.prototype.BITQuestion = BITQuestion;
|
|
Parser.prototype.BITPercent = BITPercent;
|
|
Parser.prototype.BITTilde = BITTilde;
|
|
Parser.prototype.BITUnicode = BITUnicode;
|
|
Parser.prototype.BITIgnore = BITIgnore;
|
|
Parser.prototype.BITError = BITError;
|
|
Parser.prototype.BITAll = BITAll;
|
|
|
|
Parser.prototype.BITFlavorException = BITFlavorException;
|
|
Parser.prototype.BITFlavorExtStyle = BITFlavorExtStyle;
|
|
Parser.prototype.BITFlavorExtStrong = BITFlavorExtStrong;
|
|
Parser.prototype.BITFlavorExtCosmetic = BITFlavorExtCosmetic;
|
|
Parser.prototype.BITFlavorExtScriptlet = BITFlavorExtScriptlet;
|
|
Parser.prototype.BITFlavorExtHTML = BITFlavorExtHTML;
|
|
Parser.prototype.BITFlavorIgnore = BITFlavorIgnore;
|
|
Parser.prototype.BITFlavorUnsupported = BITFlavorUnsupported;
|
|
Parser.prototype.BITFlavorError = BITFlavorError;
|
|
|
|
Parser.prototype.OPTTokenInvalid = OPTTokenInvalid;
|
|
Parser.prototype.OPTTokenAll = OPTTokenAll;
|
|
Parser.prototype.OPTTokenBadfilter = OPTTokenBadfilter;
|
|
Parser.prototype.OPTTokenCname = OPTTokenCname;
|
|
Parser.prototype.OPTTokenCsp = OPTTokenCsp;
|
|
Parser.prototype.OPTTokenDenyAllow = OPTTokenDenyAllow;
|
|
Parser.prototype.OPTTokenDoc = OPTTokenDoc;
|
|
Parser.prototype.OPTTokenDomain = OPTTokenDomain;
|
|
Parser.prototype.OPTTokenEhide = OPTTokenEhide;
|
|
Parser.prototype.OPTTokenEmpty = OPTTokenEmpty;
|
|
Parser.prototype.OPTToken1p = OPTToken1p;
|
|
Parser.prototype.OPTTokenFont = OPTTokenFont;
|
|
Parser.prototype.OPTTokenGenericblock = OPTTokenGenericblock;
|
|
Parser.prototype.OPTTokenGhide = OPTTokenGhide;
|
|
Parser.prototype.OPTTokenImage = OPTTokenImage;
|
|
Parser.prototype.OPTTokenImportant = OPTTokenImportant;
|
|
Parser.prototype.OPTTokenInlineFont = OPTTokenInlineFont;
|
|
Parser.prototype.OPTTokenInlineScript = OPTTokenInlineScript;
|
|
Parser.prototype.OPTTokenMedia = OPTTokenMedia;
|
|
Parser.prototype.OPTTokenMp4 = OPTTokenMp4;
|
|
Parser.prototype.OPTTokenObject = OPTTokenObject;
|
|
Parser.prototype.OPTTokenOther = OPTTokenOther;
|
|
Parser.prototype.OPTTokenPing = OPTTokenPing;
|
|
Parser.prototype.OPTTokenPopunder = OPTTokenPopunder;
|
|
Parser.prototype.OPTTokenPopup = OPTTokenPopup;
|
|
Parser.prototype.OPTTokenRedirect = OPTTokenRedirect;
|
|
Parser.prototype.OPTTokenRedirectRule = OPTTokenRedirectRule;
|
|
Parser.prototype.OPTTokenScript = OPTTokenScript;
|
|
Parser.prototype.OPTTokenShide = OPTTokenShide;
|
|
Parser.prototype.OPTTokenCss = OPTTokenCss;
|
|
Parser.prototype.OPTTokenFrame = OPTTokenFrame;
|
|
Parser.prototype.OPTToken3p = OPTToken3p;
|
|
Parser.prototype.OPTTokenXhr = OPTTokenXhr;
|
|
Parser.prototype.OPTTokenWebrtc = OPTTokenWebrtc;
|
|
Parser.prototype.OPTTokenWebsocket = OPTTokenWebsocket;
|
|
|
|
Parser.prototype.OPTCanNegate = OPTCanNegate;
|
|
Parser.prototype.OPTBlockOnly = OPTBlockOnly;
|
|
Parser.prototype.OPTAllowOnly = OPTAllowOnly;
|
|
Parser.prototype.OPTMustAssign = OPTMustAssign;
|
|
Parser.prototype.OPTAllowMayAssign = OPTAllowMayAssign;
|
|
Parser.prototype.OPTDomainList = OPTDomainList;
|
|
Parser.prototype.OPTType = OPTType;
|
|
Parser.prototype.OPTNetworkType = OPTNetworkType;
|
|
Parser.prototype.OPTRedirectType = OPTRedirectType;
|
|
Parser.prototype.OPTRedirectableType = OPTRedirectableType;
|
|
Parser.prototype.OPTNotSupported = OPTNotSupported;
|
|
|
|
/******************************************************************************/
|
|
|
|
const netOptionTokens = new Map([
|
|
[ '1p', OPTToken1p | OPTCanNegate ],
|
|
[ 'first-party', OPTToken1p | OPTCanNegate ],
|
|
[ '3p', OPTToken3p | OPTCanNegate ],
|
|
[ 'third-party', OPTToken3p | OPTCanNegate ],
|
|
[ 'all', OPTTokenAll | OPTType | OPTNetworkType ],
|
|
[ 'badfilter', OPTTokenBadfilter ],
|
|
[ 'cname', OPTTokenCname | OPTAllowOnly | OPTType ],
|
|
[ 'csp', OPTTokenCsp | OPTMustAssign | OPTAllowMayAssign ],
|
|
[ 'css', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'stylesheet', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'denyallow', OPTTokenDenyAllow | OPTMustAssign | OPTDomainList ],
|
|
[ 'doc', OPTTokenDoc | OPTType | OPTNetworkType ],
|
|
[ 'document', OPTTokenDoc | OPTType | OPTNetworkType ],
|
|
[ 'domain', OPTTokenDomain | OPTMustAssign | OPTDomainList ],
|
|
[ 'ehide', OPTTokenEhide | OPTType ],
|
|
[ 'elemhide', OPTTokenEhide | OPTType ],
|
|
[ 'empty', OPTTokenEmpty | OPTBlockOnly | OPTRedirectType ],
|
|
[ 'frame', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'subdocument', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'font', OPTTokenFont | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'genericblock', OPTTokenGenericblock | OPTNotSupported ],
|
|
[ 'ghide', OPTTokenGhide | OPTType ],
|
|
[ 'generichide', OPTTokenGhide | OPTType ],
|
|
[ 'image', OPTTokenImage | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'important', OPTTokenImportant | OPTBlockOnly ],
|
|
[ 'inline-font', OPTTokenInlineFont | OPTType ],
|
|
[ 'inline-script', OPTTokenInlineScript | OPTType ],
|
|
[ 'media', OPTTokenMedia | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'mp4', OPTTokenMp4 | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType | OPTRedirectableType ],
|
|
[ 'object', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'object-subrequest', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType ],
|
|
[ 'other', OPTTokenOther | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'ping', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ],
|
|
[ 'beacon', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ],
|
|
[ 'popunder', OPTTokenPopunder | OPTType ],
|
|
[ 'popup', OPTTokenPopup | OPTType ],
|
|
[ 'redirect', OPTTokenRedirect | OPTMustAssign | OPTBlockOnly | OPTRedirectType ],
|
|
[ 'redirect-rule', OPTTokenRedirectRule | OPTMustAssign | OPTBlockOnly | OPTRedirectType ],
|
|
[ 'script', OPTTokenScript | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'shide', OPTTokenShide | OPTType ],
|
|
[ 'specifichide', OPTTokenShide | OPTType ],
|
|
[ 'xhr', OPTTokenXhr | OPTCanNegate| OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'xmlhttprequest', OPTTokenXhr | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ],
|
|
[ 'webrtc', OPTTokenWebrtc | OPTNotSupported ],
|
|
[ 'websocket', OPTTokenWebsocket | OPTCanNegate | OPTType | OPTNetworkType ],
|
|
]);
|
|
|
|
Parser.prototype.netOptionTokens = netOptionTokens;
|
|
|
|
/******************************************************************************/
|
|
|
|
const Span = class {
|
|
constructor() {
|
|
this.reset();
|
|
}
|
|
reset() {
|
|
this.i = this.len = 0;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const NetOptionsIterator = class {
|
|
constructor(parser) {
|
|
this.parser = parser;
|
|
this.exception = false;
|
|
this.interactive = false;
|
|
this.optSlices = [];
|
|
this.writePtr = 0;
|
|
this.readPtr = 0;
|
|
this.item = {
|
|
id: OPTTokenInvalid,
|
|
val: undefined,
|
|
not: false,
|
|
};
|
|
this.value = undefined;
|
|
this.done = true;
|
|
}
|
|
[Symbol.iterator]() {
|
|
return this.init();
|
|
}
|
|
init() {
|
|
this.readPtr = this.writePtr = 0;
|
|
this.done = this.parser.optionsSpan.len === 0;
|
|
if ( this.done ) {
|
|
this.value = undefined;
|
|
return this;
|
|
}
|
|
// Prime iterator
|
|
this.value = this.item;
|
|
this.exception = this.parser.isException();
|
|
this.interactive = this.parser.interactive;
|
|
// Each option is encoded as follow:
|
|
//
|
|
// desc ~token=value,
|
|
// 0 1| 3| 5
|
|
// 2 4
|
|
//
|
|
// At index 0 is the option descriptor.
|
|
// At indices 1-5 is a slice index.
|
|
const lopts = this.parser.optionsSpan.i;
|
|
const ropts = lopts + this.parser.optionsSpan.len;
|
|
const slices = this.parser.slices;
|
|
const optSlices = this.optSlices;
|
|
let typeCount = 0;
|
|
let redirectableTypeCount = 0;
|
|
let redirectIndex = -1;
|
|
let cspIndex = -1;
|
|
let writePtr = 0;
|
|
let lopt = lopts;
|
|
while ( lopt < ropts ) {
|
|
let good = true;
|
|
let ltok = lopt;
|
|
// Parse optional negation
|
|
if ( hasBits(slices[lopt], BITTilde) ) {
|
|
if ( slices[lopt+2] > 1 ) { good = false; }
|
|
ltok += 3;
|
|
}
|
|
// Find end of current option
|
|
let lval = 0;
|
|
let i = ltok;
|
|
while ( i < ropts ) {
|
|
const bits = slices[i];
|
|
if ( hasBits(bits, BITComma) ) {
|
|
if ( this.interactive && (i === lopt || slices[i+2] > 1) ) {
|
|
slices[i] |= BITError;
|
|
}
|
|
break;
|
|
}
|
|
if ( lval === 0 && hasBits(bits, BITEqual) ) { lval = i; }
|
|
i += 3;
|
|
}
|
|
// Check for proper assignement
|
|
let assigned = false;
|
|
if ( good && lval !== 0 ) {
|
|
good = assigned = slices[lval+2] === 1 && lval + 3 !== i;
|
|
}
|
|
let descriptor;
|
|
if ( good ) {
|
|
const rtok = lval === 0 ? i : lval;
|
|
const token = this.parser.raw.slice(slices[ltok+1], slices[rtok+1]);
|
|
descriptor = netOptionTokens.get(token);
|
|
}
|
|
// Validate option according to context
|
|
if (
|
|
descriptor === undefined ||
|
|
ltok !== lopt && hasNoBits(descriptor, OPTCanNegate) ||
|
|
this.exception && hasBits(descriptor, OPTBlockOnly) ||
|
|
this.exception === false && hasBits(descriptor, OPTAllowOnly) ||
|
|
assigned && hasNoBits(descriptor, OPTMustAssign) ||
|
|
assigned === false && hasBits(descriptor, OPTMustAssign) && (
|
|
this.exception === false ||
|
|
hasNoBits(descriptor, OPTAllowMayAssign)
|
|
)
|
|
) {
|
|
descriptor = OPTTokenInvalid;
|
|
}
|
|
// Keep count of types
|
|
if ( hasBits(descriptor, OPTType) ) {
|
|
typeCount += 1;
|
|
if ( hasBits(descriptor, OPTRedirectableType) ) {
|
|
redirectableTypeCount += 1;
|
|
}
|
|
}
|
|
// Only one `redirect` or `csp` can be present
|
|
if ( hasBits(descriptor, OPTRedirectType) ) {
|
|
if ( redirectIndex === -1 ) {
|
|
redirectIndex = writePtr;
|
|
} else {
|
|
descriptor = OPTTokenInvalid;
|
|
}
|
|
} else if ( (descriptor & 0xFF) === OPTTokenCsp ) {
|
|
if ( cspIndex === -1 ) {
|
|
cspIndex = writePtr;
|
|
} else {
|
|
descriptor = OPTTokenInvalid;
|
|
}
|
|
}
|
|
// Mark slices in case of invalid filter option
|
|
if (
|
|
this.interactive && (
|
|
descriptor === OPTTokenInvalid ||
|
|
hasBits(descriptor, OPTNotSupported)
|
|
)
|
|
) {
|
|
this.parser.markSlices(lopt, i, BITError);
|
|
}
|
|
// Store indices to raw slices -- this will be used during
|
|
// iteration
|
|
optSlices[writePtr+0] = descriptor;
|
|
optSlices[writePtr+1] = lopt;
|
|
optSlices[writePtr+2] = ltok;
|
|
if ( lval !== 0 ) {
|
|
optSlices[writePtr+3] = lval;
|
|
optSlices[writePtr+4] = lval+3;
|
|
if ( this.interactive && hasBits(descriptor, OPTDomainList) ) {
|
|
this.parser.analyzeDomainList(
|
|
lval + 3, i, BITPipe,
|
|
(descriptor & 0xFF) === OPTTokenDomain ? 0b01 : 0b00
|
|
);
|
|
}
|
|
} else {
|
|
optSlices[writePtr+3] = i;
|
|
optSlices[writePtr+4] = i;
|
|
}
|
|
optSlices[writePtr+5] = i;
|
|
// Advance to next option
|
|
writePtr += 6;
|
|
lopt = i + 3;
|
|
}
|
|
this.writePtr = writePtr;
|
|
// Dangling comma
|
|
if ( this.interactive && hasBits(this.parser.slices[ropts-3], BITComma) ) {
|
|
this.parser.slices[ropts-3] |= BITError;
|
|
}
|
|
// Invalid combinations of options
|
|
//
|
|
// `csp` can't be used with any other types or redirection
|
|
if ( cspIndex !== -1 && ( typeCount !== 0 || redirectIndex !== -1 ) ) {
|
|
optSlices[cspIndex] = OPTTokenInvalid;
|
|
if ( this.interactive ) {
|
|
this.parser.markSlices(
|
|
optSlices[cspIndex+1],
|
|
optSlices[cspIndex+5],
|
|
BITError
|
|
);
|
|
}
|
|
}
|
|
// `redirect` requires one single redirectable type, EXCEPT for when we
|
|
// redirect to `empty`, in which case it is allowed to not have any
|
|
// network type specified.
|
|
if (
|
|
redirectIndex !== -1 &&
|
|
redirectableTypeCount !== 1 && (
|
|
redirectableTypeCount !== 0 ||
|
|
typeCount !== 0 ||
|
|
this.parser.raw.slice(
|
|
this.parser.slices[optSlices[redirectIndex+0]+1],
|
|
this.parser.slices[optSlices[redirectIndex+5]+1]
|
|
).endsWith('empty') === false
|
|
)
|
|
) {
|
|
optSlices[redirectIndex] = OPTTokenInvalid;
|
|
if ( this.interactive ) {
|
|
this.parser.markSlices(
|
|
optSlices[redirectIndex+1],
|
|
optSlices[redirectIndex+5],
|
|
BITError
|
|
);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
next() {
|
|
const i = this.readPtr;
|
|
if ( i === this.writePtr ) {
|
|
this.value = undefined;
|
|
this.done = true;
|
|
return this;
|
|
}
|
|
const optSlices = this.optSlices;
|
|
const descriptor = optSlices[i+0];
|
|
this.item.id = descriptor & 0xFF;
|
|
this.item.not = optSlices[i+2] !== optSlices[i+1];
|
|
this.item.val = undefined;
|
|
if ( optSlices[i+4] !== optSlices[i+5] ) {
|
|
const parser = this.parser;
|
|
this.item.val = parser.raw.slice(
|
|
parser.slices[optSlices[i+4]+1],
|
|
parser.slices[optSlices[i+5]+1]
|
|
);
|
|
}
|
|
this.readPtr = i + 6;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// https://github.com/gorhill/uBlock/issues/997
|
|
// Ignore token if preceded by wildcard.
|
|
|
|
const PatternTokenIterator = class {
|
|
constructor(parser) {
|
|
this.parser = parser;
|
|
this.l = this.r = this.i = 0;
|
|
this.value = undefined;
|
|
this.done = true;
|
|
}
|
|
[Symbol.iterator]() {
|
|
const { i, len } = this.parser.patternSpan;
|
|
if ( len === 0 ) {
|
|
return this.end();
|
|
}
|
|
this.l = i;
|
|
this.r = i + len;
|
|
this.i = i;
|
|
this.done = false;
|
|
this.value = { token: '', pos: 0 };
|
|
return this;
|
|
}
|
|
end() {
|
|
this.value = undefined;
|
|
this.done = true;
|
|
return this;
|
|
}
|
|
next() {
|
|
const { slices, maxTokenLength } = this.parser;
|
|
let { l, r, i, value } = this;
|
|
let sl = i, sr = 0;
|
|
for (;;) {
|
|
for (;;) {
|
|
if ( sl >= r ) { return this.end(); }
|
|
if ( hasBits(slices[sl], BITPatternToken) ) { break; }
|
|
sl += 3;
|
|
}
|
|
sr = sl + 3;
|
|
while ( sr < r && hasBits(slices[sr], BITPatternToken) ) {
|
|
sr += 3;
|
|
}
|
|
if (
|
|
(
|
|
sl === 0 ||
|
|
hasNoBits(slices[sl-3], BITAsterisk)
|
|
) &&
|
|
(
|
|
sr === r ||
|
|
hasNoBits(slices[sr], BITAsterisk) ||
|
|
(slices[sr+1] - slices[sl+1]) >= maxTokenLength
|
|
)
|
|
) {
|
|
break;
|
|
}
|
|
sl = sr + 3;
|
|
}
|
|
this.i = sr + 3;
|
|
const beg = slices[sl+1];
|
|
value.token = this.parser.raw.slice(beg, slices[sr+1]);
|
|
value.pos = beg - slices[l+1];
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const ExtOptionsIterator = class {
|
|
constructor(parser) {
|
|
this.parser = parser;
|
|
this.l = this.r = 0;
|
|
this.value = undefined;
|
|
this.done = true;
|
|
}
|
|
[Symbol.iterator]() {
|
|
const { i, len } = this.parser.optionsSpan;
|
|
if ( len === 0 ) {
|
|
this.l = this.r = 0;
|
|
this.done = true;
|
|
this.value = undefined;
|
|
} else {
|
|
this.l = i;
|
|
this.r = i + len;
|
|
this.done = false;
|
|
this.value = { hn: undefined, not: false, bad: false };
|
|
}
|
|
return this;
|
|
}
|
|
next() {
|
|
if ( this.l === this.r ) {
|
|
this.value = undefined;
|
|
this.done = true;
|
|
return this;
|
|
}
|
|
const parser = this.parser;
|
|
const { slices, interactive } = parser;
|
|
const value = this.value;
|
|
value.not = value.bad = false;
|
|
let i0 = this.l;
|
|
let i = i0;
|
|
if ( hasBits(slices[i], BITTilde) ) {
|
|
if ( slices[i+2] !== 1 ) {
|
|
value.bad = true;
|
|
if ( interactive ) { slices[i] |= BITError; }
|
|
}
|
|
value.not = true;
|
|
i += 3;
|
|
i0 = i;
|
|
}
|
|
while ( i < this.r ) {
|
|
if ( hasBits(slices[i], BITComma) ) { break; }
|
|
i += 3;
|
|
}
|
|
if ( i === i0 ) { value.bad = true; }
|
|
value.hn = parser.raw.slice(slices[i0+1], slices[i+1]);
|
|
if ( i < this.r ) { i += 3; }
|
|
this.l = i;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
if ( typeof vAPI === 'object' && vAPI !== null ) {
|
|
vAPI.StaticFilteringParser = Parser;
|
|
} else {
|
|
self.StaticFilteringParser = Parser;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// <<<<< end of local scope
|
|
}
|