import { AST , classifySelector , SelectorType , parse as parseCssSelector , } from '@cliqz/adblocker-extended-selectors' ; import { Domains } from '../engine/domains' ; import { EMPTY_UINT32_ARRAY , StaticDataView , sizeOfASCII , sizeOfCosmeticSelector , sizeOfUTF8 , sizeOfRawCosmetic , } from '../data-view' ; import { getHostnameHashesFromLabelsBackward , getEntityHashesFromLabelsBackward , } from '../request' ; import { fastHash , fastHashBetween , fastStartsWithFrom , getBit , hasUnicode , setBit , tokenize , } from '../utils' ; import IFilter from './interface' ; import { HTMLSelector , extractHTMLSelectorFromRule } from '../html-filtering' ; const EMPTY_TOKENS = [ EMPTY_UINT32_ARRAY ] ; export const DEFAULT_HIDDING_STYLE = 'display: none !important;' ; /**
 * Given a `selector` starting with either '#' or '.' check if what follows is
 * a simple CSS selector: /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/
 */ function isSimpleSelector ( selector ) { for ( let i = 1 ; i < selector . length ; i += 1 ) { const code = selector . charCodeAt ( i ) ; if ( ! ( code === 45 || code === 95 || ( code >= 48 && code <= 57 ) || ( code >= 65 && code <= 90 ) || ( code >= 97 && code <= 122 ) ) ) { if ( i < selector . length - 1 ) { const nextCode = selector . charCodeAt ( i + 1 ) ; if ( code === 91 || code === 46 || code === 58 || ( code === 32 && ( nextCode === 62 || nextCode === 43 || nextCode === 126 || nextCode === 46 || nextCode === 35 ) ) ) { return true ; } } return false ; } } return true ; } /**
 * Given a `selector` starting with either 'a[' or '[', check if what follows
 * is a simple href attribute selector of the form: 'href^=' or 'href*='.
 */ function isSimpleHrefSelector ( selector , start ) { return ( selector . startsWith ( 'href^="' , start ) || selector . startsWith ( 'href*="' , start ) || selector . startsWith ( 'href="' , start ) ) ; } /**
 * Validate CSS selector. There is a fast path for simple selectors (e.g.: #foo
 * or .bar) which are the most common case. For complex ones, we rely on
 * `Element.matches` (if available).
 */ const isValidCss = ( ( ) => { const div = typeof document !== 'undefined' ? document . createElement ( 'div' ) : { matches : ( ) => { } , } ; const matches = ( selector ) : void | boolean => div . matches ( selector ) ; const validSelectorRe = /^[#.]?[\w-.]+$/ ; return function isValidCssImpl ( selector ) : boolean { if ( validSelectorRe . test ( selector ) ) { return true ; } try { matches ( selector ) ; } catch ( ex ) { return false ; } return true ; } ; } ) ( ) ; /**
 * Masks used to store options of cosmetic filters in a bitmask.
 */ const enum COSMETICS_MASK { unhide = 1 << 0 , scriptInject = 1 << 1 , isUnicode = 1 << 2 , isClassSelector = 1 << 3 , isIdSelector = 1 << 4 , isHrefSelector = 1 << 5 , remove = 1 << 6 , extended = 1 << 7 } function computeFilterId ( mask , selector , domains , style ) { let hash = ( 5437 * 33 ) ^ mask ; if ( selector !== undefined ) { for ( let i = 0 ; i < selector . length ; i += 1 ) { hash = ( hash * 33 ) ^ selector . charCodeAt ( i ) ; } } if ( domains !== undefined ) { hash = domains . updateId ( hash ) ; } if ( style !== undefined ) { for ( let i = 0 ; i < style . length ; i += 1 ) { hash = ( hash * 33 ) ^ style . charCodeAt ( i ) ; } } return hash >>> 0 ; } /***************************************************************************
 *  Cosmetic filters parsing
 * ************************************************************************ */ export default class CosmeticFilter implements IFilter { /**
     * Given a line that we know contains a cosmetic filter, create a CosmeticFiler
     * instance out of it. This function should be *very* efficient, as it will be
     * used to parse tens of thousands of lines.
     */ public static parse ( line , debug = false ) { let mask = 0 ; let selector ; let domains ; let style ; const sharpIndex = line . indexOf ( '#' ) ; const afterSharpIndex = sharpIndex + 1 ; let suffixStartIndex = afterSharpIndex + 1 ; if ( line . length > afterSharpIndex ) { if ( line [ afterSharpIndex ] === '@' ) { mask = setBit ( mask , COSMETICS_MASK . unhide ) ; suffixStartIndex += 1 ; } else if ( line [ afterSharpIndex ] === '?' ) { suffixStartIndex += 1 ; } } if ( suffixStartIndex >= line . length ) { return null ; } if ( sharpIndex > 0 ) { domains = Domains . parse ( line . slice ( 0 , sharpIndex ) . split ( ',' ) ) ; } if ( line . endsWith ( ':remove()' ) ) { mask = setBit ( mask , COSMETICS_MASK . remove ) ; mask = setBit ( mask , COSMETICS_MASK . extended ) ; line = line . slice ( 0 , - 9 ) ; } else if ( line . length - suffixStartIndex >= 8 && line . endsWith ( ')' ) && line . indexOf ( ':style(' , suffixStartIndex ) !== - 1 ) { const indexOfStyle = line . indexOf ( ':style(' , suffixStartIndex ) ; style = line . slice ( indexOfStyle + 7 , - 1 ) ; line = line . slice ( 0 , indexOfStyle ) ; } if ( line . charCodeAt ( suffixStartIndex ) === 94 ) { if ( fastStartsWithFrom ( line , 'script:has-text(' , suffixStartIndex + 1 ) === false || line . charCodeAt ( line . length - 1 ) !== 41 ) { return null ; } selector = line . slice ( suffixStartIndex , line . length ) ; if ( extractHTMLSelectorFromRule ( selector ) === undefined ) { return null ; } } else if ( line . length - suffixStartIndex > 4 && line . charCodeAt ( suffixStartIndex ) === 43 && fastStartsWithFrom ( line , '+js(' , suffixStartIndex ) ) { if ( ( domains === undefined || ( domains . hostnames === undefined && domains . entities === undefined ) ) && getBit ( mask , COSMETICS_MASK . unhide ) === false ) { return null ; } mask = setBit ( mask , COSMETICS_MASK . scriptInject ) ; selector = line . slice ( suffixStartIndex + 4 , line . length - 1 ) ; if ( getBit ( mask , COSMETICS_MASK . unhide ) === false && selector . length === 0 ) { return null ; } } else { selector = line . slice ( suffixStartIndex ) ; const selectorType = classifySelector ( selector ) ; if ( selectorType === SelectorType . Extended ) { mask = setBit ( mask , COSMETICS_MASK . extended ) ; } else if ( selectorType === SelectorType . Invalid || ! isValidCss ( selector ) ) { return null ; } } if ( domains === undefined && getBit ( mask , COSMETICS_MASK . extended ) === true ) { return null ; } if ( selector !== undefined ) { if ( hasUnicode ( selector ) ) { mask = setBit ( mask , COSMETICS_MASK . isUnicode ) ; } if ( getBit ( mask , COSMETICS_MASK . scriptInject ) === false && getBit ( mask , COSMETICS_MASK . remove ) === false && getBit ( mask , COSMETICS_MASK . extended ) === false && selector . startsWith ( '^' ) === false ) { const c0 = selector . charCodeAt ( 0 ) ; const c1 = selector . charCodeAt ( 1 ) ; const c2 = selector . charCodeAt ( 2 ) ; if ( getBit ( mask , COSMETICS_MASK . scriptInject ) === false ) { if ( c0 === 46 && isSimpleSelector ( selector ) ) { mask = setBit ( mask , COSMETICS_MASK . isClassSelector ) ; } else if ( c0 === 35 && isSimpleSelector ( selector ) ) { mask = setBit ( mask , COSMETICS_MASK . isIdSelector ) ; } else if ( c0 === 97 && c1 === 91 && c2 === 104 && isSimpleHrefSelector ( selector , 2 ) ) { mask = setBit ( mask , COSMETICS_MASK . isHrefSelector ) ; } else if ( c0 === 91 && c1 === 104 && isSimpleHrefSelector ( selector , 1 ) ) { mask = setBit ( mask , COSMETICS_MASK . isHrefSelector ) ; } } } } return new CosmeticFilter ( { mask , rawLine : debug === true ? line : undefined , selector , style , domains , } ) ; } /**
     * Deserialize cosmetic filters. The code accessing the buffer should be
     * symetrical to the one in `serializeCosmeticFilter`.
     */ public static deserialize ( buffer ) { const mask = buffer . getUint8 ( ) ; const isUnicode = getBit ( mask , COSMETICS_MASK . isUnicode ) ; const optionalParts = buffer . getUint8 ( ) ; const selector = isUnicode ? buffer . getUTF8 ( ) : buffer . getCosmeticSelector ( ) ; return new CosmeticFilter ( { mask , selector , domains : ( optionalParts & 1 ) === 1 ? Domains . deserialize ( buffer ) : undefined , rawLine : ( optionalParts & 2 ) === 2 ? buffer . getRawCosmetic ( ) : undefined , style : ( optionalParts & 4 ) === 4 ? buffer . getASCII ( ) : undefined , } ) ; } public readonly mask : number ; public readonly selector : string ; public readonly domains : Domains | undefined ; public readonly style : string | undefined ; public readonly rawLine : string | undefined ; private id : number | undefined ; constructor ( { mask , selector , domains , rawLine , style , } ) { this . mask = mask ; this . selector = selector ; this . domains = domains ; this . style = style ; this . id = undefined ; this . rawLine = rawLine ; } public isCosmeticFilter ( ) { return true ; } public isNetworkFilter ( ) { return false ; } /**
     * The format of a cosmetic filter is:
     *
     * | mask | selector length | selector... | hostnames length | hostnames...
     *   32     16                              16
     *
     * The header (mask) is 32 bits, then we have a total of 32 bits to store the
     * length of `selector` and `hostnames` (16 bits each).
     *
     * Improvements similar to the onces mentioned in `serializeNetworkFilters`
     * could be applied here, to get a more compact representation.
     */ public serialize ( buffer ) { buffer . pushUint8 ( this . mask ) ; const index = buffer . getPos ( ) ; buffer . pushUint8 ( 0 ) ; if ( this . isUnicode ( ) ) { buffer . pushUTF8 ( this . selector ) ; } else { buffer . pushCosmeticSelector ( this . selector ) ; } let optionalParts = 0 ; if ( this . domains !== undefined ) { optionalParts |= 1 ; this . domains . serialize ( buffer ) ; } if ( this . rawLine !== undefined ) { optionalParts |= 2 ; buffer . pushRawCosmetic ( this . rawLine ) ; } if ( this . style !== undefined ) { optionalParts |= 4 ; buffer . pushASCII ( this . style ) ; } buffer . setByte ( index , optionalParts ) ; } /**
     * Return an estimation of the size (in bytes) needed to persist this filter
     * in a DataView. This does not need to be 100% accurate but should be an
     * upper-bound. It should also be as fast as possible.
     */ public getSerializedSize ( compression ) { let estimate = 1 + 1 ; if ( this . isUnicode ( ) ) { estimate += sizeOfUTF8 ( this . selector ) ; } else { estimate += sizeOfCosmeticSelector ( this . selector , compression ) ; } if ( this . domains !== undefined ) { estimate += this . domains . getSerializedSize ( ) ; } if ( this . rawLine !== undefined ) { estimate += sizeOfRawCosmetic ( this . rawLine , compression ) ; } if ( this . style !== undefined ) { estimate += sizeOfASCII ( this . style ) ; } return estimate ; } /**
     * Create a more human-readable version of this filter. It is mainly used for
     * debugging purpose, as it will expand the values stored in the bit mask.
     */ public toString ( ) { if ( this . rawLine !== undefined ) { return this . rawLine ; } let filter = '' ; if ( this . domains !== undefined ) { filter += '<hostnames>' ; } if ( this . isUnhide ( ) ) { filter += '#@#' ; } else { filter += '##' ; } if ( this . isScriptInject ( ) ) { filter += '+js(' ; filter += this . selector ; filter += ')' ; } else { filter += this . selector ; } return filter ; } public match ( hostname , domain ) { if ( this . hasHostnameConstraint ( ) === false ) { return true ; } if ( ! hostname && this . hasHostnameConstraint ( ) ) { return false ; } if ( this . domains !== undefined ) { return this . domains . match ( hostname . length === 0 ? EMPTY_UINT32_ARRAY : getHostnameHashesFromLabelsBackward ( hostname , domain ) , hostname . length === 0 ? EMPTY_UINT32_ARRAY : getEntityHashesFromLabelsBackward ( hostname , domain ) ) ; } return true ; } /**
     * Get tokens for this filter. It can be indexed multiple times if multiple
     * hostnames are specified (e.g.: host1,host2##.selector).
     */ public getTokens ( ) { const tokens = [ ] ; if ( this . domains !== undefined ) { const { hostnames , entities } = this . domains ; if ( hostnames !== undefined ) { for ( const hostname of hostnames ) { tokens . push ( new Uint32Array ( [ hostname ] ) ) ; } } if ( entities !== undefined ) { for ( const entity of entities ) { tokens . push ( new Uint32Array ( [ entity ] ) ) ; } } } if ( tokens . length === 0 && this . isUnhide ( ) === false ) { if ( this . isIdSelector ( ) || this . isClassSelector ( ) ) { let endOfSelector = 1 ; const selector = this . selector ; for ( ; endOfSelector < selector . length ; endOfSelector += 1 ) { const code = selector . charCodeAt ( endOfSelector ) ; if ( code === 32 || code === 46 || code === 58 || code === 91 ) { break ; } } const arr = new Uint32Array ( 1 ) ; arr [ 0 ] = fastHashBetween ( selector , 1 , endOfSelector ) ; tokens . push ( arr ) ; } else if ( this . isHrefSelector ( ) === true ) { const selector = this . getSelector ( ) ; let hrefIndex = selector . indexOf ( 'href' ) ; if ( hrefIndex === - 1 ) { return EMPTY_TOKENS ; } hrefIndex += 4 ; let skipFirstToken = false ; let skipLastToken = true ; if ( selector . charCodeAt ( hrefIndex ) === 42 ) { skipFirstToken = true ; hrefIndex += 1 ; } else if ( selector . charCodeAt ( hrefIndex ) === 94 ) { hrefIndex += 1 ; } else { skipLastToken = false ; } hrefIndex += 2 ; const hrefEnd = selector . indexOf ( '"' , hrefIndex ) ; if ( hrefEnd === - 1 ) { return EMPTY_TOKENS ; } tokens . push ( tokenize ( this . selector . slice ( hrefIndex , hrefEnd ) , skipFirstToken , skipLastToken ) ) ; } } if ( tokens . length === 0 ) { return EMPTY_TOKENS ; } return tokens ; } public getScript ( js ) { let scriptName = this . getSelector ( ) ; let scriptArguments = [ ] ; if ( scriptName . indexOf ( ',' ) !== - 1 ) { const parts = scriptName . split ( ',' ) ; if ( parts . length === 0 ) { return undefined ; } const firstPart = parts [ 0 ] ; if ( firstPart === undefined ) { return undefined ; } scriptName = firstPart ; scriptArguments = parts . slice ( 1 ) . map ( ( s ) => s . trim ( ) ) ; } let script = js . get ( scriptName ) ; if ( script !== undefined ) { for ( let i = 0 ; i < scriptArguments . length ; i += 1 ) { script = script . replace ( `{{${ i + 1 }}}` , scriptArguments [ i ] ) ; } return script ; } return undefined ; } public hasHostnameConstraint ( ) { return this . domains !== undefined ; } public getId ( ) { if ( this . id === undefined ) { this . id = computeFilterId ( this . mask , this . selector , this . domains , this . style ) ; } return this . id ; } public hasCustomStyle ( ) { return this . style !== undefined ; } public getStyle ( ) { return this . style || DEFAULT_HIDDING_STYLE ; } public getStyleAttributeHash ( ) { return `s${ fastHash ( this . getStyle ( ) ) }` ; } public getSelector ( ) { return this . selector ; } public getSelectorAST ( ) { return parseCssSelector ( this . getSelector ( ) ) ; } public getExtendedSelector ( ) { return extractHTMLSelectorFromRule ( this . selector ) ; } public isExtended ( ) { return getBit ( this . mask , COSMETICS_MASK . extended ) ; } public isRemove ( ) { return getBit ( this . mask , COSMETICS_MASK . remove ) ; } public isUnhide ( ) { return getBit ( this . mask , COSMETICS_MASK . unhide ) ; } public isScriptInject ( ) { return getBit ( this . mask , COSMETICS_MASK . scriptInject ) ; } public isCSS ( ) { return this . isScriptInject ( ) === false ; } public isIdSelector ( ) { return getBit ( this . mask , COSMETICS_MASK . isIdSelector ) ; } public isClassSelector ( ) { return getBit ( this . mask , COSMETICS_MASK . isClassSelector ) ; } public isHrefSelector ( ) { return getBit ( this . mask , COSMETICS_MASK . isHrefSelector ) ; } public isUnicode ( ) { return getBit ( this . mask , COSMETICS_MASK . isUnicode ) ; } public isHtmlFiltering ( ) { return this . getSelector ( ) . startsWith ( '^' ) ; } public isGenericHide ( ) { return this ?. domains ?. hostnames === undefined && this ?. domains ?. entities === undefined ; } }