diff options
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/static-filtering-parser.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/static-filtering-parser.js | 4413 |
1 files changed, 4413 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/static-filtering-parser.js b/data/extensions/uBlock0@raymondhill.net/js/static-filtering-parser.js new file mode 100644 index 0000000..2087d36 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/static-filtering-parser.js @@ -0,0 +1,4413 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + 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 +*/ + +import * as cssTree from '../lib/csstree/css-tree.js'; +import { ArglistParser } from './arglist-parser.js'; +import { JSONPath } from './jsonpath.js'; + +/******************************************************************************* + * + * The parser creates a simple unidirectional AST from a raw line of text. + * Each node in the AST is a sequence of numbers, so as to avoid the need to + * make frequent memory allocation to represent the AST. + * + * All the AST nodes are allocated in the same integer-only array, which + * array is reused when parsing new lines. + * + * The AST can only be walked from top to bottom, then left to right. + * + * Each node typically refer to a corresponding string slice in the source + * text. + * + * It may happens a node requires to normalize the corresponding source slice, + * in which case there will be a reference in the AST to a transformed source + * string. (For example, a domain name might contain unicode characters, in + * which case the corresponding node will contain a reference to the + * (transformed) punycoded version of the domain name.) + * + * The AST can be easily used for syntax coloring purpose, in which case it's + * just a matter of walking through all the nodes in natural order. + * + * A tree walking utility class exists for compilation and syntax coloring + * purpose. + * +**/ + +/******************************************************************************/ + +let iota = 0; + +iota = 0; +export const AST_TYPE_NONE = iota++; +export const AST_TYPE_UNKNOWN = iota++; +export const AST_TYPE_COMMENT = iota++; +export const AST_TYPE_NETWORK = iota++; +export const AST_TYPE_EXTENDED = iota++; + +iota = 0; +export const AST_TYPE_NETWORK_PATTERN_ANY = iota++; +export const AST_TYPE_NETWORK_PATTERN_HOSTNAME = iota++; +export const AST_TYPE_NETWORK_PATTERN_PLAIN = iota++; +export const AST_TYPE_NETWORK_PATTERN_REGEX = iota++; +export const AST_TYPE_NETWORK_PATTERN_GENERIC = iota++; +export const AST_TYPE_NETWORK_PATTERN_BAD = iota++; +export const AST_TYPE_EXTENDED_COSMETIC = iota++; +export const AST_TYPE_EXTENDED_SCRIPTLET = iota++; +export const AST_TYPE_EXTENDED_HTML = iota++; +export const AST_TYPE_EXTENDED_RESPONSEHEADER = iota++; +export const AST_TYPE_COMMENT_PREPARSER = iota++; + +iota = 0; +export const AST_FLAG_UNSUPPORTED = 1 << iota++; +export const AST_FLAG_IGNORE = 1 << iota++; +export const AST_FLAG_HAS_ERROR = 1 << iota++; +export const AST_FLAG_IS_EXCEPTION = 1 << iota++; +export const AST_FLAG_EXT_STRONG = 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_RIGHT_PATHANCHOR = 1 << iota++; +export const AST_FLAG_NET_PATTERN_LEFT_ANCHOR = 1 << iota++; +export const AST_FLAG_NET_PATTERN_RIGHT_ANCHOR = 1 << iota++; +export const AST_FLAG_HAS_OPTIONS = 1 << iota++; + +iota = 0; +export const AST_ERROR_NONE = 1 << iota++; +export const AST_ERROR_REGEX = 1 << iota++; +export const AST_ERROR_PATTERN = 1 << iota++; +export const AST_ERROR_DOMAIN_NAME = 1 << iota++; +export const AST_ERROR_OPTION_DUPLICATE = 1 << iota++; +export const AST_ERROR_OPTION_UNKNOWN = 1 << iota++; +export const AST_ERROR_OPTION_BADVALUE = 1 << iota++; +export const AST_ERROR_OPTION_EXCLUDED = 1 << iota++; +export const AST_ERROR_IF_TOKEN_UNKNOWN = 1 << iota++; +export const AST_ERROR_UNTRUSTED_SOURCE = 1 << iota++; + +iota = 0; +const NODE_RIGHT_INDEX = iota++; +const NOOP_NODE_SIZE = iota; +const NODE_TYPE_INDEX = iota++; +const NODE_DOWN_INDEX = iota++; +const NODE_BEG_INDEX = iota++; +const NODE_END_INDEX = iota++; +const NODE_FLAGS_INDEX = iota++; +const NODE_TRANSFORM_INDEX = iota++; +const FULL_NODE_SIZE = iota; + +iota = 0; +export const NODE_TYPE_NOOP = iota++; +export const NODE_TYPE_LINE_RAW = iota++; +export const NODE_TYPE_LINE_BODY = iota++; +export const NODE_TYPE_WHITESPACE = iota++; +export const NODE_TYPE_COMMENT = iota++; +export const NODE_TYPE_IGNORE = iota++; +export const NODE_TYPE_EXT_RAW = iota++; +export const NODE_TYPE_EXT_OPTIONS_ANCHOR = iota++; +export const NODE_TYPE_EXT_OPTIONS = iota++; +export const NODE_TYPE_EXT_DECORATION = iota++; +export const NODE_TYPE_EXT_PATTERN_RAW = iota++; +export const NODE_TYPE_EXT_PATTERN_COSMETIC = iota++; +export const NODE_TYPE_EXT_PATTERN_HTML = iota++; +export const NODE_TYPE_EXT_PATTERN_RESPONSEHEADER = iota++; +export const NODE_TYPE_EXT_PATTERN_SCRIPTLET = iota++; +export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN = iota++; +export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS = iota++; +export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG = iota++; +export const NODE_TYPE_NET_RAW = iota++; +export const NODE_TYPE_NET_EXCEPTION = iota++; +export const NODE_TYPE_NET_PATTERN_RAW = iota++; +export const NODE_TYPE_NET_PATTERN = iota++; +export const NODE_TYPE_NET_PATTERN_PART = iota++; +export const NODE_TYPE_NET_PATTERN_PART_SPECIAL = iota++; +export const NODE_TYPE_NET_PATTERN_PART_UNICODE = iota++; +export const NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR = iota++; +export const NODE_TYPE_NET_PATTERN_LEFT_ANCHOR = iota++; +export const NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR = iota++; +export const NODE_TYPE_NET_OPTIONS_ANCHOR = iota++; +export const NODE_TYPE_NET_OPTIONS = iota++; +export const NODE_TYPE_NET_OPTION_SEPARATOR = iota++; +export const NODE_TYPE_NET_OPTION_SENTINEL = iota++; +export const NODE_TYPE_NET_OPTION_RAW = iota++; +export const NODE_TYPE_NET_OPTION_NAME_NOT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_UNKNOWN = iota++; +export const NODE_TYPE_NET_OPTION_NAME_1P = iota++; +export const NODE_TYPE_NET_OPTION_NAME_STRICT1P = iota++; +export const NODE_TYPE_NET_OPTION_NAME_3P = iota++; +export const NODE_TYPE_NET_OPTION_NAME_STRICT3P = iota++; +export const NODE_TYPE_NET_OPTION_NAME_ALL = iota++; +export const NODE_TYPE_NET_OPTION_NAME_BADFILTER = iota++; +export const NODE_TYPE_NET_OPTION_NAME_CNAME = iota++; +export const NODE_TYPE_NET_OPTION_NAME_CSP = iota++; +export const NODE_TYPE_NET_OPTION_NAME_CSS = iota++; +export const NODE_TYPE_NET_OPTION_NAME_DENYALLOW = iota++; +export const NODE_TYPE_NET_OPTION_NAME_DOC = iota++; +export const NODE_TYPE_NET_OPTION_NAME_EHIDE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_EMPTY = iota++; +export const NODE_TYPE_NET_OPTION_NAME_FONT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_FRAME = iota++; +export const NODE_TYPE_NET_OPTION_NAME_FROM = iota++; +export const NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK = iota++; +export const NODE_TYPE_NET_OPTION_NAME_GHIDE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_HEADER = iota++; +export const NODE_TYPE_NET_OPTION_NAME_IMAGE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_IMPORTANT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_INLINEFONT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_IPADDRESS = iota++; +export const NODE_TYPE_NET_OPTION_NAME_MATCHCASE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_MEDIA = iota++; +export const NODE_TYPE_NET_OPTION_NAME_METHOD = iota++; +export const NODE_TYPE_NET_OPTION_NAME_MP4 = iota++; +export const NODE_TYPE_NET_OPTION_NAME_NOOP = iota++; +export const NODE_TYPE_NET_OPTION_NAME_OBJECT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_OTHER = iota++; +export const NODE_TYPE_NET_OPTION_NAME_PERMISSIONS = iota++; +export const NODE_TYPE_NET_OPTION_NAME_PING = iota++; +export const NODE_TYPE_NET_OPTION_NAME_POPUNDER = iota++; +export const NODE_TYPE_NET_OPTION_NAME_POPUP = iota++; +export const NODE_TYPE_NET_OPTION_NAME_REASON = iota++; +export const NODE_TYPE_NET_OPTION_NAME_REDIRECT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM = iota++; +export const NODE_TYPE_NET_OPTION_NAME_REPLACE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_SCRIPT = iota++; +export const NODE_TYPE_NET_OPTION_NAME_SHIDE = iota++; +export const NODE_TYPE_NET_OPTION_NAME_TO = iota++; +export const NODE_TYPE_NET_OPTION_NAME_URLSKIP = iota++; +export const NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM = iota++; +export const NODE_TYPE_NET_OPTION_NAME_XHR = iota++; +export const NODE_TYPE_NET_OPTION_NAME_WEBRTC = iota++; +export const NODE_TYPE_NET_OPTION_NAME_WEBSOCKET = iota++; +export const NODE_TYPE_NET_OPTION_ASSIGN = iota++; +export const NODE_TYPE_NET_OPTION_QUOTE = iota++; +export const NODE_TYPE_NET_OPTION_VALUE = iota++; +export const NODE_TYPE_OPTION_VALUE_DOMAIN_LIST = iota++; +export const NODE_TYPE_OPTION_VALUE_DOMAIN_RAW = iota++; +export const NODE_TYPE_OPTION_VALUE_NOT = iota++; +export const NODE_TYPE_OPTION_VALUE_DOMAIN = iota++; +export const NODE_TYPE_OPTION_VALUE_SEPARATOR = iota++; +export const NODE_TYPE_PREPARSE_DIRECTIVE = iota++; +export const NODE_TYPE_PREPARSE_DIRECTIVE_VALUE = iota++; +export const NODE_TYPE_PREPARSE_DIRECTIVE_IF = iota++; +export const NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE = iota++; +export const NODE_TYPE_COMMENT_URL = iota++; +export const NODE_TYPE_COUNT = iota; + +iota = 0; +export const NODE_FLAG_IGNORE = 1 << iota++; +export const NODE_FLAG_ERROR = 1 << iota++; +export const NODE_FLAG_IS_NEGATED = 1 << iota++; +export const NODE_FLAG_OPTION_HAS_VALUE = 1 << iota++; +export const NODE_FLAG_PATTERN_UNTOKENIZABLE = 1 << iota++; + +export const nodeTypeFromOptionName = new Map([ + [ '', NODE_TYPE_NET_OPTION_NAME_UNKNOWN ], + [ '1p', NODE_TYPE_NET_OPTION_NAME_1P ], + /* synonym */ [ 'first-party', NODE_TYPE_NET_OPTION_NAME_1P ], + [ 'strict1p', NODE_TYPE_NET_OPTION_NAME_STRICT1P ], + /* synonym */ [ 'strict-first-party', NODE_TYPE_NET_OPTION_NAME_STRICT1P ], + [ '3p', NODE_TYPE_NET_OPTION_NAME_3P ], + /* synonym */ [ 'third-party', NODE_TYPE_NET_OPTION_NAME_3P ], + [ 'strict3p', NODE_TYPE_NET_OPTION_NAME_STRICT3P ], + /* synonym */ [ 'strict-third-party', NODE_TYPE_NET_OPTION_NAME_STRICT3P ], + [ 'all', NODE_TYPE_NET_OPTION_NAME_ALL ], + [ 'badfilter', NODE_TYPE_NET_OPTION_NAME_BADFILTER ], + [ 'cname', NODE_TYPE_NET_OPTION_NAME_CNAME ], + [ 'csp', NODE_TYPE_NET_OPTION_NAME_CSP ], + [ 'css', NODE_TYPE_NET_OPTION_NAME_CSS ], + /* synonym */ [ 'stylesheet', NODE_TYPE_NET_OPTION_NAME_CSS ], + [ 'denyallow', NODE_TYPE_NET_OPTION_NAME_DENYALLOW ], + [ 'doc', NODE_TYPE_NET_OPTION_NAME_DOC ], + /* synonym */ [ 'document', NODE_TYPE_NET_OPTION_NAME_DOC ], + [ 'ehide', NODE_TYPE_NET_OPTION_NAME_EHIDE ], + /* synonym */ [ 'elemhide', NODE_TYPE_NET_OPTION_NAME_EHIDE ], + [ 'empty', NODE_TYPE_NET_OPTION_NAME_EMPTY ], + [ 'font', NODE_TYPE_NET_OPTION_NAME_FONT ], + [ 'frame', NODE_TYPE_NET_OPTION_NAME_FRAME ], + /* synonym */ [ 'subdocument', NODE_TYPE_NET_OPTION_NAME_FRAME ], + [ 'from', NODE_TYPE_NET_OPTION_NAME_FROM ], + /* synonym */ [ 'domain', NODE_TYPE_NET_OPTION_NAME_FROM ], + [ 'genericblock', NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK ], + [ 'ghide', NODE_TYPE_NET_OPTION_NAME_GHIDE ], + /* synonym */ [ 'generichide', NODE_TYPE_NET_OPTION_NAME_GHIDE ], + [ 'header', NODE_TYPE_NET_OPTION_NAME_HEADER ], + [ 'image', NODE_TYPE_NET_OPTION_NAME_IMAGE ], + [ 'important', NODE_TYPE_NET_OPTION_NAME_IMPORTANT ], + [ 'inline-font', NODE_TYPE_NET_OPTION_NAME_INLINEFONT ], + [ 'inline-script', NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT ], + [ 'ipaddress', NODE_TYPE_NET_OPTION_NAME_IPADDRESS ], + [ 'match-case', NODE_TYPE_NET_OPTION_NAME_MATCHCASE ], + [ 'media', NODE_TYPE_NET_OPTION_NAME_MEDIA ], + [ 'method', NODE_TYPE_NET_OPTION_NAME_METHOD ], + [ 'mp4', NODE_TYPE_NET_OPTION_NAME_MP4 ], + [ '_', NODE_TYPE_NET_OPTION_NAME_NOOP ], + [ 'object', NODE_TYPE_NET_OPTION_NAME_OBJECT ], + /* synonym */ [ 'object-subrequest', NODE_TYPE_NET_OPTION_NAME_OBJECT ], + [ 'other', NODE_TYPE_NET_OPTION_NAME_OTHER ], + [ 'permissions', NODE_TYPE_NET_OPTION_NAME_PERMISSIONS ], + [ 'ping', NODE_TYPE_NET_OPTION_NAME_PING ], + /* synonym */ [ 'beacon', NODE_TYPE_NET_OPTION_NAME_PING ], + [ 'popunder', NODE_TYPE_NET_OPTION_NAME_POPUNDER ], + [ 'popup', NODE_TYPE_NET_OPTION_NAME_POPUP ], + [ 'reason', NODE_TYPE_NET_OPTION_NAME_REASON ], + [ 'redirect', NODE_TYPE_NET_OPTION_NAME_REDIRECT ], + /* synonym */ [ 'rewrite', NODE_TYPE_NET_OPTION_NAME_REDIRECT ], + [ 'redirect-rule', NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE ], + [ 'removeparam', NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM ], + [ 'replace', NODE_TYPE_NET_OPTION_NAME_REPLACE ], + /* synonym */ [ 'queryprune', NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM ], + [ 'script', NODE_TYPE_NET_OPTION_NAME_SCRIPT ], + [ 'shide', NODE_TYPE_NET_OPTION_NAME_SHIDE ], + /* synonym */ [ 'specifichide', NODE_TYPE_NET_OPTION_NAME_SHIDE ], + [ 'to', NODE_TYPE_NET_OPTION_NAME_TO ], + [ 'urlskip', NODE_TYPE_NET_OPTION_NAME_URLSKIP ], + [ 'uritransform', NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM ], + [ 'xhr', NODE_TYPE_NET_OPTION_NAME_XHR ], + /* synonym */ [ 'xmlhttprequest', NODE_TYPE_NET_OPTION_NAME_XHR ], + [ 'webrtc', NODE_TYPE_NET_OPTION_NAME_WEBRTC ], + [ 'websocket', NODE_TYPE_NET_OPTION_NAME_WEBSOCKET ], +]); + +export const nodeNameFromNodeType = new Map([ + [ NODE_TYPE_NOOP, 'noop' ], + [ NODE_TYPE_LINE_RAW, 'lineRaw' ], + [ NODE_TYPE_LINE_BODY, 'lineBody' ], + [ NODE_TYPE_WHITESPACE, 'whitespace' ], + [ NODE_TYPE_COMMENT, 'comment' ], + [ NODE_TYPE_IGNORE, 'ignore' ], + [ NODE_TYPE_EXT_RAW, 'extRaw' ], + [ NODE_TYPE_EXT_OPTIONS_ANCHOR, 'extOptionsAnchor' ], + [ NODE_TYPE_EXT_OPTIONS, 'extOptions' ], + [ NODE_TYPE_EXT_DECORATION, 'extDecoration' ], + [ NODE_TYPE_EXT_PATTERN_RAW, 'extPatternRaw' ], + [ NODE_TYPE_EXT_PATTERN_COSMETIC, 'extPatternCosmetic' ], + [ NODE_TYPE_EXT_PATTERN_HTML, 'extPatternHtml' ], + [ NODE_TYPE_EXT_PATTERN_RESPONSEHEADER, 'extPatternResponseheader' ], + [ NODE_TYPE_EXT_PATTERN_SCRIPTLET, 'extPatternScriptlet' ], + [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, 'extPatternScriptletToken' ], + [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, 'extPatternScriptletArgs' ], + [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, 'extPatternScriptletArg' ], + [ NODE_TYPE_NET_RAW, 'netRaw' ], + [ NODE_TYPE_NET_EXCEPTION, 'netException' ], + [ NODE_TYPE_NET_PATTERN_RAW, 'netPatternRaw' ], + [ NODE_TYPE_NET_PATTERN, 'netPattern' ], + [ NODE_TYPE_NET_PATTERN_PART, 'netPatternPart' ], + [ NODE_TYPE_NET_PATTERN_PART_SPECIAL, 'netPatternPartSpecial' ], + [ NODE_TYPE_NET_PATTERN_PART_UNICODE, 'netPatternPartUnicode' ], + [ NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR, 'netPatternLeftHnanchor' ], + [ NODE_TYPE_NET_PATTERN_LEFT_ANCHOR, 'netPatternLeftAnchor' ], + [ NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR, 'netPatternRightAnchor' ], + [ NODE_TYPE_NET_OPTIONS_ANCHOR, 'netOptionsAnchor' ], + [ NODE_TYPE_NET_OPTIONS, 'netOptions' ], + [ NODE_TYPE_NET_OPTION_RAW, 'netOptionRaw' ], + [ NODE_TYPE_NET_OPTION_SEPARATOR, 'netOptionSeparator'], + [ NODE_TYPE_NET_OPTION_SENTINEL, 'netOptionSentinel' ], + [ NODE_TYPE_NET_OPTION_NAME_NOT, 'netOptionNameNot'], + [ NODE_TYPE_NET_OPTION_ASSIGN, 'netOptionAssign' ], + [ NODE_TYPE_NET_OPTION_VALUE, 'netOptionValue' ], + [ NODE_TYPE_OPTION_VALUE_DOMAIN_LIST, 'netOptionValueDomainList' ], + [ NODE_TYPE_OPTION_VALUE_DOMAIN_RAW, 'netOptionValueDomainRaw' ], + [ NODE_TYPE_OPTION_VALUE_NOT, 'netOptionValueNot' ], + [ NODE_TYPE_OPTION_VALUE_DOMAIN, 'netOptionValueDomain' ], + [ NODE_TYPE_OPTION_VALUE_SEPARATOR, 'netOptionsValueSeparator' ], +]); +{ + for ( const [ name, type ] of nodeTypeFromOptionName ) { + nodeNameFromNodeType.set(type, name); + } +} + +/******************************************************************************/ + +// Local constants + +const DOMAIN_CAN_USE_WILDCARD = 0b0000001; +const DOMAIN_CAN_USE_ENTITY = 0b0000010; +const DOMAIN_CAN_USE_SINGLE_WILDCARD = 0b0000100; +const DOMAIN_CAN_BE_NEGATED = 0b0001000; +const DOMAIN_CAN_BE_REGEX = 0b0010000; +const DOMAIN_CAN_BE_ANCESTOR = 0b0100000; +const DOMAIN_CAN_HAVE_PATH = 0b1000000; + +const DOMAIN_FROM_FROMTO_LIST = DOMAIN_CAN_USE_ENTITY | + DOMAIN_CAN_BE_NEGATED | + DOMAIN_CAN_BE_REGEX; +const DOMAIN_FROM_DENYALLOW_LIST = 0; +const DOMAIN_FROM_EXT_LIST = DOMAIN_CAN_USE_ENTITY | + DOMAIN_CAN_USE_SINGLE_WILDCARD | + DOMAIN_CAN_BE_NEGATED | + DOMAIN_CAN_BE_REGEX | + DOMAIN_CAN_BE_ANCESTOR | + DOMAIN_CAN_HAVE_PATH; + +/******************************************************************************/ + +// Precomputed AST layouts for most common filters. + +const astTemplates = { + // ||example.com^ + netHnAnchoredHostnameAscii: { + flags: AST_FLAG_NET_PATTERN_LEFT_HNANCHOR | + AST_FLAG_NET_PATTERN_RIGHT_PATHANCHOR, + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_PATTERN_RAW, + beg: 0, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR, + beg: 0, + end: 2, + }, { + type: NODE_TYPE_NET_PATTERN, + beg: 2, + end: -1, + register: true, + }, { + type: NODE_TYPE_NET_PATTERN_PART_SPECIAL, + beg: -1, + end: 0, + }], + }], + }], + }, + // ||example.com^$third-party + net3pHnAnchoredHostnameAscii: { + flags: AST_FLAG_NET_PATTERN_LEFT_HNANCHOR | + AST_FLAG_NET_PATTERN_RIGHT_PATHANCHOR | + AST_FLAG_HAS_OPTIONS, + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_PATTERN_RAW, + beg: 0, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR, + beg: 0, + end: 2, + }, { + type: NODE_TYPE_NET_PATTERN, + beg: 2, + end: -13, + register: true, + }, { + type: NODE_TYPE_NET_PATTERN_PART_SPECIAL, + beg: -13, + end: -12, + }], + }, { + type: NODE_TYPE_NET_OPTIONS_ANCHOR, + beg: -12, + end: -11, + }, { + type: NODE_TYPE_NET_OPTIONS, + beg: -11, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_NET_OPTION_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_OPTION_NAME_3P, + beg: 0, + end: 0, + register: true, + }], + }], + }], + }], + }, + // ||example.com/path/to/resource + netHnAnchoredPlainAscii: { + flags: AST_FLAG_NET_PATTERN_LEFT_HNANCHOR, + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_PATTERN_RAW, + beg: 0, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR, + beg: 0, + end: 2, + }, { + type: NODE_TYPE_NET_PATTERN, + beg: 2, + end: 0, + register: true, + }], + }], + }], + }, + // example.com + // -resource. + netPlainAscii: { + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_PATTERN_RAW, + beg: 0, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_NET_PATTERN, + beg: 0, + end: 0, + register: true, + }], + }], + }], + }, + // 127.0.0.1 example.com + netHosts1: { + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_PATTERN_RAW, + beg: 0, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_IGNORE, + beg: 0, + end: 10, + }, { + type: NODE_TYPE_NET_PATTERN, + beg: 10, + end: 0, + register: true, + }], + }], + }], + }, + // 0.0.0.0 example.com + netHosts2: { + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_NET_PATTERN_RAW, + beg: 0, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_IGNORE, + beg: 0, + end: 8, + }, { + type: NODE_TYPE_NET_PATTERN, + beg: 8, + end: 0, + register: true, + }], + }], + }], + }, + // ##.ads-container + extPlainGenericSelector: { + type: NODE_TYPE_LINE_BODY, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_EXT_RAW, + beg: 0, + end: 0, + children: [{ + type: NODE_TYPE_EXT_OPTIONS_ANCHOR, + beg: 0, + end: 2, + register: true, + }, { + type: NODE_TYPE_EXT_PATTERN_RAW, + beg: 2, + end: 0, + register: true, + children: [{ + type: NODE_TYPE_EXT_PATTERN_COSMETIC, + beg: 0, + end: 0, + }], + }], + }], + }, +}; + +/******************************************************************************/ + +export const removableHTTPHeaders = new Set([ + 'location', + 'refresh', + 'report-to', + 'set-cookie', +]); + +export const preparserIfTokens = new Set([ + 'ext_ublock', + 'ext_ubol', + 'ext_devbuild', + 'env_chromium', + 'env_edge', + 'env_firefox', + 'env_legacy', + 'env_mobile', + 'env_mv3', + 'env_safari', + 'cap_html_filtering', + 'cap_ipaddress', + 'cap_user_stylesheet', + 'false', + 'ext_abp', + 'adguard', + 'adguard_app_android', + 'adguard_app_cli', + 'adguard_app_ios', + 'adguard_app_mac', + 'adguard_app_windows', + 'adguard_ext_android_cb', + 'adguard_ext_chromium', + 'adguard_ext_chromium_mv3', + 'adguard_ext_edge', + 'adguard_ext_firefox', + 'adguard_ext_opera', + 'adguard_ext_safari', +]); + +/******************************************************************************/ + +const exCharCodeAt = (s, i) => { + const pos = i >= 0 ? i : s.length + i; + return pos >= 0 ? s.charCodeAt(pos) : -1; +}; + +const escapeForRegex = s => + s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/******************************************************************************/ + +class AstWalker { + constructor(parser, from = 0) { + this.parser = parser; + this.stack = []; + this.reset(from); + } + get depth() { + return this.stackPtr; + } + reset(from = 0) { + this.nodes = this.parser.nodes; + this.stackPtr = 0; + return (this.current = from || this.parser.rootNode); + } + next() { + const current = this.current; + if ( current === 0 ) { return 0; } + const down = this.nodes[current+NODE_DOWN_INDEX]; + if ( down !== 0 ) { + this.stack[this.stackPtr++] = this.current; + return (this.current = down); + } + const right = this.nodes[current+NODE_RIGHT_INDEX]; + if ( right !== 0 && this.stackPtr !== 0 ) { + return (this.current = right); + } + while ( this.stackPtr !== 0 ) { + const parent = this.stack[--this.stackPtr]; + const right = this.nodes[parent+NODE_RIGHT_INDEX]; + if ( right !== 0 ) { + return (this.current = right); + } + } + return (this.current = 0); + } + right() { + const current = this.current; + if ( current === 0 ) { return 0; } + const right = this.nodes[current+NODE_RIGHT_INDEX]; + if ( right !== 0 && this.stackPtr !== 0 ) { + return (this.current = right); + } + while ( this.stackPtr !== 0 ) { + const parent = this.stack[--this.stackPtr]; + const right = this.nodes[parent+NODE_RIGHT_INDEX]; + if ( right !== 0 ) { + return (this.current = right); + } + } + return (this.current = 0); + } + until(which) { + let node = this.next(); + while ( node !== 0 ) { + if ( this.nodes[node+NODE_TYPE_INDEX] === which ) { return node; } + node = this.next(); + } + return 0; + } + canGoDown() { + return this.nodes[this.current+NODE_DOWN_INDEX] !== 0; + } + dispose() { + this.parser.walkerJunkyard.push(this); + } +} + +/******************************************************************************/ + +class DomainListIterator { + constructor(parser, root) { + this.parser = parser; + this.walker = parser.getWalker(); + this.value = undefined; + this.item = { hn: '', not: false, bad: false }; + this.reuse(root); + } + next() { + if ( this.done ) { return this.value; } + let node = this.walker.current; + let ready = false; + while ( node !== 0 ) { + switch ( this.parser.getNodeType(node) ) { + case NODE_TYPE_OPTION_VALUE_DOMAIN_RAW: + this.item.hn = ''; + this.item.not = false; + this.item.bad = this.parser.getNodeFlags(node, NODE_FLAG_ERROR) !== 0; + break; + case NODE_TYPE_OPTION_VALUE_NOT: + this.item.not = true; + break; + case NODE_TYPE_OPTION_VALUE_DOMAIN: + this.item.hn = this.parser.getNodeTransform(node); + this.value = this.item; + ready = true; + break; + default: + break; + } + node = this.walker.next(); + if ( ready ) { return this; } + } + return this.stop(); + } + reuse(root) { + this.walker.reset(root); + this.done = false; + return this; + } + stop() { + this.done = true; + this.value = undefined; + this.parser.domainListIteratorJunkyard.push(this); + return this; + } + [Symbol.iterator]() { + return this; + } +} + +/******************************************************************************/ + +export class AstFilterParser { + constructor(options = {}) { + this.raw = ''; + this.rawEnd = 0; + this.nodes = new Uint32Array(16384); + this.nodePoolPtr = FULL_NODE_SIZE; + this.nodePoolEnd = this.nodes.length; + this.astTransforms = [ null ]; + this.astTransformPtr = 1; + this.rootNode = 0; + this.astType = AST_TYPE_NONE; + this.astTypeFlavor = AST_TYPE_NONE; + this.astFlags = 0; + this.astError = 0; + this.nodeTypeRegister = []; + this.nodeTypeRegisterPtr = 0; + this.nodeTypeLookupTable = new Uint32Array(NODE_TYPE_COUNT); + this.punycoder = new URL('https://ublock0.invalid/'); + this.domainListIteratorJunkyard = []; + this.walkerJunkyard = []; + this.hasWhitespace = false; + this.hasUnicode = false; + this.hasUppercase = false; + // Options + this.options = options; + this.interactive = options.interactive || false; + this.badTypes = new Set(options.badTypes || []); + this.maxTokenLength = options.maxTokenLength || 7; + // TODO: rethink this + this.result = { exception: false, raw: '', compiled: '', error: undefined }; + this.selectorCompiler = new ExtSelectorCompiler(options); + // Regexes + this.reWhitespaceStart = /^\s+/; + this.reWhitespaceEnd = /(?:^|\S)(\s+)$/; + this.reCommentLine = /^(?:!|#\s|####|\[adblock)/i; + this.reExtAnchor = /(#@?(?:\$\?|\$|%|\?)?#).{1,2}/; + this.reInlineComment = /(?:\s+#).*?$/; + this.reNetException = /^@@/; + this.reNetAnchor = /(?:)\$[^,\w~]/; + this.reHnAnchoredPlainAscii = /^\|\|[0-9a-z%&,\-./:;=?_]+$/; + this.reHnAnchoredHostnameAscii = /^\|\|(?:[\da-z][\da-z_-]*\.)*[\da-z_-]*[\da-z]\^$/; + this.reHnAnchoredHostnameUnicode = /^\|\|(?:[\p{L}\p{N}][\p{L}\p{N}\u{2d}]*\.)*[\p{L}\p{N}\u{2d}]*[\p{L}\p{N}]\^$/u; + this.reHn3pAnchoredHostnameAscii = /^\|\|(?:[\da-z][\da-z_-]*\.)*[\da-z_-]*[\da-z]\^\$third-party$/; + this.rePlainAscii = /^[0-9a-z%&\-./:;=?_]{2,}$/; + this.reNetHosts1 = /^127\.0\.0\.1 (?:[\da-z][\da-z_-]*\.)+[\da-z-]*[a-z]$/; + this.reNetHosts2 = /^0\.0\.0\.0 (?:[\da-z][\da-z_-]*\.)+[\da-z-]*[a-z]$/; + this.rePlainGenericCosmetic = /^##[.#][A-Za-z_][\w-]*$/; + this.reHostnameAscii = /^(?:[\da-z][\da-z_-]*\.)*[\da-z][\da-z-]*[\da-z]$/; + this.rePlainEntity = /^(?:[\da-z][\da-z_-]*\.)+\*$/; + this.reHostsSink = /^[\w%.:[\]-]+\s+/; + this.reHostsRedirect = /(?:0\.0\.0\.0|broadcasthost|local|localhost(?:\.localdomain)?|ip6-\w+)(?:[^\w.-]|$)/; + this.reNetOptionComma = /,(?:~?[13a-z-]+(?:=.*?)?|_+)(?:,|$)/; + this.rePointlessLeftAnchor = /^\|\|?\*+/; + this.rePointlessLeadingWildcards = /^(\*+)[^%0-9A-Za-z\u{a0}-\u{10FFFF}]/u; + this.rePointlessTrailingSeparator = /\*(\^\**)$/; + this.rePointlessTrailingWildcards = /(?:[^%0-9A-Za-z]|[%0-9A-Za-z]{7,})(\*+)$/; + this.reHasWhitespaceChar = /\s/; + this.reHasUppercaseChar = /[A-Z]/; + this.reHasUnicodeChar = /[^\x00-\x7F]/; + this.reUnicodeChars = /\P{ASCII}/gu; + this.reBadHostnameChars = /[\x00-\x24\x26-\x29\x2b\x2c\x2f\x3b-\x40\x5c\x5e\x60\x7b-\x7f]/; + this.reIsEntity = /^[^*]+\.\*$/; + this.rePreparseDirectiveIf = /^!#if /; + this.rePreparseDirectiveAny = /^!#(?:else|endif|if |include )/; + this.reURL = /\bhttps?:\/\/\S+/; + this.reHasPatternSpecialChars = /[*^]/; + this.rePatternAllSpecialChars = /[*^]+|[^\x00-\x7f]+/g; + // https://github.com/uBlockOrigin/uBlock-issues/issues/1146 + // From https://codemirror.net/doc/manual.html#option_specialChars + this.reHasInvalidChar = /[\x00-\x1F\x7F-\x9F\xAD\u061C\u200B-\u200F\u2028\u2029\uFEFF\uFFF9-\uFFFC]/; + this.reHostnamePatternPart = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]+/; + this.reHostnameLabel = /[^.]+/g; + this.reResponseheaderPattern = /^\^responseheader\(.*\)$/; + this.rePatternScriptletJsonArgs = /^\{.*\}$/; + this.reBadCSP = /(?:^|[;,])\s*report-(?:to|uri)\b/i; + this.reBadPP = /(?:^|[;,])\s*report-to\b/i; + this.reNetOption = /^(~?)([134a-z_-]+)(=?)/; + this.reNoopOption = /^_+$/; + this.reAdvancedDomainSyntax = /^([^>]+?)(>>)?(\/.*)?$/; + this.netOptionValueParser = new ArglistParser(','); + this.scriptletArgListParser = new ArglistParser(','); + this.domainRegexValueParser = new ArglistParser('/'); + } + + finish() { + this.selectorCompiler.finish(); + } + + parse(raw) { + this.raw = raw; + this.rawEnd = raw.length; + this.nodePoolPtr = FULL_NODE_SIZE; + this.nodeTypeRegisterPtr = 0; + this.astTransformPtr = 1; + this.astType = AST_TYPE_NONE; + this.astTypeFlavor = AST_TYPE_NONE; + this.astFlags = 0; + this.astError = 0; + this.rootNode = this.allocTypedNode(NODE_TYPE_LINE_RAW, 0, this.rawEnd); + if ( this.rawEnd === 0 ) { return; } + + // Fast-track very common simple filters using pre-computed AST layouts + // to skip parsing and validation. + const c1st = this.raw.charCodeAt(0); + const clast = exCharCodeAt(this.raw, -1); + if ( c1st === 0x7C /* | */ ) { + if ( + clast === 0x5E /* ^ */ && + this.reHnAnchoredHostnameAscii.test(this.raw) + ) { + // ||example.com^ + this.astType = AST_TYPE_NETWORK; + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + const node = this.astFromTemplate(this.rootNode, + astTemplates.netHnAnchoredHostnameAscii + ); + this.linkDown(this.rootNode, node); + return; + } + if ( + this.raw.endsWith('$third-party') && + this.reHn3pAnchoredHostnameAscii.test(this.raw) + ) { + // ||example.com^$third-party + this.astType = AST_TYPE_NETWORK; + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + const node = this.astFromTemplate(this.rootNode, + astTemplates.net3pHnAnchoredHostnameAscii + ); + this.linkDown(this.rootNode, node); + return; + } + if ( this.reHnAnchoredPlainAscii.test(this.raw) ) { + // ||example.com/path/to/resource + this.astType = AST_TYPE_NETWORK; + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_PLAIN; + const node = this.astFromTemplate(this.rootNode, + astTemplates.netHnAnchoredPlainAscii + ); + this.linkDown(this.rootNode, node); + return; + } + } else if ( c1st === 0x23 /* # */ ) { + if ( this.rePlainGenericCosmetic.test(this.raw) ) { + // ##.ads-container + this.astType = AST_TYPE_EXTENDED; + this.astTypeFlavor = AST_TYPE_EXTENDED_COSMETIC; + const node = this.astFromTemplate(this.rootNode, + astTemplates.extPlainGenericSelector + ); + this.linkDown(this.rootNode, node); + this.result.exception = false; + this.result.raw = this.raw.slice(2); + this.result.compiled = this.raw.slice(2); + return; + } + } else if ( c1st === 0x31 /* 1 */ ) { + if ( this.reNetHosts1.test(this.raw) ) { + // 127.0.0.1 example.com + this.astType = AST_TYPE_NETWORK; + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + const node = this.astFromTemplate(this.rootNode, + astTemplates.netHosts1 + ); + this.linkDown(this.rootNode, node); + return; + } + } else if ( c1st === 0x30 /* 0 */ ) { + if ( this.reNetHosts2.test(this.raw) ) { + // 0.0.0.0 example.com + this.astType = AST_TYPE_NETWORK; + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + const node = this.astFromTemplate(this.rootNode, + astTemplates.netHosts2 + ); + this.linkDown(this.rootNode, node); + return; + } + } else if ( + (c1st !== 0x2F /* / */ || clast !== 0x2F /* / */) && + (this.rePlainAscii.test(this.raw)) + ) { + // example.com + // -resource. + this.astType = AST_TYPE_NETWORK; + this.astTypeFlavor = this.reHostnameAscii.test(this.raw) + ? AST_TYPE_NETWORK_PATTERN_HOSTNAME + : AST_TYPE_NETWORK_PATTERN_PLAIN; + const node = this.astFromTemplate(this.rootNode, + astTemplates.netPlainAscii + ); + this.linkDown(this.rootNode, node); + return; + } + + // All else: full parsing and validation. + this.hasWhitespace = this.reHasWhitespaceChar.test(raw); + this.linkDown(this.rootNode, this.parseRaw(this.rootNode)); + } + + astFromTemplate(parent, template) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const beg = template.beg + (template.beg >= 0 ? parentBeg : parentEnd); + const end = template.end + (template.end <= 0 ? parentEnd : parentBeg); + const node = this.allocTypedNode(template.type, beg, end); + if ( template.register ) { + this.addNodeToRegister(template.type, node); + } + if ( template.flags ) { + this.addFlags(template.flags); + } + if ( template.nodeFlags ) { + this.addNodeFlags(node, template.nodeFlags); + } + const children = template.children; + if ( children === undefined ) { return node; } + const head = this.astFromTemplate(node, children[0]); + this.linkDown(node, head); + const n = children.length; + if ( n === 1 ) { return node; } + let prev = head; + for ( let i = 1; i < n; i++ ) { + prev = this.linkRight(prev, this.astFromTemplate(node, children[i])); + } + return node; + } + + getType() { + return this.astType; + } + + isComment() { + return this.astType === AST_TYPE_COMMENT; + } + + isFilter() { + return this.isNetworkFilter() || this.isExtendedFilter(); + } + + isNetworkFilter() { + return this.astType === AST_TYPE_NETWORK; + } + + isExtendedFilter() { + return this.astType === AST_TYPE_EXTENDED; + } + + isCosmeticFilter() { + return this.astType === AST_TYPE_EXTENDED && + this.astTypeFlavor === AST_TYPE_EXTENDED_COSMETIC; + } + + isScriptletFilter() { + return this.astType === AST_TYPE_EXTENDED && + this.astTypeFlavor === AST_TYPE_EXTENDED_SCRIPTLET; + } + + isHtmlFilter() { + return this.astType === AST_TYPE_EXTENDED && + this.astTypeFlavor === AST_TYPE_EXTENDED_HTML; + } + + isResponseheaderFilter() { + return this.astType === AST_TYPE_EXTENDED && + this.astTypeFlavor === AST_TYPE_EXTENDED_RESPONSEHEADER; + } + + getFlags(flags = 0xFFFFFFFF) { + return this.astFlags & flags; + } + + addFlags(flags) { + this.astFlags |= flags; + } + + parseRaw(parent) { + const head = this.allocHeadNode(); + let prev = head, next = 0; + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const l1 = this.hasWhitespace + ? this.leftWhitespaceCount(this.getNodeString(parent)) + : 0; + if ( l1 !== 0 ) { + next = this.allocTypedNode( + NODE_TYPE_WHITESPACE, + parentBeg, + parentBeg + l1 + ); + prev = this.linkRight(prev, next); + if ( l1 === parentEnd ) { return this.throwHeadNode(head); } + } + const r0 = this.hasWhitespace + ? parentEnd - this.rightWhitespaceCount(this.getNodeString(parent)) + : parentEnd; + if ( r0 !== l1 ) { + next = this.allocTypedNode( + NODE_TYPE_LINE_BODY, + parentBeg + l1, + parentBeg + r0 + ); + this.linkDown(next, this.parseFilter(next)); + prev = this.linkRight(prev, next); + } + if ( r0 !== parentEnd ) { + next = this.allocTypedNode( + NODE_TYPE_WHITESPACE, + parentBeg + r0, + parentEnd + ); + this.linkRight(prev, next); + } + return this.throwHeadNode(head); + } + + parseFilter(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const parentStr = this.getNodeString(parent); + + // A comment? + if ( this.reCommentLine.test(parentStr) ) { + const head = this.allocTypedNode(NODE_TYPE_COMMENT, parentBeg, parentEnd); + this.astType = AST_TYPE_COMMENT; + if ( this.interactive ) { + this.linkDown(head, this.parseComment(head)); + } + return head; + } + + // An extended filter? (or rarely, a comment) + if ( this.reExtAnchor.test(parentStr) ) { + const match = this.reExtAnchor.exec(parentStr); + const matchLen = match[1].length; + const head = this.allocTypedNode(NODE_TYPE_EXT_RAW, parentBeg, parentEnd); + this.linkDown(head, this.parseExt(head, parentBeg + match.index, matchLen)); + return head; + } else if ( parentStr.charCodeAt(0) === 0x23 /* # */ ) { + const head = this.allocTypedNode(NODE_TYPE_COMMENT, parentBeg, parentEnd); + this.astType = AST_TYPE_COMMENT; + return head; + } + + // Good to know in advance to avoid costly tests later on + this.hasUppercase = this.reHasUppercaseChar.test(parentStr); + this.hasUnicode = this.reHasUnicodeChar.test(parentStr); + + // A network filter (probably) + this.astType = AST_TYPE_NETWORK; + + // Parse inline comment if any + let tail = 0, tailStart = parentEnd; + if ( this.hasWhitespace && this.reInlineComment.test(parentStr) ) { + const match = this.reInlineComment.exec(parentStr); + tailStart = parentBeg + match.index; + tail = this.allocTypedNode(NODE_TYPE_COMMENT, tailStart, parentEnd); + } + + const head = this.allocTypedNode(NODE_TYPE_NET_RAW, parentBeg, tailStart); + if ( this.linkDown(head, this.parseNet(head)) === 0 ) { + this.astType = AST_TYPE_UNKNOWN; + this.addFlags(AST_FLAG_UNSUPPORTED | AST_FLAG_HAS_ERROR); + } + if ( tail !== 0 ) { + this.linkRight(head, tail); + } + return head; + } + + parseComment(parent) { + const parentStr = this.getNodeString(parent); + if ( this.rePreparseDirectiveAny.test(parentStr) ) { + this.astTypeFlavor = AST_TYPE_COMMENT_PREPARSER; + return this.parsePreparseDirective(parent, parentStr); + } + if ( this.reURL.test(parentStr) === false ) { return 0; } + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const match = this.reURL.exec(parentStr); + const urlBeg = parentBeg + match.index; + const urlEnd = urlBeg + match[0].length; + const head = this.allocTypedNode(NODE_TYPE_COMMENT, parentBeg, urlBeg); + let next = this.allocTypedNode(NODE_TYPE_COMMENT_URL, urlBeg, urlEnd); + let prev = this.linkRight(head, next); + if ( urlEnd !== parentEnd ) { + next = this.allocTypedNode(NODE_TYPE_COMMENT, urlEnd, parentEnd); + this.linkRight(prev, next); + } + return head; + } + + parsePreparseDirective(parent, s) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const match = this.rePreparseDirectiveAny.exec(s); + const directiveEnd = parentBeg + match[0].length; + const head = this.allocTypedNode( + NODE_TYPE_PREPARSE_DIRECTIVE, + parentBeg, + directiveEnd + ); + if ( directiveEnd !== parentEnd ) { + const type = s.startsWith('!#if ') + ? NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE + : NODE_TYPE_PREPARSE_DIRECTIVE_VALUE; + const next = this.allocTypedNode(type, directiveEnd, parentEnd); + this.addNodeToRegister(type, next); + this.linkRight(head, next); + if ( type === NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE ) { + const rawToken = this.getNodeString(next).trim(); + if ( utils.preparser.evaluateExpr(rawToken) === undefined ) { + this.addNodeFlags(next, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_IF_TOKEN_UNKNOWN; + } + } + } + return head; + } + + // Very common, look into fast-tracking such plain pattern: + // /^[^!#\$\*\^][^#\$\*\^]*[^\$\*\|]$/ + parseNet(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const parentStr = this.getNodeString(parent); + const head = this.allocHeadNode(); + let patternBeg = parentBeg; + let prev = head, next = 0, tail = 0; + if ( this.reNetException.test(parentStr) ) { + this.addFlags(AST_FLAG_IS_EXCEPTION); + next = this.allocTypedNode(NODE_TYPE_NET_EXCEPTION, parentBeg, parentBeg+2); + prev = this.linkRight(prev, next); + patternBeg += 2; + } + let anchorBeg = this.indexOfNetAnchor(parentStr, patternBeg); + if ( anchorBeg === -1 ) { return 0; } + anchorBeg += parentBeg; + if ( anchorBeg !== parentEnd ) { + tail = this.allocTypedNode( + NODE_TYPE_NET_OPTIONS_ANCHOR, + anchorBeg, + anchorBeg + 1 + ); + next = this.allocTypedNode( + NODE_TYPE_NET_OPTIONS, + anchorBeg + 1, + parentEnd + ); + this.addFlags(AST_FLAG_HAS_OPTIONS); + this.addNodeToRegister(NODE_TYPE_NET_OPTIONS, next); + this.linkDown(next, this.parseNetOptions(next)); + this.linkRight(tail, next); + } + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN_RAW, + patternBeg, + anchorBeg + ); + this.addNodeToRegister(NODE_TYPE_NET_PATTERN_RAW, next); + this.linkDown(next, this.parseNetPattern(next)); + prev = this.linkRight(prev, next); + if ( tail !== 0 ) { + this.linkRight(prev, tail); + } + if ( this.astType === AST_TYPE_NETWORK ) { + this.validateNet(); + } + return this.throwHeadNode(head); + } + + validateNet() { + const isException = this.isException(); + let bad = false, realBad = false; + let abstractTypeCount = 0; + let behaviorTypeCount = 0; + let docTypeCount = 0; + let modifierType = 0; + let requestTypeCount = 0; + let unredirectableTypeCount = 0; + let badfilter = false; + for ( let i = 0, n = this.nodeTypeRegisterPtr; i < n; i++ ) { + const type = this.nodeTypeRegister[i]; + const targetNode = this.nodeTypeLookupTable[type]; + if ( targetNode === 0 ) { continue; } + if ( this.badTypes.has(type) ) { + this.addNodeFlags(NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_OPTION_EXCLUDED; + } + const flags = this.getNodeFlags(targetNode); + if ( (flags & NODE_FLAG_ERROR) !== 0 ) { continue; } + const isNegated = (flags & NODE_FLAG_IS_NEGATED) !== 0; + const hasValue = (flags & NODE_FLAG_OPTION_HAS_VALUE) !== 0; + bad = false; realBad = false; + switch ( type ) { + case NODE_TYPE_NET_OPTION_NAME_ALL: + realBad = isNegated || hasValue || modifierType !== 0; + break; + case NODE_TYPE_NET_OPTION_NAME_1P: + case NODE_TYPE_NET_OPTION_NAME_3P: + realBad = hasValue; + break; + case NODE_TYPE_NET_OPTION_NAME_BADFILTER: + badfilter = true; + /* falls through */ + case NODE_TYPE_NET_OPTION_NAME_NOOP: + realBad = isNegated || hasValue; + break; + case NODE_TYPE_NET_OPTION_NAME_CSS: + case NODE_TYPE_NET_OPTION_NAME_FONT: + case NODE_TYPE_NET_OPTION_NAME_IMAGE: + case NODE_TYPE_NET_OPTION_NAME_MEDIA: + case NODE_TYPE_NET_OPTION_NAME_OBJECT: + case NODE_TYPE_NET_OPTION_NAME_OTHER: + case NODE_TYPE_NET_OPTION_NAME_SCRIPT: + case NODE_TYPE_NET_OPTION_NAME_XHR: + realBad = hasValue; + if ( realBad ) { break; } + requestTypeCount += 1; + break; + case NODE_TYPE_NET_OPTION_NAME_CNAME: + realBad = isException === false || isNegated || hasValue; + if ( realBad ) { break; } + modifierType = type; + break; + case NODE_TYPE_NET_OPTION_NAME_CSP: + realBad = (hasValue || isException) === false || + modifierType !== 0 || + this.reBadCSP.test( + this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_CSP) + ); + if ( realBad ) { break; } + modifierType = type; + break; + case NODE_TYPE_NET_OPTION_NAME_DENYALLOW: + realBad = isNegated || hasValue === false || + this.getBranchFromType(NODE_TYPE_NET_OPTION_NAME_FROM) === 0; + break; + case NODE_TYPE_NET_OPTION_NAME_DOC: + case NODE_TYPE_NET_OPTION_NAME_FRAME: + realBad = hasValue; + if ( realBad ) { break; } + docTypeCount += 1; + break; + case NODE_TYPE_NET_OPTION_NAME_EHIDE: + case NODE_TYPE_NET_OPTION_NAME_GHIDE: + case NODE_TYPE_NET_OPTION_NAME_SHIDE: + realBad = isNegated || hasValue || modifierType !== 0; + if ( realBad ) { break; } + behaviorTypeCount += 1; + unredirectableTypeCount += 1; + break; + case NODE_TYPE_NET_OPTION_NAME_EMPTY: + case NODE_TYPE_NET_OPTION_NAME_MP4: + realBad = isNegated || hasValue || modifierType !== 0; + if ( realBad ) { break; } + modifierType = type; + break; + case NODE_TYPE_NET_OPTION_NAME_FROM: + case NODE_TYPE_NET_OPTION_NAME_METHOD: + case NODE_TYPE_NET_OPTION_NAME_TO: + realBad = isNegated || hasValue === false; + break; + case NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK: + bad = true; + realBad = isException === false || isNegated || hasValue; + break; + case NODE_TYPE_NET_OPTION_NAME_HEADER: + realBad = isNegated || hasValue === false; + break; + case NODE_TYPE_NET_OPTION_NAME_IMPORTANT: + realBad = isException || isNegated || hasValue; + break; + case NODE_TYPE_NET_OPTION_NAME_INLINEFONT: + case NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT: + realBad = hasValue; + if ( realBad ) { break; } + modifierType = type; + unredirectableTypeCount += 1; + break; + case NODE_TYPE_NET_OPTION_NAME_IPADDRESS: { + const value = this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_IPADDRESS); + if ( /^\/.+\/$/.test(value) ) { + try { void new RegExp(value); } + catch { realBad = true; } + } + break; + } + case NODE_TYPE_NET_OPTION_NAME_MATCHCASE: + realBad = this.isRegexPattern() === false; + break; + case NODE_TYPE_NET_OPTION_NAME_PERMISSIONS: + realBad = modifierType !== 0 || + (hasValue || isException) === false || + this.reBadPP.test( + this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_PERMISSIONS) + ); + if ( realBad ) { break; } + modifierType = type; + break; + case NODE_TYPE_NET_OPTION_NAME_PING: + case NODE_TYPE_NET_OPTION_NAME_WEBSOCKET: + realBad = hasValue; + if ( realBad ) { break; } + requestTypeCount += 1; + unredirectableTypeCount += 1; + break; + case NODE_TYPE_NET_OPTION_NAME_POPUNDER: + case NODE_TYPE_NET_OPTION_NAME_POPUP: + realBad = hasValue; + if ( realBad ) { break; } + abstractTypeCount += 1; + unredirectableTypeCount += 1; + break; + case NODE_TYPE_NET_OPTION_NAME_REASON: + realBad = hasValue === false; + break; + case NODE_TYPE_NET_OPTION_NAME_REDIRECT: + case NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE: + case NODE_TYPE_NET_OPTION_NAME_REPLACE: + case NODE_TYPE_NET_OPTION_NAME_URLSKIP: + case NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM: + realBad = isNegated || (isException || hasValue) === false || + modifierType !== 0; + if ( realBad ) { break; } + modifierType = type; + break; + case NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM: + realBad = isNegated || modifierType !== 0; + if ( realBad ) { break; } + modifierType = type; + break; + case NODE_TYPE_NET_OPTION_NAME_STRICT1P: + case NODE_TYPE_NET_OPTION_NAME_STRICT3P: + realBad = isNegated || hasValue; + break; + case NODE_TYPE_NET_OPTION_NAME_UNKNOWN: + this.astError = AST_ERROR_OPTION_UNKNOWN; + realBad = true; + break; + case NODE_TYPE_NET_OPTION_NAME_WEBRTC: + realBad = true; + break; + case NODE_TYPE_NET_PATTERN_RAW: + realBad = this.hasOptions() === false && + this.getNetPattern().length <= 1; + break; + default: + break; + } + if ( bad || realBad ) { + this.addNodeFlags(targetNode, NODE_FLAG_ERROR); + } + if ( realBad ) { + this.addFlags(AST_FLAG_HAS_ERROR); + } + } + const requiresTrustedSource = ( ) => + this.options.trustedSource !== true && + isException === false && badfilter === false; + switch ( modifierType ) { + case NODE_TYPE_NET_OPTION_NAME_CNAME: + realBad = abstractTypeCount || behaviorTypeCount || requestTypeCount; + break; + case NODE_TYPE_NET_OPTION_NAME_CSP: + case NODE_TYPE_NET_OPTION_NAME_PERMISSIONS: + realBad = abstractTypeCount || behaviorTypeCount || requestTypeCount; + break; + case NODE_TYPE_NET_OPTION_NAME_INLINEFONT: + case NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT: + realBad = behaviorTypeCount; + break; + case NODE_TYPE_NET_OPTION_NAME_EMPTY: + realBad = abstractTypeCount || behaviorTypeCount; + break; + case NODE_TYPE_NET_OPTION_NAME_MEDIA: + case NODE_TYPE_NET_OPTION_NAME_MP4: + realBad = abstractTypeCount || behaviorTypeCount || docTypeCount || requestTypeCount; + break; + case NODE_TYPE_NET_OPTION_NAME_REDIRECT: + case NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE: { + realBad = abstractTypeCount || behaviorTypeCount || unredirectableTypeCount; + break; + } + case NODE_TYPE_NET_OPTION_NAME_REPLACE: { + realBad = abstractTypeCount || behaviorTypeCount || unredirectableTypeCount; + if ( realBad ) { break; } + if ( requiresTrustedSource() ) { + this.astError = AST_ERROR_UNTRUSTED_SOURCE; + realBad = true; + break; + } + const value = this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_REPLACE); + if ( parseReplaceValue(value) === undefined ) { + this.astError = AST_ERROR_OPTION_BADVALUE; + realBad = true; + } + break; + } + case NODE_TYPE_NET_OPTION_NAME_URLSKIP: { + realBad = abstractTypeCount || behaviorTypeCount || unredirectableTypeCount; + if ( realBad ) { break; } + if ( requiresTrustedSource() ) { + this.astError = AST_ERROR_UNTRUSTED_SOURCE; + realBad = true; + break; + } + const value = this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_URLSKIP); + if ( value.length < 2 ) { + this.astError = AST_ERROR_OPTION_BADVALUE; + realBad = true; + } + break; + } + case NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM: { + realBad = abstractTypeCount || behaviorTypeCount || unredirectableTypeCount; + if ( realBad ) { break; } + if ( requiresTrustedSource() ) { + this.astError = AST_ERROR_UNTRUSTED_SOURCE; + realBad = true; + break; + } + const value = this.getNetOptionValue(NODE_TYPE_NET_OPTION_NAME_URLTRANSFORM); + if ( value !== '' && parseReplaceByRegexValue(value) === undefined ) { + this.astError = AST_ERROR_OPTION_BADVALUE; + realBad = true; + } + break; + } + case NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM: + realBad = abstractTypeCount || behaviorTypeCount; + break; + default: + break; + } + if ( realBad ) { + const targetNode = this.getBranchFromType(modifierType); + this.addNodeFlags(targetNode, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + } + + indexOfNetAnchor(s, start = 0) { + const end = s.length; + if ( end === start ) { return end; } + let j = s.lastIndexOf('$'); + if ( j === -1 ) { return end; } + if ( (j+1) === end ) { return end; } + for (;;) { + const before = s.charAt(j-1); + if ( before === '$' ) { return -1; } + const after = s.charAt(j+1); + if ( ')/|'.includes(after) === false ) { + if ( before === '' || '"\'\\`'.includes(before) === false ) { + return j; + } + } + if ( j === start ) { break; } + j = s.lastIndexOf('$', j-1); + if ( j === -1 ) { break; } + } + return end; + } + + parseNetPattern(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + + // Empty pattern + if ( parentEnd === parentBeg ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_ANY; + const node = this.allocTypedNode( + NODE_TYPE_NET_PATTERN, + parentBeg, + parentEnd + ); + this.addNodeToRegister(NODE_TYPE_NET_PATTERN, node); + this.setNodeTransform(node, '*'); + return node; + } + + const head = this.allocHeadNode(); + let prev = head, next = 0, tail = 0; + let pattern = this.getNodeString(parent); + const hasWildcard = pattern.includes('*'); + const c1st = pattern.charCodeAt(0); + const c2nd = pattern.charCodeAt(1) || 0; + const clast = exCharCodeAt(pattern, -1); + + // Common case: Easylist syntax-based hostname + if ( + hasWildcard === false && + c1st === 0x7C /* | */ && c2nd === 0x7C /* | */ && + clast === 0x5E /* ^ */ && + this.isAdblockHostnamePattern(pattern) + ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + this.addFlags( + AST_FLAG_NET_PATTERN_LEFT_HNANCHOR | + AST_FLAG_NET_PATTERN_RIGHT_PATHANCHOR + ); + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR, + parentBeg, + parentBeg + 2 + ); + prev = this.linkRight(prev, next); + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN, + parentBeg + 2, + parentEnd - 1 + ); + pattern = pattern.slice(2, -1); + const normal = this.hasUnicode + ? this.normalizeHostnameValue(pattern) + : pattern; + if ( normal !== undefined && normal !== pattern ) { + this.setNodeTransform(next, normal); + } + this.addNodeToRegister(NODE_TYPE_NET_PATTERN, next); + prev = this.linkRight(prev, next); + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN_PART_SPECIAL, + parentEnd - 1, + parentEnd + ); + this.linkRight(prev, next); + return this.throwHeadNode(head); + } + + let patternBeg = parentBeg; + let patternEnd = parentEnd; + + // Hosts file entry? + if ( + this.hasWhitespace && + this.isException() === false && + this.hasOptions() === false && + this.reHostsSink.test(pattern) + ) { + const match = this.reHostsSink.exec(pattern); + patternBeg += match[0].length; + pattern = pattern.slice(patternBeg); + next = this.allocTypedNode(NODE_TYPE_IGNORE, parentBeg, patternBeg); + prev = this.linkRight(prev, next); + if ( + this.reHostsRedirect.test(pattern) || + this.reHostnameAscii.test(pattern) === false + ) { + this.astType = AST_TYPE_NONE; + this.addFlags(AST_FLAG_IGNORE); + next = this.allocTypedNode(NODE_TYPE_IGNORE, patternBeg, parentEnd); + prev = this.linkRight(prev, next); + return this.throwHeadNode(head); + } + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + this.addFlags( + AST_FLAG_NET_PATTERN_LEFT_HNANCHOR | + AST_FLAG_NET_PATTERN_RIGHT_PATHANCHOR + ); + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN, + patternBeg, + parentEnd + ); + this.addNodeToRegister(NODE_TYPE_NET_PATTERN, next); + this.linkRight(prev, next); + return this.throwHeadNode(head); + } + + // Regex? + if ( + c1st === 0x2F /* / */ && clast === 0x2F /* / */ && + pattern.length > 2 + ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_REGEX; + const normal = this.normalizeRegexPattern(pattern); + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN, + patternBeg, + patternEnd + ); + this.addNodeToRegister(NODE_TYPE_NET_PATTERN, next); + if ( normal !== '' ) { + if ( normal !== pattern ) { + this.setNodeTransform(next, normal); + } + } else { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_BAD; + this.astError = AST_ERROR_REGEX; + this.addFlags(AST_FLAG_HAS_ERROR); + this.addNodeFlags(next, NODE_FLAG_ERROR); + } + this.linkRight(prev, next); + return this.throwHeadNode(head); + } + + // Left anchor + if ( c1st === 0x7C /* '|' */ ) { + if ( c2nd === 0x7C /* '|' */ ) { + const type = this.isTokenCharCode(pattern.charCodeAt(2) || 0) + ? NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR + : NODE_TYPE_IGNORE; + next = this.allocTypedNode(type, patternBeg, patternBeg+2); + if ( type === NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR ) { + this.addFlags(AST_FLAG_NET_PATTERN_LEFT_HNANCHOR); + } + patternBeg += 2; + pattern = pattern.slice(2); + } else { + const type = this.isTokenCharCode(c2nd) + ? NODE_TYPE_NET_PATTERN_LEFT_ANCHOR + : NODE_TYPE_IGNORE; + next = this.allocTypedNode(type, patternBeg, patternBeg+1); + if ( type === NODE_TYPE_NET_PATTERN_LEFT_ANCHOR ) { + this.addFlags(AST_FLAG_NET_PATTERN_LEFT_ANCHOR); + } + patternBeg += 1; + pattern = pattern.slice(1); + } + prev = this.linkRight(prev, next); + if ( patternBeg === patternEnd ) { + this.addNodeFlags(next, NODE_FLAG_IGNORE); + } + } + + // Right anchor + if ( exCharCodeAt(pattern, -1) === 0x7C /* | */ ) { + const type = exCharCodeAt(pattern, -2) !== 0x2A /* * */ + ? NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR + : NODE_TYPE_IGNORE; + tail = this.allocTypedNode(type, patternEnd-1, patternEnd); + if ( type === NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR ) { + this.addFlags(AST_FLAG_NET_PATTERN_RIGHT_ANCHOR); + } + patternEnd -= 1; + pattern = pattern.slice(0, -1); + if ( patternEnd === patternBeg ) { + this.addNodeFlags(tail, NODE_FLAG_IGNORE); + } + } + + // Ignore pointless leading wildcards + if ( hasWildcard && this.rePointlessLeadingWildcards.test(pattern) ) { + const match = this.rePointlessLeadingWildcards.exec(pattern); + const ignoreLen = match[1].length; + next = this.allocTypedNode( + NODE_TYPE_IGNORE, + patternBeg, + patternBeg + ignoreLen + ); + prev = this.linkRight(prev, next); + patternBeg += ignoreLen; + pattern = pattern.slice(ignoreLen); + } + + // Ignore pointless trailing separators + if ( this.rePointlessTrailingSeparator.test(pattern) ) { + const match = this.rePointlessTrailingSeparator.exec(pattern); + const ignoreLen = match[1].length; + next = this.allocTypedNode( + NODE_TYPE_IGNORE, + patternEnd - ignoreLen, + patternEnd + ); + patternEnd -= ignoreLen; + pattern = pattern.slice(0, -ignoreLen); + if ( tail !== 0 ) { this.linkRight(next, tail); } + tail = next; + } + + // Ignore pointless trailing wildcards. Exception: when removing the + // trailing wildcard make the pattern look like a regex. + if ( hasWildcard && this.rePointlessTrailingWildcards.test(pattern) ) { + const match = this.rePointlessTrailingWildcards.exec(pattern); + const ignoreLen = match[1].length; + const needWildcard = pattern.charCodeAt(0) === 0x2F && + exCharCodeAt(pattern, -ignoreLen-1) === 0x2F; + const goodWildcardBeg = patternEnd - ignoreLen; + const badWildcardBeg = goodWildcardBeg + (needWildcard ? 1 : 0); + if ( badWildcardBeg !== patternEnd ) { + next = this.allocTypedNode( + NODE_TYPE_IGNORE, + badWildcardBeg, + patternEnd + ); + if ( tail !== 0 ) {this.linkRight(next, tail); } + tail = next; + } + if ( goodWildcardBeg !== badWildcardBeg ) { + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN_PART_SPECIAL, + goodWildcardBeg, + badWildcardBeg + ); + if ( tail !== 0 ) { this.linkRight(next, tail); } + tail = next; + } + patternEnd -= ignoreLen; + pattern = pattern.slice(0, -ignoreLen); + } + + const patternHasWhitespace = this.hasWhitespace && + this.reHasWhitespaceChar.test(pattern); + const needNormalization = this.needPatternNormalization(pattern); + const normal = needNormalization + ? this.normalizePattern(pattern) + : pattern; + next = this.allocTypedNode(NODE_TYPE_NET_PATTERN, patternBeg, patternEnd); + if ( patternHasWhitespace || normal === undefined ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_BAD; + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_PATTERN; + this.addNodeFlags(next, NODE_FLAG_ERROR); + } else if ( normal === '*' ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_ANY; + } else if ( this.reHostnameAscii.test(normal) ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_HOSTNAME; + } else if ( this.reHasPatternSpecialChars.test(normal) ) { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_GENERIC; + } else { + this.astTypeFlavor = AST_TYPE_NETWORK_PATTERN_PLAIN; + } + this.addNodeToRegister(NODE_TYPE_NET_PATTERN, next); + if ( needNormalization && normal !== undefined ) { + this.setNodeTransform(next, normal); + } + if ( this.interactive ) { + this.linkDown(next, this.parsePatternParts(next, pattern)); + } + prev = this.linkRight(prev, next); + + if ( tail !== 0 ) { + this.linkRight(prev, tail); + } + return this.throwHeadNode(head); + } + + isAdblockHostnamePattern(pattern) { + if ( this.hasUnicode ) { + return this.reHnAnchoredHostnameUnicode.test(pattern); + } + return this.reHnAnchoredHostnameAscii.test(pattern); + } + + parsePatternParts(parent, pattern) { + if ( pattern.length === 0 ) { return 0; } + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const matches = pattern.matchAll(this.rePatternAllSpecialChars); + const head = this.allocHeadNode(); + let prev = head, next = 0; + let plainPartBeg = 0; + for ( const match of matches ) { + const plainPartEnd = match.index; + if ( plainPartEnd !== plainPartBeg ) { + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN_PART, + parentBeg + plainPartBeg, + parentBeg + plainPartEnd + ); + prev = this.linkRight(prev, next); + } + plainPartBeg = plainPartEnd + match[0].length; + const type = match[0].charCodeAt(0) < 0x80 + ? NODE_TYPE_NET_PATTERN_PART_SPECIAL + : NODE_TYPE_NET_PATTERN_PART_UNICODE; + next = this.allocTypedNode( + type, + parentBeg + plainPartEnd, + parentBeg + plainPartBeg + ); + prev = this.linkRight(prev, next); + } + if ( plainPartBeg !== pattern.length ) { + next = this.allocTypedNode( + NODE_TYPE_NET_PATTERN_PART, + parentBeg + plainPartBeg, + parentBeg + pattern.length + ); + this.linkRight(prev, next); + } + return this.throwHeadNode(head); + } + + // 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. + // Prepend with '*' character to prevent the browser API from refusing to + // punycode -- this occurs when the extracted label starts with a dash. + needPatternNormalization(pattern) { + return pattern.length === 0 || this.hasUppercase || this.hasUnicode; + } + + normalizePattern(pattern) { + if ( pattern.length === 0 ) { return '*'; } + if ( this.reHasInvalidChar.test(pattern) ) { return; } + let normal = pattern.toLowerCase(); + if ( this.hasUnicode === false ) { return normal; } + // Punycode hostname part of the pattern. + if ( this.reHostnamePatternPart.test(normal) ) { + const match = this.reHostnamePatternPart.exec(normal); + const hn = match[0].replace(this.reHostnameLabel, s => { + if ( this.reHasUnicodeChar.test(s) === false ) { return s; } + if ( s.charCodeAt(0) === 0x2D /* - */ ) { s = '*' + s; } + return this.normalizeHostnameValue(s, DOMAIN_CAN_USE_WILDCARD) || s; + }); + normal = hn + normal.slice(match.index + match[0].length); + } + if ( this.reHasUnicodeChar.test(normal) === false ) { return normal; } + // Percent-encode remaining Unicode characters. + try { + normal = normal.replace(this.reUnicodeChars, s => + encodeURIComponent(s).toLowerCase() + ); + } catch { + return; + } + return normal; + } + + getNetPattern() { + const node = this.nodeTypeLookupTable[NODE_TYPE_NET_PATTERN]; + return this.getNodeTransform(node); + } + + isAnyPattern() { + return this.astTypeFlavor === AST_TYPE_NETWORK_PATTERN_ANY; + } + + isHostnamePattern() { + return this.astTypeFlavor === AST_TYPE_NETWORK_PATTERN_HOSTNAME; + } + + isRegexPattern() { + return this.astTypeFlavor === AST_TYPE_NETWORK_PATTERN_REGEX; + } + + isPlainPattern() { + return this.astTypeFlavor === AST_TYPE_NETWORK_PATTERN_PLAIN; + } + + isGenericPattern() { + return this.astTypeFlavor === AST_TYPE_NETWORK_PATTERN_GENERIC; + } + + isBadPattern() { + return this.astTypeFlavor === AST_TYPE_NETWORK_PATTERN_BAD; + } + + parseNetOptions(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + if ( parentEnd === parentBeg ) { return 0; } + const s = this.getNodeString(parent); + const optionsEnd = s.length; + const parseDetails = { node: 0, len: 0 }; + const head = this.allocHeadNode(); + let prev = head, next = 0; + let optionBeg = 0, optionEnd = 0; + while ( optionBeg !== optionsEnd ) { + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_RAW, + parentBeg + optionBeg, + parentBeg + optionsEnd // open ended + ); + this.parseNetOption(next, parseDetails); + // set next's end to down's end + optionEnd += parseDetails.len; + this.nodes[next+NODE_END_INDEX] = parentBeg + optionEnd; + this.linkDown(next, parseDetails.node); + prev = this.linkRight(prev, next); + if ( optionEnd === optionsEnd ) { break; } + optionBeg = optionEnd + 1; + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_SEPARATOR, + parentBeg + optionEnd, + parentBeg + optionBeg + ); + if ( parseDetails.len === 0 || optionBeg === optionsEnd ) { + this.addNodeFlags(next, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + prev = this.linkRight(prev, next); + optionEnd = optionBeg; + } + this.linkRight(prev, + this.allocSentinelNode(NODE_TYPE_NET_OPTION_SENTINEL, parentEnd) + ); + return this.throwHeadNode(head); + } + + parseNetOption(parent, parseDetails) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const s = this.getNodeString(parent); + const match = this.reNetOption.exec(s) || []; + if ( match.length === 0 ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_OPTION_UNKNOWN; + parseDetails.node = 0; + parseDetails.len = s.length; + return; + } + const head = this.allocHeadNode(); + let prev = head, next = 0; + const matchEnd = match && match[0].length || 0; + const negated = match[1] === '~'; + if ( negated ) { + this.addNodeFlags(parent, NODE_FLAG_IS_NEGATED); + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_NAME_NOT, + parentBeg, + parentBeg+1 + ); + prev = this.linkRight(prev, next); + } + const nameBeg = negated ? 1 : 0; + const assigned = match[3] === '='; + const nameEnd = matchEnd - (assigned ? 1 : 0); + const name = match[2] || ''; + let nodeOptionType = nodeTypeFromOptionName.get(name); + if ( nodeOptionType === undefined ) { + nodeOptionType = this.reNoopOption.test(name) + ? NODE_TYPE_NET_OPTION_NAME_NOOP + : NODE_TYPE_NET_OPTION_NAME_UNKNOWN; + } + next = this.allocTypedNode( + nodeOptionType, + parentBeg + nameBeg, + parentBeg + nameEnd + ); + if ( + nodeOptionType !== NODE_TYPE_NET_OPTION_NAME_NOOP && + this.getBranchFromType(nodeOptionType) !== 0 + ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_OPTION_DUPLICATE; + } else { + this.addNodeToRegister(nodeOptionType, parent); + } + prev = this.linkRight(prev, next); + if ( assigned === false ) { + parseDetails.node = this.throwHeadNode(head); + parseDetails.len = matchEnd; + return; + } + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_ASSIGN, + parentBeg + matchEnd - 1, + parentBeg + matchEnd + ); + prev = this.linkRight(prev, next); + this.addNodeFlags(parent, NODE_FLAG_OPTION_HAS_VALUE); + const details = this.netOptionValueParser.nextArg(s, matchEnd); + if ( details.quoteBeg !== details.argBeg ) { + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_QUOTE, + parentBeg + details.quoteBeg, + parentBeg + details.argBeg + ); + prev = this.linkRight(prev, next); + } else { + const argEnd = this.endOfNetOption(s, matchEnd); + if ( argEnd !== details.argEnd ) { + details.argEnd = details.quoteEnd = argEnd; + } + } + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_VALUE, + parentBeg + details.argBeg, + parentBeg + details.argEnd + ); + if ( details.argBeg === details.argEnd ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_OPTION_BADVALUE; + } else if ( details.transform ) { + const arg = s.slice(details.argBeg, details.argEnd); + this.setNodeTransform(next, this.netOptionValueParser.normalizeArg(arg)); + } + switch ( nodeOptionType ) { + case NODE_TYPE_NET_OPTION_NAME_DENYALLOW: + this.linkDown(next, this.parseDomainList(next, '|'), DOMAIN_FROM_DENYALLOW_LIST); + break; + case NODE_TYPE_NET_OPTION_NAME_FROM: + case NODE_TYPE_NET_OPTION_NAME_TO: + this.linkDown(next, this.parseDomainList(next, '|', DOMAIN_FROM_FROMTO_LIST)); + break; + default: + break; + } + prev = this.linkRight(prev, next); + if ( details.quoteEnd !== details.argEnd ) { + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_QUOTE, + parentBeg + details.argEnd, + parentBeg + details.quoteEnd + ); + this.linkRight(prev, next); + } + parseDetails.node = this.throwHeadNode(head); + parseDetails.len = details.quoteEnd; + } + + endOfNetOption(s, beg) { + const match = this.reNetOptionComma.exec(s.slice(beg)); + return match !== null ? beg + match.index : s.length; + } + + getNetOptionValue(type) { + if ( this.nodeTypeRegister.includes(type) === false ) { return ''; } + const optionNode = this.nodeTypeLookupTable[type]; + if ( optionNode === 0 ) { return ''; } + const valueNode = this.findDescendantByType(optionNode, NODE_TYPE_NET_OPTION_VALUE); + if ( valueNode === 0 ) { return ''; } + return this.getNodeTransform(valueNode); + } + + parseDomainList(parent, separator, mode = 0) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const containerNode = this.allocTypedNode( + NODE_TYPE_OPTION_VALUE_DOMAIN_LIST, + parentBeg, + parentEnd + ); + if ( parentEnd === parentBeg ) { return containerNode; } + const separatorCode = separator.charCodeAt(0); + const parseDetails = { separator, mode, node: 0, len: 0 }; + const listNode = this.allocHeadNode(); + let prev = listNode; + let domainNode = 0; + let separatorNode = 0; + const s = this.getNodeString(parent); + const listEnd = s.length; + let beg = 0, end = 0; + while ( beg < listEnd ) { + const next = this.allocTypedNode( + NODE_TYPE_OPTION_VALUE_DOMAIN_RAW, + parentBeg + beg, + parentBeg + listEnd // open ended + ); + this.parseDomain(next, parseDetails); + end = beg + parseDetails.len; + const badSeparator = end < listEnd && s.charCodeAt(end) !== separatorCode; + if ( badSeparator ) { + end = s.indexOf(separator, end); + if ( end === -1 ) { end = listEnd; } + } + this.nodes[next+NODE_END_INDEX] = parentBeg + end; + if ( end !== beg ) { + domainNode = next; + this.linkDown(domainNode, parseDetails.node); + prev = this.linkRight(prev, domainNode); + if ( badSeparator ) { + this.addNodeFlags(domainNode, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + } else { + domainNode = 0; + if ( separatorNode !== 0 ) { + this.addNodeFlags(separatorNode, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + } + if ( s.charCodeAt(end) === separatorCode ) { + beg = end; + end += 1; + separatorNode = this.allocTypedNode( + NODE_TYPE_OPTION_VALUE_SEPARATOR, + parentBeg + beg, + parentBeg + end + ); + prev = this.linkRight(prev, separatorNode); + if ( domainNode === 0 ) { + this.addNodeFlags(separatorNode, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + } else { + separatorNode = 0; + } + beg = end; + } + // Dangling separator node + if ( separatorNode !== 0 ) { + this.addNodeFlags(separatorNode, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + this.linkDown(containerNode, this.throwHeadNode(listNode)); + return containerNode; + } + + parseDomain(parent, parseDetails) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const not = this.charCodeAt(parentBeg) === 0x7E /* ~ */; + let head = 0, next = 0; + let beg = parentBeg; + if ( not ) { + this.addNodeFlags(parent, NODE_FLAG_IS_NEGATED); + head = this.allocTypedNode(NODE_TYPE_OPTION_VALUE_NOT, beg, beg + 1); + if ( (parseDetails.mode & DOMAIN_CAN_BE_NEGATED) === 0 ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + } + beg += 1; + } + const c0 = this.charCodeAt(beg); + let end = beg; + let isRegex = false; + if ( c0 === 0x2F /* / */ ) { + this.domainRegexValueParser.nextArg(this.raw, beg+1); + end = this.domainRegexValueParser.separatorEnd; + isRegex = true; + } else if ( c0 === 0x5B /* [ */ && this.startsWith('[$domain=/', beg) ) { + end = this.indexOf('/]', beg + 10, parentEnd); + if ( end !== -1 ) { end += 2; } + isRegex = true; + } else { + end = this.indexOf(parseDetails.separator, end, parentEnd); + } + if ( end === -1 ) { end = parentEnd; } + if ( beg !== end ) { + next = this.allocTypedNode(NODE_TYPE_OPTION_VALUE_DOMAIN, beg, end); + const hn = this.normalizeDomainValue(next, isRegex, parseDetails.mode); + if ( hn !== undefined ) { + if ( hn !== '' ) { + this.setNodeTransform(next, hn); + } else { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_DOMAIN_NAME; + } + } + if ( head === 0 ) { + head = next; + } else { + this.linkRight(head, next); + } + } else { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + parseDetails.node = head; + parseDetails.len = end - parentBeg; + } + + normalizeDomainValue(node, isRegex, modeBits) { + const raw = this.getNodeString(node); + if ( isRegex ) { + if ( (modeBits & DOMAIN_CAN_BE_REGEX) === 0 ) { return ''; } + return this.normalizeDomainRegexValue(raw); + } + // Common: Assume plain hostname + const r1 = this.normalizeHostnameValue(raw, modeBits); + if ( r1 === undefined ) { return; } + if ( r1 !== '' ) { return r1; } + // Rare: Maybe advanced syntax is used + const match = this.reAdvancedDomainSyntax.exec(raw); + if ( match === null ) { return '' }; + const isAncestor = match[2] !== undefined; + if ( isAncestor && (modeBits & DOMAIN_CAN_BE_ANCESTOR) === 0 ) { return ''; } + const hasPath = match[3] !== undefined; + if ( hasPath && (modeBits & DOMAIN_CAN_HAVE_PATH) === 0 ) { return ''; } + if ( isAncestor && hasPath ) { return ''; } + const r2 = this.normalizeHostnameValue(match[1], modeBits); + if ( r2 === undefined ) { return; } + if ( r2 === '' ) { return ''; } + return `${r2}${match[2] ?? ''}${match[3] ?? ''}`; + } + + normalizeDomainRegexValue(before) { + const regex = before.startsWith('[$domain=/') + ? `${before.slice(9, -1)}` + : before; + const source = this.normalizeRegexPattern(regex); + if ( source === '' ) { return ''; } + const after = `/${source}/`; + if ( after === before ) { return; } + return after; + } + + parseExt(parent, anchorBeg, anchorLen) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const head = this.allocHeadNode(); + let prev = head, next = 0; + this.astType = AST_TYPE_EXTENDED; + this.addFlags(this.extFlagsFromAnchor(anchorBeg)); + if ( anchorBeg > parentBeg ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_OPTIONS, + parentBeg, + anchorBeg + ); + this.addFlags(AST_FLAG_HAS_OPTIONS); + this.addNodeToRegister(NODE_TYPE_EXT_OPTIONS, next); + const down = this.parseDomainList(next, ',', DOMAIN_FROM_EXT_LIST); + this.linkDown(next, down); + prev = this.linkRight(prev, next); + } + next = this.allocTypedNode( + NODE_TYPE_EXT_OPTIONS_ANCHOR, + anchorBeg, + anchorBeg + anchorLen + ); + this.addNodeToRegister(NODE_TYPE_EXT_OPTIONS_ANCHOR, next); + prev = this.linkRight(prev, next); + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_RAW, + anchorBeg + anchorLen, + parentEnd + ); + this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_RAW, 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.validateExt(); + return this.throwHeadNode(head); + } + + extFlagsFromAnchor(anchorBeg) { + let c = this.charCodeAt(anchorBeg+1) ; + if ( c === 0x23 /* # */ ) { return 0; } + if ( c === 0x25 /* % */ ) { return AST_FLAG_EXT_SCRIPTLET_ADG; } + if ( c === 0x3F /* ? */ ) { return AST_FLAG_EXT_STRONG; } + if ( c === 0x24 /* $ */ ) { + c = this.charCodeAt(anchorBeg+2); + if ( c === 0x23 /* # */ ) { return AST_FLAG_EXT_STYLE; } + if ( c === 0x3F /* ? */ ) { + return AST_FLAG_EXT_STYLE | AST_FLAG_EXT_STRONG; + } + } + if ( c === 0x40 /* @ */ ) { + return AST_FLAG_IS_EXCEPTION | this.extFlagsFromAnchor(anchorBeg+1); + } + return AST_FLAG_UNSUPPORTED | AST_FLAG_HAS_ERROR; + } + + validateExt() { + const isException = this.isException(); + let realBad = false; + for ( let i = 0, n = this.nodeTypeRegisterPtr; i < n; i++ ) { + const type = this.nodeTypeRegister[i]; + const targetNode = this.nodeTypeLookupTable[type]; + if ( targetNode === 0 ) { continue; } + const flags = this.getNodeFlags(targetNode); + if ( (flags & NODE_FLAG_ERROR) !== 0 ) { continue; } + realBad = false; + switch ( type ) { + case NODE_TYPE_EXT_PATTERN_RESPONSEHEADER: { + const pattern = this.getNodeString(targetNode); + realBad = + pattern !== '' && removableHTTPHeaders.has(pattern) === false || + pattern === '' && isException === false; + break; + } + case NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN: { + if ( this.interactive !== true ) { break; } + if ( isException ) { break; } + const { trustedSource, trustedScriptletTokens } = this.options; + if ( trustedScriptletTokens instanceof Set === false ) { break; } + const token = this.getNodeString(targetNode); + if ( trustedScriptletTokens.has(token) && trustedSource !== true ) { + this.astError = AST_ERROR_UNTRUSTED_SOURCE; + realBad = true; + } + break; + } + default: + break; + } + if ( realBad ) { + this.addNodeFlags(targetNode, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + } + } + + parseExtPattern(parent) { + const c = this.charCodeAt(this.nodes[parent+NODE_BEG_INDEX]); + // ##+js(...) + if ( c === 0x2B /* + */ ) { + const s = this.getNodeString(parent); + if ( /^\+js\(.*\)$/.exec(s) !== null ) { + this.astTypeFlavor = AST_TYPE_EXTENDED_SCRIPTLET; + 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(...) + if ( c === 0x5E /* ^ */ ) { + const s = this.getNodeString(parent); + if ( this.reResponseheaderPattern.test(s) ) { + this.astTypeFlavor = AST_TYPE_EXTENDED_RESPONSEHEADER; + return this.parseExtPatternResponseheader(parent); + } + this.astTypeFlavor = AST_TYPE_EXTENDED_HTML; + return this.parseExtPatternHtml(parent); + } + // ##... + this.astTypeFlavor = AST_TYPE_EXTENDED_COSMETIC; + return this.parseExtPatternCosmetic(parent); + } + + parseExtPatternScriptlet(parent) { + const beg = this.nodes[parent+NODE_BEG_INDEX]; + const end = this.nodes[parent+NODE_END_INDEX]; + const s = this.getNodeString(parent); + const rawArg0 = beg + (s.startsWith('+js') ? 4 : 12); + const rawArg1 = end - 1; + const head = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, beg, rawArg0); + let prev = head, next = 0; + next = this.allocTypedNode(NODE_TYPE_EXT_PATTERN_SCRIPTLET, rawArg0, rawArg1); + this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_SCRIPTLET, next); + this.linkDown(next, this.parseExtPatternScriptletArgs(next)); + prev = this.linkRight(prev, next); + next = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, rawArg1, end); + this.linkRight(prev, next); + return head; + } + + parseExtPatternScriptletArgs(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + if ( parentEnd === parentBeg ) { return 0; } + const head = this.allocHeadNode(); + let prev = head, next = 0; + const s = this.getNodeString(parent); + const argsEnd = s.length; + // token + this.scriptletArgListParser.mustQuote = + this.getFlags(AST_FLAG_EXT_SCRIPTLET_ADG) !== 0; + const details = this.scriptletArgListParser.nextArg(s, 0); + if ( details.argBeg > 0 ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_DECORATION, + parentBeg, + parentBeg + details.argBeg + ); + prev = this.linkRight(prev, next); + } + const token = s.slice(details.argBeg, details.argEnd); + const tokenEnd = details.argEnd - (token.endsWith('.js') ? 3 : 0); + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, + parentBeg + details.argBeg, + parentBeg + tokenEnd + ); + this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, next); + if ( details.failed ) { + this.addNodeFlags(next, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + prev = this.linkRight(prev, next); + if ( tokenEnd < details.argEnd ) { + next = this.allocTypedNode( + NODE_TYPE_IGNORE, + parentBeg + tokenEnd, + 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); + } + // all args + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, + parentBeg + details.separatorEnd, + parentBeg + argsEnd + ); + this.linkDown(next, this.parseExtPatternScriptletArglist(next)); + this.linkRight(prev, next); + return this.throwHeadNode(head); + } + + parseExtPatternScriptletArglist(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + if ( parentEnd === parentBeg ) { return 0; } + const s = this.getNodeString(parent); + const argsEnd = s.length; + const head = this.allocHeadNode(); + let prev = head, next = 0; + let decorationBeg = 0; + let i = 0; + for (;;) { + const details = this.scriptletArgListParser.nextArg(s, i); + if ( decorationBeg < details.argBeg ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_DECORATION, + parentBeg + decorationBeg, + parentBeg + details.argBeg + ); + 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 ) { + const arg = s.slice(details.argBeg, details.argEnd); + this.setNodeTransform(next, + this.scriptletArgListParser.normalizeArg(arg) + ); + } + 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); + } + + getScriptletArgs() { + 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) { + const beg = this.nodes[parent+NODE_BEG_INDEX]; + const end = this.nodes[parent+NODE_END_INDEX]; + const s = this.getNodeString(parent); + const rawArg0 = beg + 16; + const rawArg1 = end - 1; + const head = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, beg, rawArg0); + let prev = head, next = 0; + const trimmedArg0 = rawArg0 + this.leftWhitespaceCount(s); + 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_RESPONSEHEADER, rawArg0, rawArg1); + this.addNodeToRegister(NODE_TYPE_EXT_PATTERN_RESPONSEHEADER, next); + if ( rawArg1 === rawArg0 && this.isException() === false ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + 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); + this.linkRight(prev, next); + return head; + } + + getResponseheaderName() { + if ( this.isResponseheaderFilter() === false ) { return ''; } + const root = this.getBranchFromType(NODE_TYPE_EXT_PATTERN_RESPONSEHEADER); + return this.getNodeString(root); + } + + parseExtPatternHtml(parent) { + const beg = this.nodes[parent+NODE_BEG_INDEX]; + const end = this.nodes[parent+NODE_END_INDEX]; + const head = this.allocTypedNode(NODE_TYPE_EXT_DECORATION, beg, beg + 1); + let prev = head, next = 0; + next = this.allocTypedNode(NODE_TYPE_EXT_PATTERN_HTML, beg + 1, end); + this.linkRight(prev, next); + if ( (this.hasOptions() || this.isException()) === false ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + return head; + } + this.result.exception = this.isException(); + this.result.raw = this.getNodeString(next); + this.result.compiled = undefined; + const success = this.selectorCompiler.compile( + this.result.raw, + this.result, { + asProcedural: this.getFlags(AST_FLAG_EXT_STRONG) !== 0 + } + ); + if ( success !== true ) { + this.addNodeFlags(next, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + return head; + } + + parseExtPatternCosmetic(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + const head = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_COSMETIC, + parentBeg, + parentEnd + ); + this.result.exception = this.isException(); + this.result.raw = this.getNodeString(head); + this.result.compiled = undefined; + const success = this.selectorCompiler.compile( + this.result.raw, + this.result, { + asProcedural: this.getFlags(AST_FLAG_EXT_STRONG) !== 0, + adgStyleSyntax: this.getFlags(AST_FLAG_EXT_STYLE) !== 0, + } + ); + if ( success !== true ) { + this.addNodeFlags(head, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + return head; + } + + hasError() { + return (this.astFlags & AST_FLAG_HAS_ERROR) !== 0; + } + + isUnsupported() { + return (this.astFlags & AST_FLAG_UNSUPPORTED) !== 0; + } + + hasOptions() { + return (this.astFlags & AST_FLAG_HAS_OPTIONS) !== 0; + } + + isNegatedOption(type) { + const node = this.nodeTypeLookupTable[type]; + const flags = this.nodes[node+NODE_FLAGS_INDEX]; + return (flags & NODE_FLAG_IS_NEGATED) !== 0; + } + + isException() { + return (this.astFlags & AST_FLAG_IS_EXCEPTION) !== 0; + } + + isLeftHnAnchored() { + return (this.astFlags & AST_FLAG_NET_PATTERN_LEFT_HNANCHOR) !== 0; + } + + isLeftAnchored() { + return (this.astFlags & AST_FLAG_NET_PATTERN_LEFT_ANCHOR) !== 0; + } + + isRightAnchored() { + return (this.astFlags & AST_FLAG_NET_PATTERN_RIGHT_ANCHOR) !== 0; + } + + linkRight(prev, next) { + return (this.nodes[prev+NODE_RIGHT_INDEX] = next); + } + + linkDown(node, down) { + return (this.nodes[node+NODE_DOWN_INDEX] = down); + } + + makeChain(nodes) { + for ( let i = 1; i < nodes.length; i++ ) { + this.nodes[nodes[i-1]+NODE_RIGHT_INDEX] = nodes[i]; + } + return nodes[0]; + } + + allocHeadNode() { + const node = this.nodePoolPtr; + this.nodePoolPtr += NOOP_NODE_SIZE; + if ( this.nodePoolPtr > this.nodePoolEnd ) { + this.growNodePool(this.nodePoolPtr); + } + this.nodes[node+NODE_RIGHT_INDEX] = 0; + return node; + } + + throwHeadNode(head) { + return this.nodes[head+NODE_RIGHT_INDEX]; + } + + allocTypedNode(type, beg, end) { + const node = this.nodePoolPtr; + this.nodePoolPtr += FULL_NODE_SIZE; + if ( this.nodePoolPtr > this.nodePoolEnd ) { + this.growNodePool(this.nodePoolPtr); + } + this.nodes[node+NODE_RIGHT_INDEX] = 0; + this.nodes[node+NODE_TYPE_INDEX] = type; + this.nodes[node+NODE_DOWN_INDEX] = 0; + this.nodes[node+NODE_BEG_INDEX] = beg; + this.nodes[node+NODE_END_INDEX] = end; + this.nodes[node+NODE_TRANSFORM_INDEX] = 0; + this.nodes[node+NODE_FLAGS_INDEX] = 0; + return node; + } + + allocSentinelNode(type, beg) { + return this.allocTypedNode(type, beg, beg); + } + + growNodePool(min) { + const oldSize = this.nodes.length; + const newSize = (min + 16383) & ~16383; + if ( newSize === oldSize ) { return; } + const newArray = new Uint32Array(newSize); + newArray.set(this.nodes); + this.nodes = newArray; + this.nodePoolEnd = newSize; + } + + getNodeTypes() { + return this.nodeTypeRegister.slice(0, this.nodeTypeRegisterPtr); + } + + getNodeType(node) { + return node !== 0 ? this.nodes[node+NODE_TYPE_INDEX] : 0; + } + + getNodeFlags(node, flags = 0xFFFFFFFF) { + return this.nodes[node+NODE_FLAGS_INDEX] & flags; + } + + setNodeFlags(node, flags) { + this.nodes[node+NODE_FLAGS_INDEX] = flags; + } + + addNodeFlags(node, flags) { + if ( node === 0 ) { return; } + this.nodes[node+NODE_FLAGS_INDEX] |= flags; + } + + removeNodeFlags(node, flags) { + this.nodes[node+NODE_FLAGS_INDEX] &= ~flags; + } + + addNodeToRegister(type, node) { + this.nodeTypeRegister[this.nodeTypeRegisterPtr++] = type; + this.nodeTypeLookupTable[type] = node; + } + + getBranchFromType(type) { + const ptr = this.nodeTypeRegisterPtr; + if ( ptr === 0 ) { return 0; } + return this.nodeTypeRegister.lastIndexOf(type, ptr-1) !== -1 + ? this.nodeTypeLookupTable[type] + : 0; + } + + nodeIsEmptyString(node) { + return this.nodes[node+NODE_END_INDEX] === + this.nodes[node+NODE_BEG_INDEX]; + } + + getNodeString(node) { + const beg = this.nodes[node+NODE_BEG_INDEX]; + const end = this.nodes[node+NODE_END_INDEX]; + if ( end === beg ) { return ''; } + if ( beg === 0 && end === this.rawEnd ) { + return this.raw; + } + return this.raw.slice(beg, end); + } + + getNodeStringBeg(node) { + return this.nodes[node+NODE_BEG_INDEX]; + } + + getNodeStringEnd(node) { + return this.nodes[node+NODE_END_INDEX]; + } + + getNodeStringLen(node) { + if ( node === 0 ) { return ''; } + return this.nodes[node+NODE_END_INDEX] - this.nodes[node+NODE_BEG_INDEX]; + } + + isNodeTransformed(node) { + return this.nodes[node+NODE_TRANSFORM_INDEX] !== 0; + } + + getNodeTransform(node) { + if ( node === 0 ) { return ''; } + const slot = this.nodes[node+NODE_TRANSFORM_INDEX]; + return slot !== 0 ? this.astTransforms[slot] : this.getNodeString(node); + } + + setNodeTransform(node, value) { + const slot = this.astTransformPtr++; + this.astTransforms[slot] = value; + this.nodes[node+NODE_TRANSFORM_INDEX] = slot; + } + + getTypeString(type) { + const node = this.getBranchFromType(type); + if ( node === 0 ) { return; } + return this.getNodeString(node); + } + + leftWhitespaceCount(s) { + const match = this.reWhitespaceStart.exec(s); + return match === null ? 0 : match[0].length; + } + + rightWhitespaceCount(s) { + const match = this.reWhitespaceEnd.exec(s); + return match === null ? 0 : match[1].length; + } + + nextCommaInCommaSeparatedListString(s, start) { + const n = s.length; + if ( n === 0 ) { return -1; } + const ilastchar = n - 1; + let i = start; + while ( i < n ) { + const c = s.charCodeAt(i); + if ( c === 0x2C /* ',' */ ) { return i + 1; } + if ( c === 0x5C /* '\\' */ ) { + if ( i < ilastchar ) { i += 1; } + } + } + return -1; + } + + endOfLiteralRegex(s, start) { + const n = s.length; + if ( n === 0 ) { return -1; } + const ilastchar = n - 1; + let i = start + 1; + while ( i < n ) { + const c = s.charCodeAt(i); + if ( c === 0x2F /* '/' */ ) { return i + 1; } + if ( c === 0x5C /* '\\' */ ) { + if ( i < ilastchar ) { i += 1; } + } + i += 1; + } + return -1; + } + + charCodeAt(pos) { + return pos < this.rawEnd ? this.raw.charCodeAt(pos) : -1; + } + + indexOf(needle, beg, end = 0) { + const haystack = end === 0 ? this.raw : this.raw.slice(0, end); + return haystack.indexOf(needle, beg); + } + + startsWith(s, pos) { + return pos < this.rawEnd && this.raw.startsWith(s, pos); + } + + isTokenCharCode(c) { + return c === 0x25 || + c >= 0x30 && c <= 0x39 || + c >= 0x41 && c <= 0x5A || + c >= 0x61 && c <= 0x7A; + } + + // Ultimately, let the browser API do the hostname normalization, after + // making some other trivial checks. + // + // returns: + // undefined: no normalization needed, use original hostname + // empty string: hostname is invalid + // non-empty string: normalized hostname + normalizeHostnameValue(s, modeBits = 0) { + if ( this.reHostnameAscii.test(s) ) { return; } + if ( this.reBadHostnameChars.test(s) ) { return ''; } + let hn = s; + const hasWildcard = hn.includes('*'); + if ( hasWildcard ) { + if ( modeBits === 0 ) { return ''; } + if ( hn.length === 1 ) { + if ( (modeBits & DOMAIN_CAN_USE_SINGLE_WILDCARD) === 0 ) { return ''; } + return; + } + if ( (modeBits & DOMAIN_CAN_USE_ENTITY) !== 0 ) { + if ( this.rePlainEntity.test(hn) ) { return; } + if ( this.reIsEntity.test(hn) === false ) { return ''; } + } else if ( (modeBits & DOMAIN_CAN_USE_WILDCARD) === 0 ) { + return ''; + } + hn = hn.replace(/\*/g, '__asterisk__'); + } + this.punycoder.hostname = '_'; + try { + this.punycoder.hostname = hn; + hn = this.punycoder.hostname; + } catch { + return ''; + } + if ( hn === '_' || hn === '' ) { return ''; } + if ( hasWildcard ) { + hn = this.punycoder.hostname.replace(/__asterisk__/g, '*'); + } + if ( + (modeBits & DOMAIN_CAN_USE_WILDCARD) === 0 && ( + hn.charCodeAt(0) === 0x2E /* . */ || + exCharCodeAt(hn, -1) === 0x2E /* . */ + ) + ) { + return ''; + } + return hn; + } + + normalizeRegexPattern(s) { + try { + const source = /^\/.+\/$/.test(s) ? s.slice(1,-1) : s; + const regex = new RegExp(source); + return regex.source; + } catch (ex) { + this.normalizeRegexPattern.message = ex.toString(); + } + return ''; + } + + getDomainListIterator(root) { + const iter = this.domainListIteratorJunkyard.length !== 0 + ? this.domainListIteratorJunkyard.pop().reuse(root) + : new DomainListIterator(this, root); + return root !== 0 ? iter : iter.stop(); + } + + getNetFilterFromOptionIterator() { + return this.getDomainListIterator( + this.getBranchFromType(NODE_TYPE_NET_OPTION_NAME_FROM) + ); + } + + getNetFilterToOptionIterator() { + return this.getDomainListIterator( + this.getBranchFromType(NODE_TYPE_NET_OPTION_NAME_TO) + ); + } + + getNetFilterDenyallowOptionIterator() { + return this.getDomainListIterator( + this.getBranchFromType(NODE_TYPE_NET_OPTION_NAME_DENYALLOW) + ); + } + + getExtFilterDomainIterator() { + return this.getDomainListIterator( + this.getBranchFromType(NODE_TYPE_EXT_OPTIONS) + ); + } + + getWalker(from) { + if ( this.walkerJunkyard.length === 0 ) { + return new AstWalker(this, from); + } + const walker = this.walkerJunkyard.pop(); + walker.reset(from); + return walker; + } + + findDescendantByType(from, type) { + const walker = this.getWalker(from); + let node = walker.next(); + while ( node !== 0 ) { + if ( this.getNodeType(node) === type ) { return node; } + node = walker.next(); + } + return 0; + } + + dump() { + if ( this.astType === AST_TYPE_COMMENT ) { return; } + const walker = this.getWalker(); + for ( let node = walker.reset(); node !== 0; node = walker.next() ) { + const type = this.nodes[node+NODE_TYPE_INDEX]; + const value = this.getNodeString(node); + const name = nodeNameFromNodeType.get(type) || `${type}`; + const bits = this.getNodeFlags(node).toString(2).padStart(4, '0'); + const indent = ' '.repeat(walker.depth); + console.log(`${indent}type=${name} "${value}" 0b${bits}`); + if ( this.isNodeTransformed(node) ) { + console.log(`${indent} transform="${this.getNodeTransform(node)}`); + } + } + } +} + +/******************************************************************************/ + +export function parseRedirectValue(arg) { + let token = arg.trim(); + let priority = 0; + const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */; + if ( asDataURI ) { token = token.slice(1); } + const match = /:-?\d+$/.exec(token); + if ( match !== null ) { + priority = parseInt(token.slice(match.index + 1), 10); + token = token.slice(0, match.index); + } + return { token, priority, asDataURI }; +} + +export function parseQueryPruneValue(arg) { + let s = arg.trim(); + if ( s === '' ) { return { all: true }; } + const out = { }; + out.not = s.charCodeAt(0) === 0x7E /* '~' */; + if ( out.not ) { + s = s.slice(1); + } + const match = /^\/(.+)\/(i)?$/.exec(s); + if ( match !== null ) { + try { + out.re = new RegExp(match[1], match[2] || ''); + } + catch { + out.bad = true; + } + return out; + } + // TODO: remove once no longer used in filter lists + if ( s.startsWith('|') ) { + try { + out.re = new RegExp('^' + s.slice(1), 'i'); + } catch { + out.bad = true; + } + return out; + } + // Multiple values not supported (because very inefficient) + if ( s.includes('|') ) { + out.bad = true; + return out; + } + out.name = s; + return out; +} + +export function parseHeaderValue(arg) { + let s = arg.trim(); + const out = { }; + let pos = s.indexOf(':'); + if ( pos === -1 ) { pos = s.length; } + out.name = s.slice(0, pos).toLowerCase(); + out.bad = out.name === ''; + s = s.slice(pos + 1); + out.not = s.charCodeAt(0) === 0x7E /* '~' */; + if ( out.not ) { s = s.slice(1); } + out.value = s; + if ( s === '' ) { return out; } + const match = /^\/(.+)\/(i)?$/.exec(s); + out.isRegex = match !== null; + if ( out.isRegex ) { + out.reStr = match[1]; + out.reFlags = match[2] || ''; + try { new RegExp(out.reStr, out.reFlags); } + catch { out.bad = true; } + return out; + } + out.reFlags = 'i'; + if ( /[*?]/.test(s) === false ) { + out.reStr = escapeForRegex(s); + return out; + } + const reConstruct = /(?<!\\)[*?]/g; + const reParts = []; + let beg = 0; + for (;;) { + const match = reConstruct.exec(s); + if ( match === null ) { break; } + reParts.push( + escapeForRegex(s.slice(beg, match.index)), + match[0] === '*' ? '.*' : '.?', + ); + beg = reConstruct.lastIndex; + } + reParts.push(escapeForRegex(s.slice(beg))); + out.reStr = reParts.join(''); + return out; +} + +// https://adguard.com/kb/general/ad-filtering/create-own-filters/#replace-modifier + +export function parseReplaceByRegexValue(s) { + if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; } + const parser = new ArglistParser('/'); + parser.nextArg(s, 1); + let pattern = s.slice(parser.argBeg, parser.argEnd); + if ( parser.transform ) { + pattern = parser.normalizeArg(pattern); + } + if ( pattern === '' ) { return; } + pattern = parser.normalizeArg(pattern, '$'); + pattern = parser.normalizeArg(pattern, ','); + parser.nextArg(s, parser.separatorEnd); + let replacement = s.slice(parser.argBeg, parser.argEnd); + if ( parser.separatorEnd === parser.separatorBeg ) { return; } + if ( parser.transform ) { + replacement = parser.normalizeArg(replacement); + } + replacement = parser.normalizeArg(replacement, '$'); + replacement = parser.normalizeArg(replacement, ','); + const flags = s.slice(parser.separatorEnd); + try { + return { re: new RegExp(pattern, flags), replacement }; + } catch { + } +} + +export function parseReplaceValue(s) { + if ( s.startsWith('/') ) { + const r = parseReplaceByRegexValue(s); + if ( r ) { r.type = 'text'; } + return r; + } + const pos = s.indexOf(':'); + if ( pos === -1 ) { return; } + const type = s.slice(0, pos); + if ( type === 'json' || type === 'jsonl' ) { + const query = s.slice(pos+1); + const jsonp = JSONPath.create(query); + if ( jsonp.valid === false ) { return; } + return { type, jsonp }; + } +} + +/******************************************************************************/ + +export const netOptionTokenDescriptors = new Map([ + [ '1p', { canNegate: true } ], + /* synonym */ [ 'first-party', { canNegate: true } ], + [ 'strict1p', { } ], + /* synonym */ [ 'strict-first-party', { } ], + [ '3p', { canNegate: true } ], + /* synonym */ [ 'third-party', { canNegate: true } ], + [ 'strict3p', { } ], + /* synonym */ [ 'strict-third-party', { } ], + [ 'all', { } ], + [ 'badfilter', { } ], + [ 'cname', { allowOnly: true } ], + [ 'csp', { mustAssign: true } ], + [ 'css', { canNegate: true } ], + /* synonym */ [ 'stylesheet', { canNegate: true } ], + [ 'denyallow', { mustAssign: true } ], + [ 'doc', { canNegate: true } ], + /* synonym */ [ 'document', { canNegate: true } ], + [ 'ehide', { } ], + /* synonym */ [ 'elemhide', { } ], + [ 'empty', { blockOnly: true } ], + [ 'frame', { canNegate: true } ], + /* synonym */ [ 'subdocument', { canNegate: true } ], + [ 'from', { mustAssign: true } ], + /* synonym */ [ 'domain', { mustAssign: true } ], + [ 'font', { canNegate: true } ], + [ 'genericblock', { } ], + [ 'ghide', { } ], + /* synonym */ [ 'generichide', { } ], + [ 'header', { mustAssign: true } ], + [ 'image', { canNegate: true } ], + [ 'important', { blockOnly: true } ], + [ 'inline-font', { canNegate: true } ], + [ 'inline-script', { canNegate: true } ], + [ 'ipaddress', { mustAssign: true } ], + [ 'match-case', { } ], + [ 'media', { canNegate: true } ], + [ 'method', { mustAssign: true } ], + [ 'mp4', { blockOnly: true } ], + [ '_', { } ], + [ 'object', { canNegate: true } ], + /* synonym */ [ 'object-subrequest', { canNegate: true } ], + [ 'other', { canNegate: true } ], + [ 'permissions', { mustAssign: true } ], + [ 'ping', { canNegate: true } ], + /* synonym */ [ 'beacon', { canNegate: true } ], + [ 'popunder', { } ], + [ 'popup', { canNegate: true } ], + [ 'reason', { mustAssign: true } ], + [ 'redirect', { mustAssign: true } ], + /* synonym */ [ 'rewrite', { mustAssign: true } ], + [ 'redirect-rule', { mustAssign: true } ], + [ 'removeparam', { } ], + /* synonym */ [ 'queryprune', { } ], + [ 'replace', { mustAssign: true } ], + [ 'script', { canNegate: true } ], + [ 'shide', { } ], + /* synonym */ [ 'specifichide', { } ], + [ 'to', { mustAssign: true } ], + [ 'urlskip', { mustAssign: true } ], + [ 'uritransform', { mustAssign: true } ], + [ 'xhr', { canNegate: true } ], + /* synonym */ [ 'xmlhttprequest', { canNegate: true } ], + [ 'webrtc', { } ], + [ 'websocket', { canNegate: true } ], +]); + +/******************************************************************************/ + +// 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. + +class ExtSelectorCompiler { + constructor(instanceOptions) { + this.reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/; + + // Use a regex for most common CSS selectors known to be valid in any + // context. + const cssIdentifier = '[A-Za-z_][\\w-]*'; + const cssClassOrId = `[.#]${cssIdentifier}`; + const cssAttribute = `\\[${cssIdentifier}(?:[*^$]?="[^"\\]\\\\\\x09-\\x0D]+")?\\]`; + const cssSimple = + '(?:' + + `${cssIdentifier}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + + `${cssClassOrId}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + + `${cssAttribute}(?:${cssAttribute})*` + + ')'; + const cssCombinator = '(?: | [+>~] )'; + this.reCommonSelector = new RegExp( + `^${cssSimple}(?:${cssCombinator}${cssSimple})*$` + ); + // Resulting regex literal: + // /^(?:[A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\])*|[.#][A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\])*|\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\](?:\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\])*)(?:(?:\s+|\s*[>+~]\s*)(?:[A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\])*|[.#][A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\])*|\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\](?:\[[A-Za-z_][\w-]*(?:[*^$]?="[^"\]\\]+")?\])*))*$/ + + this.reEatBackslashes = /\\([()])/g; + // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes + this.knownPseudoClasses = new Set([ + 'active', 'any-link', 'autofill', + 'blank', + 'checked', 'current', + 'default', 'defined', 'dir', 'disabled', + 'empty', 'enabled', + 'first', 'first-child', 'first-of-type', 'fullscreen', 'future', 'focus', 'focus-visible', 'focus-within', + 'has', 'host', 'host-context', 'hover', + 'indeterminate', 'in-range', 'invalid', 'is', + 'lang', 'last-child', 'last-of-type', 'left', 'link', 'local-link', + 'modal', + 'not', 'nth-child', 'nth-col', 'nth-last-child', 'nth-last-col', 'nth-last-of-type', 'nth-of-type', + 'only-child', 'only-of-type', 'optional', 'out-of-range', + 'past', 'picture-in-picture', 'placeholder-shown', 'paused', 'playing', + 'read-only', 'read-write', 'required', 'right', 'root', + 'scope', 'state', 'target', 'target-within', + 'user-invalid', 'valid', 'visited', + 'where', + ]); + this.knownPseudoClassesWithArgs = new Set([ + 'dir', + 'has', 'host-context', + 'is', + 'lang', + 'not', 'nth-child', 'nth-col', 'nth-last-child', 'nth-last-col', 'nth-last-of-type', 'nth-of-type', + 'state', + 'where', + ]); + // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements + this.knownPseudoElements = new Set([ + 'after', + 'backdrop', 'before', + 'cue', 'cue-region', + 'first-letter', 'first-line', 'file-selector-button', + 'grammar-error', 'marker', + 'part', 'placeholder', + 'selection', 'slotted', 'spelling-error', + 'target-text', + ]); + this.knownPseudoElementsWithArgs = new Set([ + 'part', + 'slotted', + ]); + // https://github.com/gorhill/uBlock/issues/2793 + this.normalizedOperators = new Map([ + [ '-abp-has', 'has' ], + [ '-abp-contains', 'has-text' ], + [ 'contains', 'has-text' ], + [ 'nth-ancestor', 'upward' ], + [ 'watch-attrs', 'watch-attr' ], + ]); + this.actionOperators = new Set([ + ':remove', + ':style', + ]); + this.proceduralOperatorNames = new Set([ + 'has-text', + 'if', + 'if-not', + 'matches-attr', + 'matches-css', + 'matches-css-after', + 'matches-css-before', + 'matches-media', + 'matches-path', + 'matches-prop', + 'min-text-length', + 'others', + 'shadow', + 'upward', + 'watch-attr', + 'xpath', + ]); + this.maybeProceduralOperatorNames = new Set([ + 'has', + 'not', + ]); + this.proceduralActionNames = new Set([ + 'remove', + 'remove-attr', + 'remove-class', + 'style', + ]); + this.normalizedExtendedSyntaxOperators = new Map([ + [ 'contains', 'has-text' ], + [ 'has', 'has' ], + ]); + this.reIsRelativeSelector = /^\s*[+>~]/; + this.reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/; + this.reExtendedSyntaxReplacer = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/g; + this.abpProceduralOpReplacer = /:-abp-(?:[a-z]+)\(/g; + this.nativeCssHas = instanceOptions.nativeCssHas === true; + // https://www.w3.org/TR/css-syntax-3/#typedef-ident-token + this.reInvalidIdentifier = /^\d/; + this.error = undefined; + } + + // CSSTree library holds onto last string parsed, and this is problematic + // when the string is a slice of a huge parent string (typically a whole + // filter list), it causes the huge parent string to stay in memory. + // Asking CSSTree to parse an empty string resolves this issue. + finish() { + cssTree.parse(''); + } + + compile(raw, out, compileOptions = {}) { + this.asProcedural = compileOptions.asProcedural === true; + + // 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/EasyList style injection: translate to uBO's format. + if ( this.isStyleInjectionFilter(raw) ) { + const translated = this.translateStyleInjectionFilter(raw); + if ( translated === undefined ) { return false; } + raw = translated; + } else if ( compileOptions.adgStyleSyntax === true ) { + return false; + } + + // Normalize AdGuard's attribute-based procedural operators. + // Normalize ABP's procedural operator names + if ( this.asProcedural ) { + if ( this.reExtendedSyntax.test(raw) ) { + raw = raw.replace(this.reExtendedSyntaxReplacer, (a, a1, a2, a3) => { + const op = this.normalizedExtendedSyntaxOperators.get(a1); + if ( op === undefined ) { return a; } + return `:${op}(${a3})`; + }); + } else { + let asProcedural = false; + raw = raw.replace(this.abpProceduralOpReplacer, match => { + if ( match === ':-abp-contains(' ) { return ':has-text('; } + if ( match === ':-abp-has(' ) { return ':has('; } + asProcedural = true; + return match; + }); + this.asProcedural = asProcedural; + } + } + + // Relative selectors not allowed at top level. + if ( this.reIsRelativeSelector.test(raw) ) { return false; } + + if ( this.reCommonSelector.test(raw) ) { + out.compiled = raw; + return true; + } + + this.error = undefined; + out.compiled = this.compileSelector(raw); + if ( out.compiled === undefined ) { + out.error = this.error; + return false; + } + + if ( out.compiled instanceof Object ) { + out.compiled.raw = raw; + out.compiled = JSON.stringify(out.compiled); + } + return true; + } + + compileSelector(raw) { + const parts = this.astFromRaw(raw, 'selectorList'); + if ( parts === undefined ) { return; } + if ( this.astHasType(parts, 'Error') ) { return; } + if ( this.astHasType(parts, 'Selector') === false ) { return; } + if ( this.astIsValidSelectorList(parts) === false ) { return; } + if ( this.astHasType(parts, 'ProceduralSelector') ) { + if ( this.astHasType(parts, 'PseudoElementSelector') ) { return; } + } else if ( this.astHasType(parts, 'ActionSelector') === false ) { + return this.astSerialize(parts); + } + const r = this.astCompile(parts); + if ( this.isCssable(r) ) { + r.cssable = true; + } + return r; + } + + isCssable(r) { + if ( r instanceof Object === false ) { return false; } + if ( Array.isArray(r.action) && r.action[0] !== 'style' ) { return false; } + if ( Array.isArray(r.tasks) === false ) { return true; } + if ( r.tasks[0][0] === 'matches-media' ) { + if ( r.tasks.length === 1 ) { return true; } + if ( r.tasks.length === 2 ) { + if ( r.selector !== '' ) { return false; } + if ( r.tasks[1][0] === 'spath' ) { return true; } + } + } + return false; + } + + astFromRaw(raw, type) { + let ast; + try { + ast = cssTree.parse(raw, { + context: type, + parseValue: false, + }); + } catch(reason) { + const lines = [ reason.message ]; + const extra = reason.sourceFragment().split('\n'); + if ( extra.length !== 0 ) { lines.push(''); } + const match = /^[^|]+\|/.exec(extra[0]); + const beg = match !== null ? match[0].length : 0; + lines.push(...extra.map(a => a.slice(beg))); + this.error = lines.join('\n'); + return; + } + const parts = []; + this.astFlatten(ast, parts); + return parts; + } + + astFlatten(data, out) { + const head = data.children && data.children.head; + let args; + switch ( data.type ) { + case 'AttributeSelector': + case 'ClassSelector': + case 'Combinator': + case 'IdSelector': + case 'MediaFeature': + case 'Nth': + case 'Raw': + case 'TypeSelector': + out.push({ data }); + break; + case 'Declaration': + if ( data.value ) { + this.astFlatten(data.value, args = []); + } + out.push({ data, args }); + args = undefined; + break; + case 'DeclarationList': + case 'Identifier': + case 'MediaQueryList': + case 'Selector': + case 'SelectorList': + args = out; + out.push({ data }); + break; + case 'MediaQuery': + case 'PseudoClassSelector': + case 'PseudoElementSelector': + if ( head ) { args = []; } + out.push({ data, args }); + break; + case 'Value': + args = out; + break; + default: + break; + } + if ( head ) { + if ( args ) { + this.astFlatten(head.data, args); + } + let next = head.next; + while ( next ) { + this.astFlatten(next.data, args); + next = next.next; + } + } + if ( data.type !== 'PseudoClassSelector' ) { return; } + if ( data.name.startsWith('-abp-') && this.asProcedural === false ) { + this.error = `${data.name} requires '#?#' separator syntax`; + return; + } + // Post-analysis, mind: + // - https://w3c.github.io/csswg-drafts/selectors-4/#has-pseudo + // - https://w3c.github.io/csswg-drafts/selectors-4/#negation + data.name = this.normalizedOperators.get(data.name) || data.name; + if ( this.proceduralOperatorNames.has(data.name) ) { + data.type = 'ProceduralSelector'; + } else if ( this.proceduralActionNames.has(data.name) ) { + data.type = 'ActionSelector'; + } else if ( data.name.startsWith('-abp-') ) { + data.type = 'Error'; + this.error = `${data.name} is not supported`; + return; + } + if ( this.maybeProceduralOperatorNames.has(data.name) === false ) { + return; + } + if ( this.astHasType(args, 'ActionSelector') ) { + data.type = 'Error'; + this.error = 'invalid use of action operator'; + return; + } + if ( this.astHasType(args, 'ProceduralSelector') ) { + data.type = 'ProceduralSelector'; + return; + } + switch ( data.name ) { + case 'has': + if ( + this.asProcedural || + this.nativeCssHas !== true || + this.astHasName(args, 'has') + ) { + data.type = 'ProceduralSelector'; + } else if ( this.astHasType(args, 'PseudoElementSelector') ) { + data.type = 'Error'; + } + break; + case 'not': { + if ( this.astHasType(args, 'Combinator', 0) === false ) { break; } + if ( this.astIsValidSelectorList(args) !== true ) { + data.type = 'Error'; + } + break; + } + default: + break; + } + } + + // https://github.com/uBlockOrigin/uBlock-issues/issues/2300 + // Unquoted attribute values are parsed as Identifier instead of String. + // https://github.com/uBlockOrigin/uBlock-issues/issues/3127 + // Escape [\t\n\v\f\r] + astSerializePart(part) { + const out = []; + const { data } = part; + switch ( data.type ) { + case 'AttributeSelector': { + const name = data.name.name; + if ( this.reInvalidIdentifier.test(name) ) { return; } + if ( data.matcher === null ) { + out.push(`[${name}]`); + break; + } + let value = data.value.value; + if ( typeof value !== 'string' ) { + value = data.value.name; + } + if ( /["\\]/.test(value) ) { + value = value.replace(/["\\]/g, '\\$&'); + } + if ( /[\x09-\x0D]/.test(value) ) { + value = value.replace(/[\x09-\x0D]/g, s => + `\\${s.charCodeAt(0).toString(16).toUpperCase()} ` + ); + } + let flags = ''; + if ( typeof data.flags === 'string' ) { + if ( /^(is?|si?)$/.test(data.flags) === false ) { return; } + flags = ` ${data.flags}`; + } + out.push(`[${name}${data.matcher}"${value}"${flags}]`); + break; + } + case 'ClassSelector': + if ( this.reInvalidIdentifier.test(data.name) ) { return; } + out.push(`.${data.name}`); + break; + case 'Combinator': + out.push(data.name); + break; + case 'Identifier': + if ( this.reInvalidIdentifier.test(data.name) ) { return; } + out.push(data.name); + break; + case 'IdSelector': + if ( this.reInvalidIdentifier.test(data.name) ) { return; } + out.push(`#${data.name}`); + break; + case 'Nth': { + if ( data.selector !== null ) { return; } + if ( data.nth.type === 'AnPlusB' ) { + const a = parseInt(data.nth.a, 10) || null; + const b = parseInt(data.nth.b, 10) || null; + if ( a !== null ) { + out.push(`${a}n`); + if ( b === null ) { break; } + if ( b < 0 ) { + out.push(`${b}`); + } else { + out.push(`+${b}`); + } + } else if ( b !== null ) { + out.push(`${b}`); + } + } else if ( data.nth.type === 'Identifier' ) { + out.push(data.nth.name); + } + break; + } + case 'PseudoElementSelector': { + const hasArgs = Array.isArray(part.args); + if ( data.name.charCodeAt(0) !== 0x2D /* '-' */ ) { + if ( this.knownPseudoElements.has(data.name) === false ) { return; } + if ( this.knownPseudoElementsWithArgs.has(data.name) && hasArgs === false ) { return; } + } + out.push(`::${data.name}`); + if ( hasArgs ) { + const arg = this.astSerialize(part.args); + if ( typeof arg !== 'string' ) { return; } + out.push(`(${arg})`); + } + break; + } + case 'PseudoClassSelector': { + const hasArgs = Array.isArray(part.args); + if ( data.name.charCodeAt(0) !== 0x2D /* '-' */ ) { + if ( this.knownPseudoClasses.has(data.name) === false ) { return; } + if ( this.knownPseudoClassesWithArgs.has(data.name) && hasArgs === false ) { return; } + } + out.push(`:${data.name}`); + if ( hasArgs ) { + const arg = this.astSerialize(part.args); + if ( typeof arg !== 'string' ) { return; } + out.push(`(${arg.trim()})`); + } + break; + } + case 'Raw': + out.push(data.value); + break; + case 'TypeSelector': + if ( this.reInvalidIdentifier.test(data.name) ) { return; } + out.push(data.name); + break; + default: + break; + } + return out.join(''); + } + + astSerialize(parts, plainCSS = true) { + const out = []; + for ( const part of parts ) { + const { data } = part; + switch ( data.type ) { + case 'AttributeSelector': + case 'ClassSelector': + case 'Identifier': + case 'IdSelector': + case 'Nth': + case 'PseudoClassSelector': + case 'PseudoElementSelector': { + const s = this.astSerializePart(part); + if ( s === undefined ) { return; } + out.push(s); + break; + } + case 'Combinator': { + const s = this.astSerializePart(part); + if ( s === undefined ) { return; } + if ( out.length !== 0 ) { out.push(' '); } + if ( s !== ' ' ) { out.push(s, ' '); } + break; + } + case 'TypeSelector': { + const s = this.astSerializePart(part); + if ( s === undefined ) { return; } + if ( s === '*' && out.length !== 0 ) { + const before = out[out.length-1]; + if ( before.endsWith(' ') === false ) { return; } + } + out.push(s); + break; + } + case 'Raw': + if ( plainCSS ) { return; } + out.push(this.astSerializePart(part)); + break; + case 'Selector': + if ( out.length !== 0 ) { out.push(', '); } + break; + case 'SelectorList': + break; + default: + return; + } + } + return out.join(''); + } + + astCompile(parts, details = {}) { + if ( Array.isArray(parts) === false ) { return; } + if ( parts.length === 0 ) { return; } + if ( parts[0].data.type !== 'SelectorList' ) { return; } + const out = { selector: '' }; + const prelude = []; + const tasks = []; + let startOfSelector = true; + for ( const part of parts ) { + if ( out.action !== undefined ) { return; } + const { data } = part; + switch ( data.type ) { + case 'ActionSelector': { + if ( details.noaction ) { return; } + if ( prelude.length !== 0 ) { + if ( tasks.length === 0 ) { + out.selector = prelude.join(''); + } else { + tasks.push(this.createSpathTask(prelude.join(''))); + } + prelude.length = 0; + } + const args = this.compileArgumentAst(data.name, part.args); + if ( args === undefined ) { return; } + out.action = [ data.name, args ]; + break; + } + case 'AttributeSelector': + case 'ClassSelector': + case 'IdSelector': + case 'PseudoClassSelector': + case 'PseudoElementSelector': + case 'TypeSelector': { + const s = this.astSerializePart(part); + if ( s === undefined ) { return; } + prelude.push(s); + startOfSelector = false; + break; + } + case 'Combinator': { + const s = this.astSerializePart(part); + if ( s === undefined ) { return; } + if ( startOfSelector === false || prelude.length !== 0 ) { + prelude.push(' '); + } + if ( s !== ' ' ) { prelude.push(s, ' '); } + startOfSelector = false; + break; + } + case 'ProceduralSelector': { + if ( prelude.length !== 0 ) { + let spath = prelude.join(''); + prelude.length = 0; + if ( spath.endsWith(' ') ) { spath += '*'; } + if ( tasks.length === 0 ) { + out.selector = spath; + } else { + tasks.push(this.createSpathTask(spath)); + } + } + const args = this.compileArgumentAst(data.name, part.args); + if ( args === undefined ) { return; } + tasks.push([ data.name, args ]); + startOfSelector = false; + break; + } + case 'Selector': + if ( prelude.length !== 0 ) { + prelude.push(', '); + } + startOfSelector = true; + break; + case 'SelectorList': + startOfSelector = true; + break; + default: + return; + } + } + if ( tasks.length === 0 && out.action === undefined ) { + if ( prelude.length === 0 ) { return; } + return prelude.join('').trim(); + } + if ( prelude.length !== 0 ) { + tasks.push(this.createSpathTask(prelude.join(''))); + } + if ( tasks.length !== 0 ) { + out.tasks = tasks; + } + return out; + } + + astHasType(parts, type, depth = 0x7FFFFFFF) { + if ( Array.isArray(parts) === false ) { return false; } + for ( const part of parts ) { + if ( part.data.type === type ) { return true; } + if ( + Array.isArray(part.args) && + depth !== 0 && + this.astHasType(part.args, type, depth-1) + ) { + return true; + } + } + return false; + } + + astHasName(parts, name) { + if ( Array.isArray(parts) === false ) { return false; } + for ( const part of parts ) { + if ( part.data.name === name ) { return true; } + if ( Array.isArray(part.args) && this.astHasName(part.args, name) ) { + return true; + } + } + return false; + } + + astSelectorsFromSelectorList(args) { + if ( Array.isArray(args) === false ) { return; } + if ( args.length < 3 ) { return; } + if ( args[0].data instanceof Object === false ) { return; } + if ( args[0].data.type !== 'SelectorList' ) { return; } + if ( args[1].data instanceof Object === false ) { return; } + if ( args[1].data.type !== 'Selector' ) { return; } + const out = []; + let beg = 1, end = 0, i = 2; + for (;;) { + if ( i < args.length ) { + const type = args[i].data instanceof Object && args[i].data.type; + if ( type === 'Selector' ) { + end = i; + } + } else { + end = args.length; + } + if ( end !== 0 ) { + const components = args.slice(beg+1, end); + if ( components.length === 0 ) { return; } + out.push(components); + if ( end === args.length ) { break; } + beg = end; end = 0; + } + if ( i === args.length ) { break; } + i += 1; + } + return out; + } + + astIsValidSelector(components) { + const len = components.length; + if ( len === 0 ) { return false; } + if ( components[0].data.type === 'Combinator' ) { return false; } + if ( len === 1 ) { return true; } + if ( components[len-1].data.type === 'Combinator' ) { return false; } + return true; + } + + astIsValidSelectorList(args) { + const selectors = this.astSelectorsFromSelectorList(args); + if ( Array.isArray(selectors) === false || selectors.length === 0 ) { + return false; + } + for ( const selector of selectors ) { + if ( this.astIsValidSelector(selector) !== true ) { return false; } + } + return true; + } + + isStyleInjectionFilter(selector) { + const len = selector.length; + return len !== 0 && selector.charCodeAt(len-1) === 0x7D /* } */; + } + + translateStyleInjectionFilter(raw) { + const matches = /^(.+)\s*\{([^}]+)\}$/.exec(raw); + 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})`; + } + + createSpathTask(selector) { + return [ 'spath', selector ]; + } + + compileArgumentAst(operator, parts) { + switch ( operator ) { + case 'has': { + let r = this.astCompile(parts, { noaction: true }); + if ( typeof r === 'string' ) { + r = { selector: r.replace(/^\s*:scope\s*/, '') }; + } + return r; + } + case 'not': { + return this.astCompile(parts, { noaction: true }); + } + default: + break; + } + if ( Array.isArray(parts) === false || parts.length === 0 ) { return; } + const arg = this.astSerialize(parts, false); + if ( arg === undefined ) { return; } + switch ( operator ) { + case 'has-text': + return this.compileText(arg); + case 'if': + return this.compileSelector(arg); + case 'if-not': + return this.compileSelector(arg); + case 'matches-attr': + case 'matches-prop': + return this.compileMatchAttrArgument(arg); + case 'matches-css': + return this.compileCSSDeclaration(arg); + case 'matches-css-after': + return this.compileCSSDeclaration(`after, ${arg}`); + case 'matches-css-before': + return this.compileCSSDeclaration(`before, ${arg}`); + case 'matches-media': + return this.compileMediaQuery(arg); + case 'matches-path': + return this.compileText(arg); + case 'min-text-length': + return this.compileInteger(arg); + case 'others': + return this.compileNoArgument(arg); + case 'remove': + return this.compileNoArgument(arg); + case 'remove-attr': + return this.compileText(arg); + case 'remove-class': + return this.compileText(arg); + case 'shadow': + return this.compileSelector(arg); + case 'style': + return this.compileStyleProperties(arg); + case 'upward': + return this.compileUpwardArgument(arg); + case 'watch-attr': + return this.compileAttrList(arg); + case 'xpath': + return this.compileXpathExpression(arg); + default: + break; + } + } + + isBadRegex(s) { + try { + void new RegExp(s); + } catch (ex) { + this.isBadRegex.message = ex.toString(); + return true; + } + return false; + } + + unquoteString(s) { + const end = s.length; + if ( end === 0 ) { + return { s: '', end }; + } + if ( /^['"]/.test(s) === false ) { + return { s, i: end }; + } + const quote = s.charCodeAt(0); + const out = []; + let i = 1, c = 0; + for (;;) { + c = s.charCodeAt(i); + if ( c === quote ) { + i += 1; + break; + } + if ( c === 0x5C /* '\\' */ ) { + i += 1; + if ( i === end ) { break; } + c = s.charCodeAt(i); + if ( c !== 0x5C && c !== quote ) { + out.push(0x5C); + } + } + out.push(c); + i += 1; + if ( i === end ) { break; } + } + return { s: String.fromCharCode(...out), i }; + } + + compileMatchAttrArgument(s) { + if ( s === '' ) { return; } + let attr = '', value = ''; + let r = this.unquoteString(s); + if ( r.i === s.length ) { + const pos = r.s.indexOf('='); + if ( pos === -1 ) { + attr = r.s; + } else { + attr = r.s.slice(0, pos); + value = r.s.slice(pos+1); + } + } else { + attr = r.s; + if ( s.charCodeAt(r.i) !== 0x3D ) { return; } + value = s.slice(r.i+1); + } + if ( attr === '' ) { return; } + if ( value.length !== 0 ) { + r = this.unquoteString(value); + if ( r.i !== value.length ) { return; } + value = r.s; + } + return { attr, value }; + } + + // Remove potentially present quotes before processing. + compileText(s) { + if ( s === '' ) { + this.error = 'argument missing'; + return; + } + const r = this.unquoteString(s); + if ( r.i !== s.length ) { return; } + return r.s; + } + + compileCSSDeclaration(s) { + let pseudo; { + const match = /^[a-z-]+,/.exec(s); + if ( match !== null ) { + pseudo = match[0].slice(0, -1); + s = s.slice(match[0].length).trim(); + } + } + 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 = `^${escapeForRegex(value)}$`; + } + return { name, pseudo, value: regexDetails }; + } + + 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; + } + + compileMediaQuery(s) { + const parts = this.astFromRaw(s, 'mediaQueryList'); + if ( parts === undefined ) { return; } + if ( this.astHasType(parts, 'Raw') ) { return; } + if ( this.astHasType(parts, 'MediaQuery') === false ) { return; } + // TODO: normalize by serializing resulting AST + return s; + } + + compileUpwardArgument(s) { + const i = this.compileInteger(s, 1, 256); + if ( i !== undefined ) { return i; } + return this.compilePlainSelector(s); + } + + compilePlainSelector(s) { + const parts = this.astFromRaw(s, 'selectorList' ); + if ( this.astIsValidSelectorList(parts) !== true ) { return; } + if ( this.astHasType(parts, 'ProceduralSelector') ) { return; } + if ( this.astHasType(parts, 'ActionSelector') ) { return; } + if ( this.astHasType(parts, 'Error') ) { return; } + return s; + } + + compileNoArgument(s) { + if ( s === '' ) { return s; } + } + + // https://github.com/uBlockOrigin/uBlock-issues/issues/668 + // https://github.com/uBlockOrigin/uBlock-issues/issues/1693 + // https://github.com/uBlockOrigin/uBlock-issues/issues/1811 + // Forbid instances of: + // - `image-set(` + // - `url(` + // - any instance of `//` + // - backslashes `\` + // - opening comment `/*` + compileStyleProperties(s) { + if ( /image-set\(|url\(|\/\s*\/|\\|\/\*/i.test(s) ) { return; } + const parts = this.astFromRaw(s, 'declarationList'); + if ( parts === undefined ) { return; } + if ( this.astHasType(parts, 'Declaration') === false ) { return; } + return s; + } + + compileAttrList(s) { + if ( s === '' ) { return s; } + const attrs = s.split(/\s*,\s*/); + const out = []; + for ( const attr of attrs ) { + if ( attr !== '' ) { + out.push(attr); + } + } + return out; + } + + compileXpathExpression(s) { + const r = this.unquoteString(s); + if ( r.i !== s.length ) { return; } + const doc = globalThis.document; + if ( doc instanceof Object === false ) { return r.s; } + try { + const expr = doc.createExpression(r.s, null); + expr.evaluate(doc, XPathResult.ANY_UNORDERED_NODE_TYPE); + } catch { + return; + } + return r.s; + } +} + +// bit 0: can be used as auto-completion hint +// bit 1: can not be used in HTML filtering +// +export const proceduralOperatorTokens = new Map([ + [ '-abp-contains', 0b00 ], + [ '-abp-has', 0b00, ], + [ 'contains', 0b00, ], + [ 'has', 0b01 ], + [ 'has-text', 0b01 ], + [ 'if', 0b00 ], + [ 'if-not', 0b00 ], + [ 'matches-attr', 0b11 ], + [ 'matches-css', 0b11 ], + [ 'matches-media', 0b11 ], + [ 'matches-path', 0b11 ], + [ 'matches-prop', 0b11 ], + [ 'min-text-length', 0b01 ], + [ 'not', 0b01 ], + [ 'nth-ancestor', 0b00 ], + [ 'others', 0b11 ], + [ 'remove', 0b11 ], + [ 'remove-attr', 0b11 ], + [ 'remove-class', 0b11 ], + [ 'style', 0b11 ], + [ 'upward', 0b01 ], + [ 'watch-attr', 0b11 ], + [ 'watch-attrs', 0b00 ], + [ 'xpath', 0b01 ], +]); + +/******************************************************************************/ + +export const utils = (( ) => { + + const preparserTokens = new Map([ + [ 'ext_ublock', 'ublock' ], + [ 'ext_ubol', 'ubol' ], + [ 'ext_devbuild', 'devbuild' ], + [ 'env_chromium', 'chromium' ], + [ 'env_edge', 'edge' ], + [ 'env_firefox', 'firefox' ], + [ 'env_legacy', 'legacy' ], + [ 'env_mobile', 'mobile' ], + [ 'env_mv3', 'mv3' ], + [ 'env_safari', 'safari' ], + [ 'cap_html_filtering', 'html_filtering' ], + [ 'cap_user_stylesheet', 'user_stylesheet' ], + [ 'cap_ipaddress', 'ipaddress' ], + [ 'false', 'false' ], + // Hoping ABP-only list maintainers can at least make use of it to + // help non-ABP content blockers better deal with filters benefiting + // only ABP. + [ 'ext_abp', 'false' ], + // Compatibility with other blockers + // https://adguard.com/kb/general/ad-filtering/create-own-filters/#conditions-directive + [ 'adguard', 'adguard' ], + [ 'adguard_app_android', 'false' ], + [ 'adguard_app_cli', 'false' ], + [ 'adguard_app_ios', 'false' ], + [ 'adguard_app_mac', 'false' ], + [ 'adguard_app_windows', 'false' ], + [ 'adguard_ext_android_cb', 'false' ], + [ 'adguard_ext_chromium', 'chromium' ], + [ 'adguard_ext_chromium_mv3', 'mv3' ], + [ 'adguard_ext_edge', 'edge' ], + [ 'adguard_ext_firefox', 'firefox' ], + [ 'adguard_ext_opera', 'chromium' ], + [ 'adguard_ext_safari', 'false' ], + ]); + + const toURL = url => { + try { + return new URL(url.trim()); + } catch { + } + }; + + // Useful reference: + // https://adguard.com/kb/general/ad-filtering/create-own-filters/#conditions-directive + + class preparser { + static evaluateExprToken(token, env = []) { + const not = token.charCodeAt(0) === 0x21 /* ! */; + if ( not ) { token = token.slice(1); } + let state = preparserTokens.get(token); + if ( state === undefined ) { + if ( token.startsWith('cap_') === false ) { return; } + state = 'false'; + } + return state === 'false' && not || env.includes(state) !== not; + } + + static evaluateExpr(expr, env = []) { + if ( expr.startsWith('(') && expr.endsWith(')') ) { + expr = expr.slice(1, -1); + } + const matches = Array.from(expr.matchAll(/(?:(?:&&|\|\|)\s+)?\S+/g)); + if ( matches.length === 0 ) { return; } + if ( matches[0][0].startsWith('|') || matches[0][0].startsWith('&') ) { return; } + let result = this.evaluateExprToken(matches[0][0], env); + for ( let i = 1; i < matches.length; i++ ) { + const parts = matches[i][0].split(/ +/); + if ( parts.length !== 2 ) { return; } + const state = this.evaluateExprToken(parts[1], env); + if ( state === undefined ) { return; } + if ( parts[0] === '||' ) { + result = result || state; + } else if ( parts[0] === '&&' ) { + result = result && state; + } else { + return; + } + } + return result; + } + + // This method returns an array of indices, corresponding to position in + // the content string which should alternatively be parsed and discarded. + static splitter(content, env = []) { + const reIf = /^!#(if|else|endif)\b([^\n]*)(?:[\n\r]+|$)/gm; + const stack = []; + const parts = [ 0 ]; + let discard = false; + + const shouldDiscard = ( ) => stack.some(v => v.known && v.discard); + + const begif = details => { + if ( discard === false && details.known && details.discard ) { + parts.push(details.pos); + discard = true; + } + stack.push(details); + }; + + const endif = match => { + stack.pop(); + const stopDiscard = shouldDiscard() === false; + if ( discard && stopDiscard ) { + parts.push(match.index + match[0].length); + discard = false; + } + }; + + for (;;) { + const match = reIf.exec(content); + if ( match === null ) { break; } + + switch ( match[1] ) { + case 'if': { + const result = this.evaluateExpr(match[2].trim(), env); + begif({ + known: result !== undefined, + discard: result === false, + pos: match.index, + }); + break; + } + case 'else': { + if ( stack.length === 0 ) { break; } + const details = stack[stack.length-1]; + endif(match); + details.discard = details.discard === false; + details.pos = match.index; + begif(details); + break; + } + case 'endif': { + endif(match); + break; + } + default: + break; + } + } + + parts.push(content.length); + return parts; + } + + static expandIncludes(parts, env = []) { + const out = []; + const reInclude = /^!#include +(\S+)[^\n\r]*(?:[\n\r]+|$)/gm; + for ( const part of parts ) { + if ( typeof part === 'string' ) { + out.push(part); + continue; + } + if ( part instanceof Object === false ) { continue; } + const content = part.content; + const slices = this.splitter(content, env); + for ( let i = 0, n = slices.length - 1; i < n; i++ ) { + const slice = content.slice(slices[i+0], slices[i+1]); + if ( (i & 1) !== 0 ) { + out.push(slice); + continue; + } + let lastIndex = 0; + for (;;) { + const match = reInclude.exec(slice); + if ( match === null ) { break; } + if ( toURL(match[1]) !== undefined ) { continue; } + if ( match[1].indexOf('..') !== -1 ) { continue; } + // Compute nested list path relative to parent list path + const pos = part.url.lastIndexOf('/'); + if ( pos === -1 ) { continue; } + const subURL = part.url.slice(0, pos + 1) + match[1].trim(); + out.push( + slice.slice(lastIndex, match.index + match[0].length), + `! >>>>>>>> ${subURL}\n`, + { url: subURL }, + `! <<<<<<<< ${subURL}\n` + ); + lastIndex = reInclude.lastIndex; + } + out.push(lastIndex === 0 ? slice : slice.slice(lastIndex)); + } + } + return out; + } + + static prune(content, env) { + const parts = this.splitter(content, env); + const out = []; + for ( let i = 0, n = parts.length - 1; i < n; i += 2 ) { + const beg = parts[i+0]; + const end = parts[i+1]; + out.push(content.slice(beg, end)); + } + return out.join('\n'); + } + + static getHints() { + const out = []; + const vals = new Set(); + for ( const [ key, val ] of preparserTokens ) { + if ( vals.has(val) ) { continue; } + vals.add(val); + out.push(key); + } + return out; + } + + static getTokens(env) { + const out = new Map(); + for ( const [ key, val ] of preparserTokens ) { + out.set(key, val !== 'false' && env.includes(val)); + } + return Array.from(out); + } + } + + return { + preparser, + }; +})(); + +/******************************************************************************/ |