import { expect } from 'chai' ; import 'mocha' ; import { getDomain } from 'tldts-experimental' ; import Engine from '../../src/engine/engine' ; import NetworkFilter from '../../src/filters/network' ; import Request , { RequestType } from '../../src/request' ; import Resources from '../../src/resources' ; import requests from '../data/requests' ; import { loadEasyListFilters , typedArrayEqual } from '../utils' ; /**
 * Helper function used in the Engine tests. All the assertions are performed by
 * this function. It will be called to tests the different configurations of
 * engines, for each of the requests and all of the filters.
 */ function test ( { engine , filter , testFiltersInIsolation , resources , request , importants , redirects , exceptions , normalFilters , } ) { engine . resources = resources ; const matchingFilters = new Set ( ) ; for ( const matchingFilter of Array . from ( engine . matchAll ( request ) ) ) { ( matchingFilter . rawLine || '' ) . split ( ' <+> ' ) . forEach ( ( f ) => { matchingFilters . add ( f ) ; } ) ; } if ( filter . isImportant ( ) ) { const result = engine . match ( request ) ; expect ( result . filter ) . not . to . be . undefined ; if ( result . filter !== undefined && result . filter . rawLine !== undefined && ! result . filter . rawLine . includes ( '<+>' ) ) { expect ( importants ) . to . include ( result . filter . rawLine ) ; if ( filter . isRedirect ( ) ) { expect ( redirects ) . to . include ( result . filter . rawLine ) ; } } expect ( result . exception ) . to . be . undefined ; if ( ! filter . isRedirect ( ) ) { expect ( result . redirect ) . to . be . undefined ; } expect ( result . match ) . to . be . true ; } else if ( filter . isException ( ) && normalFilters . length !== 0 && ! testFiltersInIsolation && importants . length === 0 ) { const result = engine . match ( request ) ; expect ( result . exception ) . not . to . be . undefined ; if ( result . exception !== undefined && result . exception . rawLine !== undefined && ! result . exception . rawLine . includes ( '<+>' ) ) { expect ( exceptions ) . to . include ( result . exception . rawLine ) ; } expect ( result . filter ) . not . to . be . undefined ; expect ( result . redirect ) . to . be . undefined ; expect ( result . match ) . to . be . false ; } else if ( filter . isRedirect ( ) && exceptions . length === 0 && importants . length === 0 ) { const result = engine . match ( request ) ; expect ( result . filter ) . not . to . be . undefined ; if ( result . filter !== undefined && result . filter . rawLine !== undefined && ! result . filter . rawLine . includes ( '<+>' ) ) { expect ( redirects ) . to . include ( result . filter . rawLine ) ; } expect ( result . exception ) . to . be . undefined ; expect ( result . redirect ) . not . to . be . undefined ; expect ( result . match ) . to . be . true ; } expect ( matchingFilters ) . to . include ( filter . rawLine ) ; } function buildResourcesFromRequests ( filters ) { const resources = [ ] ; filters . forEach ( ( filter ) => { if ( filter . redirect !== undefined ) { const redirect = filter . redirect ; let type = 'application/javascript' ; if ( redirect . endsWith ( '.gif' ) ) { type = 'image/gif;base64' ; } resources . push ( `${ redirect } ${ type }\n${ redirect }` ) ; } } ) ; return Resources . parse ( resources . join ( '\n\n' ) , { checksum : '' } ) ; } function createEngine ( filters , enableOptimizations = true ) { return Engine . parse ( filters , { debug : true , enableOptimizations , } ) ; } describe ( '#FiltersEngine' , ( ) => { it ( 'network filters are disabled' , ( ) => { const request = Request . fromRawDetails ( { url : 'https://foo.com' } ) ; expect ( Engine . parse ( '||foo.com' , { loadNetworkFilters : true } ) . match ( request ) . match ) . to . be . true ; expect ( Engine . parse ( '||foo.com' , { loadNetworkFilters : false } ) . match ( request ) . match ) . to . be . false ; } ) ; it ( 'cosmetic filters are disabled' , ( ) => { expect ( Engine . parse ( '##selector' , { loadCosmeticFilters : true } ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) ) . to . eql ( { active : true , extended : [ ] , scripts : [ ] , styles : 'selector { display: none !important; }' , } ) ; expect ( Engine . parse ( '##selector' , { loadCosmeticFilters : false } ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) ) . to . eql ( { active : false , extended : [ ] , scripts : [ ] , styles : '' , } ) ; } ) ; describe ( 'cps policies' , ( ) => { it ( 'no policy in engine' , ( ) => { expect ( createEngine ( 'this is not a csp' ) . getCSPDirectives ( Request . fromRawDetails ( { url : 'https://foo.com' , } ) ) ) . to . be . undefined ; } ) ; it ( 'network filters are disabled' , ( ) => { expect ( Engine . parse ( '||foo.com$csp=bar' , { loadNetworkFilters : false } ) . getCSPDirectives ( Request . fromRawDetails ( { url : 'https://foo.com' , } ) ) ) . to . be . undefined ; } ) ; it ( 'request not supported' , ( ) => { expect ( Engine . parse ( '||foo.com$csp=bar' ) . getCSPDirectives ( Request . fromRawDetails ( { url : 'ftp://foo.com' , } ) ) ) . to . be . undefined ; expect ( Engine . parse ( '||foo.com$csp=bar' ) . getCSPDirectives ( Request . fromRawDetails ( { type : 'script' , url : 'ftp://foo.com' , } ) ) ) . to . be . undefined ; } ) ; it ( 'does not match request' , ( ) => { expect ( createEngine ( '||bar.com$csp=bar' ) . getCSPDirectives ( Request . fromRawDetails ( { url : 'https://foo.com' , } ) ) ) . to . be . undefined ; } ) ; it ( 'matches request (1 policy)' , ( ) => { expect ( createEngine ( '||foo.com$csp=bar' ) . getCSPDirectives ( Request . fromRawDetails ( { url : 'https://foo.com' , } ) ) ) . to . equal ( 'bar' ) ; } ) ; it ( 'matches request (2 policy)' , ( ) => { const policies = createEngine ( `
||foo.com$csp=bar
$csp=baz,domain=bar.com
` ) . getCSPDirectives ( Request . fromRawDetails ( { sourceUrl : 'https://bar.com' , url : 'https://foo.com' , } ) ) ; expect ( policies ) . not . to . be . undefined ; if ( policies !== undefined ) { expect ( policies . split ( '; ' ) . sort ( ) ) . to . eql ( [ 'bar' , 'baz' ] ) ; } } ) ; it ( 'matches request (1 policy with one exception)' , ( ) => { expect ( createEngine ( `
||foo.com$csp=bar
@@$csp=baz
$csp=baz,domain=bar.com
` ) . getCSPDirectives ( Request . fromRawDetails ( { sourceUrl : 'https://bar.com' , url : 'https://foo.com' , } ) ) ) . to . equal ( 'bar' ) ; } ) ; it ( 'exception global exception' , ( ) => { expect ( createEngine ( `
@@$csp,domain=bar.com
||foo.com$csp=bar
@@$csp=baz
$csp=baz,domain=bar.com
` ) . getCSPDirectives ( Request . fromRawDetails ( { sourceUrl : 'https://bar.com' , url : 'https://foo.com' , } ) ) ) . to . be . undefined ; } ) ; } ) ; describe ( 'badfilter' , ( ) => { const request = Request . fromRawDetails ( { sourceUrl : 'https://bar.com' , type : 'image' , url : 'https://foo.com' , } ) ; it ( 'does not match on its own' , ( ) => { expect ( createEngine ( '||foo.com$badfilter' ) . match ( request ) . match ) . to . be . false ; } ) ; it ( 'cancels filter with same ID' , ( ) => { expect ( createEngine ( `
||foo.com$domain=bar.com|foo.com,badfilter
||foo.com$domain=foo.com|bar.com
` ) . match ( request ) . match ) . to . be . false ; } ) ; it ( 'does not cancel similar filter' , ( ) => { expect ( createEngine ( `
||foo.com$domain=bar.com|foo.com,badfilter
||foo.com$domain=foo.com|bar.com,image
` ) . match ( request ) . match ) . to . be . true ; } ) ; it ( 'cancels exceptions' , ( ) => { expect ( createEngine ( `
@@||foo.com^
||foo.com^
` ) . match ( request ) . match ) . to . be . false ; expect ( createEngine ( `
@@||foo.com^
@@||foo.com^$badfilter
||foo.com^
` ) . match ( request ) . match ) . to . be . true ; } ) ; it ( 'works with update as well' , ( ) => { const badfilter = NetworkFilter . parse ( '||foo.com$domain=bar.com|foo.com,badfilter' ) ; expect ( badfilter ) . not . to . be . null ; if ( badfilter === null ) { return ; } const engine = Engine . parse ( '||foo.com$domain=foo.com|bar.com' , { debug : true } ) ; expect ( engine . match ( request ) . match ) . to . be . true ; engine . update ( { newNetworkFilters : [ badfilter ] , } ) ; expect ( engine . match ( request ) . match ) . to . be . false ; engine . update ( { removedNetworkFilters : [ badfilter . getId ( ) ] , } ) ; expect ( engine . match ( request ) . match ) . to . be . true ; } ) ; } ) ; describe ( 'redirect' , ( ) => { const request = Request . fromRawDetails ( { sourceUrl : 'https://bar.com' , type : 'image' , url : 'https://foo.com' , } ) ; const createEngineWithResource = ( filters , resource ) => { const engine = createEngine ( filters . join ( '\n' ) ) ; engine . resources . js . set ( resource , resource ) ; engine . resources . resources . set ( resource , { body : resource , contentType : 'application/javascript' , } ) ; return engine ; } ; it ( 'normal redirect' , ( ) => { const { filter , exception , match , redirect } = createEngineWithResource ( [ '||foo.com$image,redirect=foo.js' ] , 'foo.js' ) . match ( request ) ; expect ( match ) . to . be . true ; expect ( exception ) . to . be . undefined ; expect ( filter ) . not . to . be . undefined ; expect ( ( filter as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect=foo.js' ) ; expect ( redirect ) . to . eql ( { body : 'foo.js' , contentType : 'application/javascript' , dataUrl : 'data:application/javascript;base64,Zm9vLmpz' , } ) ; } ) ; it ( 'redirect-rule does not match on its own' , ( ) => { const { filter , exception , match , redirect } = createEngineWithResource ( [ '||foo.com$image,redirect-rule=foo.js' ] , 'foo.js' ) . match ( request ) ; expect ( match ) . to . be . false ; expect ( exception ) . to . be . undefined ; expect ( filter ) . to . be . undefined ; expect ( redirect ) . to . be . undefined ; } ) ; it ( 'redirect-rule matches if request was blocked' , ( ) => { const { filter , exception , match , redirect } = createEngineWithResource ( [ '||foo.com$image,redirect-rule=foo.js' , '||foo.com$image' ] , 'foo.js' ) . match ( request ) ; expect ( match ) . to . be . true ; expect ( exception ) . to . be . undefined ; expect ( filter ) . not . to . be . undefined ; expect ( ( filter as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect-rule=foo.js' ) ; expect ( redirect ) . to . eql ( { body : 'foo.js' , contentType : 'application/javascript' , dataUrl : 'data:application/javascript;base64,Zm9vLmpz' , } ) ; } ) ; it ( 'redirect=none cancels redirect-rule' , ( ) => { const { filter , exception , match , redirect } = createEngineWithResource ( [ '||foo.com$image,redirect-rule=foo.js' , '||foo.com$image' , '||foo.com$image,redirect=none' , ] , 'foo.js' ) . match ( request ) ; expect ( match ) . to . be . false ; expect ( exception ) . not . to . be . undefined ; expect ( ( exception as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect=none' ) ; expect ( filter ) . not . to . be . undefined ; expect ( ( filter as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect-rule=foo.js' ) ; expect ( redirect ) . to . be . undefined ; } ) ; it ( 'redirect=none cancels redirect' , ( ) => { const { filter , exception , match , redirect } = createEngineWithResource ( [ '||foo.com$image,redirect=foo.js' , '||foo.com$image' , '||foo.com$image,redirect=none' ] , 'foo.js' ) . match ( request ) ; expect ( match ) . to . be . false ; expect ( exception ) . not . to . be . undefined ; expect ( ( exception as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect=none' ) ; expect ( filter ) . not . to . be . undefined ; expect ( ( filter as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect=foo.js' ) ; expect ( redirect ) . to . be . undefined ; } ) ; it ( 'exception rule also cancels redirect' , ( ) => { const { filter , exception , match , redirect } = createEngineWithResource ( [ '||foo.com$image,redirect=foo.js' , '||foo.com$image' , '@@||foo.com$image' ] , 'foo.js' ) . match ( request ) ; expect ( match ) . to . be . false ; expect ( exception ) . not . to . be . undefined ; expect ( ( exception as NetworkFilter ) . toString ( ) ) . to . equal ( '@@||foo.com$image' ) ; expect ( filter ) . not . to . be . undefined ; expect ( ( filter as NetworkFilter ) . toString ( ) ) . to . equal ( '||foo.com$image,redirect=foo.js' ) ; expect ( redirect ) . to . be . undefined ; } ) ; } ) ; describe ( 'network filters' , ( ) => { const allRequestFilters = requests . map ( ( { filters } ) => filters . join ( '\n' ) ) . join ( '\n' ) ; const engineFullOptimized = Engine . parse ( allRequestFilters , { debug : true , enableOptimizations : true , } ) ; const engineFull = Engine . parse ( allRequestFilters , { debug : true , enableOptimizations : false , } ) ; for ( let i = 0 ; i < requests . length ; i += 1 ) { const { filters , type , url , sourceUrl } = requests [ i ] ; const exceptions = [ ] ; const importants = [ ] ; const redirects = [ ] ; const normalFilters = [ ] ; const parsedFilters = [ ] ; for ( let j = 0 ; j < filters . length ; j += 1 ) { const filter = filters [ j ] ; const parsed = NetworkFilter . parse ( filter , true ) ; expect ( parsed ) . not . to . be . null ; if ( parsed !== null ) { parsedFilters . push ( parsed ) ; if ( parsed . isException ( ) ) { exceptions . push ( filter ) ; } if ( parsed . isImportant ( ) ) { importants . push ( filter ) ; } if ( parsed . isRedirect ( ) ) { redirects . push ( filter ) ; } if ( ! parsed . isRedirect ( ) && ! parsed . isException ( ) && ! parsed . isImportant ( ) ) { normalFilters . push ( filter ) ; } } } const resources = buildResourcesFromRequests ( parsedFilters ) ; const request = Request . fromRawDetails ( { sourceUrl , type : type as RequestType , url , } ) ; it ( `[request] type=${ type } url=${ url }, sourceUrl=${ sourceUrl }` , ( ) => { for ( let j = 0 ; j < parsedFilters . length ; j += 1 ) { const filter = parsedFilters [ j ] ; const baseConfig = { exceptions , filter , importants , normalFilters , redirects , request , resources , } ; test ( { ... baseConfig , engine : new Engine ( { networkFilters : [ filter ] } ) , testFiltersInIsolation : true , } ) ; test ( { ... baseConfig , engine : engineFullOptimized , testFiltersInIsolation : false , } ) ; test ( { ... baseConfig , engine : engineFull , testFiltersInIsolation : false , } ) ; } } ) ; } } ) ; describe ( '#getCosmeticsFilters' , ( ) => { describe ( 'script injections' , ( ) => { it ( 'injects script' , ( ) => { const engine = Engine . parse ( 'foo.com##+js(script.js,arg1)' ) ; engine . resources = Resources . parse ( 'script.js application/javascript\n{{1}}' , { checksum : '' , } ) ; expect ( engine . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) . scripts ) . to . eql ( [ 'arg1' ] ) ; } ) ; it ( 'script missing' , ( ) => { expect ( Engine . parse ( 'foo.com##+js(foo,arg1)' ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) . scripts ) . to . eql ( [ ] ) ; } ) ; } ) ; describe ( 'elemhide' , ( ) => { it ( 'disables cosmetics if domain matches' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$elemhide
foo.com##selector1
##selector1
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . to . equal ( '' ) ; } ) ; } ) ; describe ( 'specifichide' , ( ) => { it ( 'allows specific cosmetics by default' , ( ) => { expect ( Engine . parse ( 'foo.com##selector' ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) . styles ) . not . to . equal ( '' ) ; } ) ; it ( 'disables specific cosmetics if domain matches' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$specifichide
foo.com##selector1
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . to . equal ( '' ) ; } ) ; it ( 'allows specific cosmetics if $specifichide' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$specifichide
foo.com##selector
||foo.com^$specifichide
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . not . to . equal ( '' ) ; } ) ; it ( 'allows specific cosmetics if $specifichide,important' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$important,specifichide
foo.com##selector
||foo.com^$specifichide,important
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . not . to . equal ( '' ) ; } ) ; it ( 'disables specific cosmetics if @@$specifichide,important' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$important,specifichide
foo.com##selector
foo.com##.selector
foo.com###selector
||foo.com^$specifichide
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . to . equal ( '' ) ; } ) ; it ( 'disabling specific hides does not impact scriptlets' , ( ) => { const engine = Engine . parse ( [ '@@||foo.com^$specifichide' , 'foo.com##+js(foo)' ] . join ( '\n' ) ) ; engine . resources . js . set ( 'foo' , '' ) ; expect ( engine . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) . scripts ) . to . have . lengthOf ( 1 ) ; } ) ; } ) ; describe ( 'generichide' , ( ) => { it ( 'allows generic cosmetics by default' , ( ) => { expect ( Engine . parse ( '##selector' ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' , } ) . styles ) . not . to . equal ( '' ) ; } ) ; it ( 'disables generic cosmetics if domain matches' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$generichide
~bar.*##selector1
##selector2
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . to . equal ( '' ) ; } ) ; it ( 'allows generic cosmetics if $generichide' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$generichide
##selector
||foo.com^$generichide
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . not . to . equal ( '' ) ; } ) ; it ( 'allows generic cosmetics if $generichide,important' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$important,generichide
##selector
||foo.com^$generichide,important
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . not . to . equal ( '' ) ; } ) ; it ( 'disables generic cosmetics if @@$generichide,important' , ( ) => { expect ( Engine . parse ( `
@@||foo.com^$important,generichide
##selector
##.selector
###selector
||foo.com^$generichide
` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . to . equal ( '' ) ; } ) ; } ) ; it ( 'handles custom :styles' , ( ) => { expect ( Engine . parse ( `
##selector :style(foo)
##selector :style(bar)
##selector1 :style(foo)` ) . getCosmeticsFilters ( { domain : 'foo.com' , hostname : 'foo.com' , url : 'https://foo.com' } ) . styles ) . to . equal ( 'selector ,\nselector1  { foo }\n\nselector  { bar }' ) ; } ) ; [ { filters : [ '##selector' ] , hostname : 'foo.com' , injections : [ ] , matches : [ 'selector' ] , } , { filters : [ '#@#selector' , '##selector' ] , hostname : 'foo.com' , injections : [ ] , matches : [ ] , } , { classes : [ 'selector' ] , filters : [ '##.selector' ] , hostname : 'foo.com' , injections : [ ] , matches : [ '.selector' ] , } , { classes : [ 'selector' ] , filters : [ '#@#.selector' , '##.selector' ] , hostname : 'foo.com' , injections : [ ] , matches : [ ] , } , { filters : [ '###selector' ] , hostname : 'foo.com' , ids : [ 'selector' ] , injections : [ ] , matches : [ '#selector' ] , } , { filters : [ '###selector' , '#@##selector' ] , hostname : 'foo.com' , ids : [ 'selector' ] , injections : [ ] , matches : [ ] , } , { filters : [ '##a[href="https://foo"]' ] , hostname : 'foo.com' , hrefs : [ 'https://foo.com/bar' ] , injections : [ ] , matches : [ 'a[href="https://foo"]' ] , } , { filters : [ '##a[href="https://foo"]' , '#@#a[href="https://foo"]' ] , hostname : 'foo.com' , hrefs : [ 'https://foo.com/bar' ] , injections : [ ] , matches : [ ] , } , { filters : [ 'foo.com##+js(scriptlet)' ] , hostname : 'foo.com' , hrefs : [ ] , injections : [ 'scriptlet' ] , matches : [ ] , } , { filters : [ 'foo.com##+js(scriptlet)' , 'foo.com#@#+js(scriptlet)' ] , hostname : 'foo.com' , hrefs : [ ] , injections : [ ] , matches : [ ] , } , { filters : [ 'foo.com##+js(scriptlet1)' , 'foo.com##+js(scriptlet2)' , ] , hostname : 'foo.com' , hrefs : [ ] , injections : [ 'scriptlet1' , 'scriptlet2' ] , matches : [ ] , } , { filters : [ 'foo.com##+js(scriptlet1)' , 'foo.com##+js(scriptlet2)' , 'foo.com#@#+js()' , ] , hostname : 'foo.com' , hrefs : [ ] , injections : [ ] , matches : [ ] , } , { filters : [ 'foo.com##+js(scriptlet1)' , 'foo.com##+js(scriptlet2)' , '#@#+js()' , ] , hostname : 'foo.com' , hrefs : [ ] , injections : [ ] , matches : [ ] , } , { filters : [ '##adwords1' , '~google.*##adwords2' ] , hostname : 'domain.com' , injections : [ ] , matches : [ 'adwords1' , 'adwords2' ] , } , { filters : [ '##.adwords1' , '~google.*##.adwords2' , '##[href="https://foo"]' , '##[href^="https://foo"]' , '##[href*="https://foo"]' , '##a[href*="https://foo"]' , ] , hostname : 'domain.com' , injections : [ ] , matches : [ ] , } , { filters : [ '##.adwords1' , '~google.*###adwords2' , '##[href="https://foo"]' , '##[href^="https://foo"]' , '##[href*="https://foo"]' , '##a[href*="https://foo"]' , ] , classes : [ 'foo' , 'bar' , 'adwords1' ] , hostname : 'domain.com' , injections : [ ] , matches : [ '.adwords1' ] , } , { filters : [ '##.adwords1' , '~google.*###adwords2' , '##[href="https://foo"]' , '##[href^="https://foo"]' , '##[href*="https://foo"]' , '##a[href*="https://foo"]' , ] , hostname : 'domain.com' , ids : [ 'foo' , 'bar' , 'adwords2' ] , injections : [ ] , matches : [ '#adwords2' ] , } , { filters : [ '##.adwords1' , '~google.*###adwords2' , '##[href="https://foo.com"]' , '##[href^="https://bar.com"]' , '##[href*="https://baz.net"]' , '##a[href*="http://foo.com"]' , ] , hostname : 'domain.com' , hrefs : [ 'https://foo.com' , 'https://bar.com' ] , injections : [ ] , matches : [ '[href="https://foo.com"]' , '[href^="https://bar.com"]' , 'a[href*="http://foo.com"]' , ] , } , { classes : [ ] , filters : [ '~foo.com##.selector' ] , hostname : 'bar.com' , injections : [ ] , matches : [ ] , } , { classes : [ 'selector' ] , filters : [ '~foo.com##.selector' ] , hostname : 'bar.com' , injections : [ ] , matches : [ '.selector' ] , } , { classes : [ 'selector' ] , filters : [ '~foo.com##.selector' ] , hostname : 'foo.com' , injections : [ ] , matches : [ ] , } , { classes : [ 'selector' ] , filters : [ '~foo.*##.selector' ] , hostname : 'foo.com' , injections : [ ] , matches : [ ] , } , { classes : [ 'selector' ] , filters : [ '~foo.*##.selector' ] , hostname : 'foo.co.uk' , injections : [ ] , matches : [ ] , } , { filters : [ '##adwords1' , '~google.*##adwords2' ] , hostname : 'google.com' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ '##adwords1' , '~google.com#@#adwords2' ] , hostname : 'google.com' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ '##adwords1' , '~google.com#@#adwords2' ] , hostname : 'google.de' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ '##adwords1' , '~google.*#@#adwords2' ] , hostname : 'google.com' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ '##adwords1' , 'google.com#@#adwords1' ] , hostname : 'google.com' , injections : [ ] , matches : [ ] , } , { filters : [ 'google.*##adwords1' , 'google.com#@#adwords1' ] , hostname : 'google.com' , injections : [ ] , matches : [ ] , } , { filters : [ 'google.com##adwords1' , 'google.com#@#adwords1' ] , hostname : 'google.com' , injections : [ ] , matches : [ ] , } , { filters : [ '##adwords1' , 'google.*#@#adwords1' ] , hostname : 'google.com' , injections : [ ] , matches : [ ] , } , { filters : [ 'google.*##adwords1' , 'google.*#@#adwords1' ] , hostname : 'google.com' , injections : [ ] , matches : [ ] , } , { filters : [ '##adwords1' , 'google.de#@#adwords2' ] , hostname : 'google.de' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ 'google.de##adwords1' , 'google.de#@#adwords2' ] , hostname : 'google.de' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ '##adwords1' , 'google.com#@#adwords1' ] , hostname : 'google.de' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ '##adwords1' , 'accounts.google.com#@#adwords1' ] , hostname : 'google.com' , injections : [ ] , matches : [ 'adwords1' ] , } , { filters : [ 'baz.com##selector' ] , hostname : 'foo.bar.baz.com' , injections : [ ] , matches : [ 'selector' ] , } , { filters : [ 'baz.*##selector' ] , hostname : 'foo.bar.baz.com' , injections : [ ] , matches : [ 'selector' ] , } , { filters : [ 'baz.*##selector' ] , hostname : 'foo.bar.baz.co.uk' , injections : [ ] , matches : [ 'selector' ] , } , { filters : [ '~baz.de##selector' ] , hostname : 'foo.bar.baz.de' , injections : [ ] , matches : [ ] , } , { filters : [ '~baz.*##selector' ] , hostname : 'foo.bar.baz.co.uk' , injections : [ ] , matches : [ ] , } , { filters : [ '##ad-stack' ] , hostname : 'speedtest.net' , injections : [ ] , matches : [ 'ad-stack' ] , } , { filters : [ '##AD300Right' ] , hostname : 'example.de' , injections : [ ] , matches : [ 'AD300Right' ] , } , { filters : [ ] , hostname : 'pokerupdate.com' , injections : [ ] , matches : [ ] , } , { filters : [ 'pokerupdate.com##related-room' , 'pokerupdate.com##prev-article' ] , hostname : 'pokerupdate.com' , injections : [ ] , matches : [ 'related-room' , 'prev-article' ] , } , { filters : [ 'google.com,~mail.google.com##.class[style="margin: 0pt;"]1' , '~mail.google.com##.class[style="margin: 0pt;"]2' , '###tads + div + .c' , '##.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '##.mw > #rcnt > #center_col > #taw > .c' , ] , hostname : 'google.com' , injections : [ ] , matches : [ '.class[style="margin: 0pt;"]1' , '.class[style="margin: 0pt;"]2' , '#tads + div + .c' , '.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '.mw > #rcnt > #center_col > #taw > .c' , ] , classes : [ 'class' , 'mw' ] , ids : [ 'tads' ] , } , { filters : [ 'google.com,~mail.google.com##.class[style="margin: 0pt;"]1' , '~mail.google.com##.class[style="margin: 0pt;"]2' , '###tads + div + .c' , '##.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '##.mw > #rcnt > #center_col > #taw > .c' , ] , hostname : 'google.com' , injections : [ ] , matches : [ '.class[style="margin: 0pt;"]1' , '#tads + div + .c' , '.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '.mw > #rcnt > #center_col > #taw > .c' , ] , classes : [ 'mw' ] , ids : [ 'tads' ] , } , { filters : [ 'google.com,~mail.google.com##.class[style="margin: 0pt;"]' , '###tads + div + .c' , '##.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '##.mw > #rcnt > #center_col > #taw > .c' , ] , hostname : 'google.com' , injections : [ ] , matches : [ '.class[style="margin: 0pt;"]' , '#tads + div + .c' ] , ids : [ 'tads' ] , } , { filters : [ 'google.com,~mail.google.com##.class[style="margin: 0pt;"]' , '###tads + div + .c' , '##.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '##.mw > #rcnt > #center_col > #taw > .c' , ] , hostname : 'google.com' , injections : [ ] , matches : [ '.class[style="margin: 0pt;"]' ] , } , { filters : [ 'google.com,~mail.google.com##.class[style="margin: 0pt;"]' , '###tads + div + .c' , '##.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '##.mw > #rcnt > #center_col > #taw > .c' , ] , hostname : 'mail.google.com' , injections : [ ] , matches : [ '#tads + div + .c' , '.mw > #rcnt > #center_col > #taw > #tvcap > .c' , '.mw > #rcnt > #center_col > #taw > .c' , ] , classes : [ 'class' , 'mw' ] , ids : [ 'tads' ] , } , { filters : [ ] , hostname : 'bitbucket.org' , injections : [ ] , matches : [ ] , } , ] . forEach ( ( { classes , filters , hostname , hrefs , ids , injections , matches , } ) => { it ( JSON . stringify ( { filters , hostname , matches , injections } ) , ( ) => { const engine = createEngine ( filters . join ( '\n' ) ) ; engine . resources . js . set ( 'scriptlet' , 'scriptlet' ) ; engine . resources . js . set ( 'scriptlet1' , 'scriptlet1' ) ; engine . resources . js . set ( 'scriptlet2' , 'scriptlet2' ) ; const { styles , scripts } = engine . getCosmeticsFilters ( { domain : getDomain ( hostname ) || '' , hostname , url : `https://${ hostname }` , classes , hrefs , ids , } ) ; expect ( scripts ) . to . have . lengthOf ( injections . length ) ; expect ( scripts . sort ( ) ) . to . eql ( injections . sort ( ) ) ; const selectors = [ ] ; if ( styles . length !== 0 ) { for ( const stylesheet of styles . trim ( ) . split ( '\n\n' ) ) { const parts = stylesheet . trim ( ) . split ( ',\n' ) ; selectors . push ( ... parts . slice ( 0 , - 1 ) ) ; const last = parts [ parts . length - 1 ] ; selectors . push ( last . slice ( 0 , last . lastIndexOf ( '{' ) ) . trim ( ) ) ; } } expect ( selectors ) . to . have . lengthOf ( matches . length ) ; expect ( selectors . sort ( ) ) . to . eql ( matches . sort ( ) ) ; } ) ; } ) ; } ) ; } ) ; describe ( 'diff updates' , ( ) => { function testUpdates ( name , baseFilters ) { describe ( name , ( ) => { const base = Engine . parse ( baseFilters . join ( '\n' ) , { debug : false , enableCompression : false , enableOptimizations : false , integrityCheck : false , loadCosmeticFilters : false , loadGenericCosmeticsFilters : false , loadNetworkFilters : true , } ) ; const baseSerialized = base . serialize ( ) ; const getSerialized = ( ) => baseSerialized . slice ( ) ; const getEngine = ( ) => Engine . deserialize ( getSerialized ( ) ) ; it ( 'stays the same with empty update' , ( ) => { const engine = getEngine ( ) ; const updated = engine . updateFromDiff ( { } ) ; expect ( updated ) . to . be . false ; expect ( typedArrayEqual ( engine . serialize ( ) , getSerialized ( ) ) ) . to . be . true ; } ) ; it ( 'stays the same with adding removing same filters' , ( ) => { const filtersAdded = [ '||hostname*^bar|$image,domain=foo.com|baz.co.uk' , '||hostname*^bar|$image,domain=foo.com|baz.co.uk,generichide' , '||hostame*^bar|$image,domain=foo.com|baz.co.uk,badfilter' , '||hostame*^bar|$image,domain=foo.com|baz.co.uk' , 'ads$csp=foo' , 'tracker$redirect=foo.js' , '@@||f*o*o.com^$~media' , '/very_important/ads.js$important,script' , 'foo.com,bar.*##.selector' , '#@#.selector' , '##+js(inject.js,arg1,arg2)' , ] ; const filtersRemoved = Array . from ( filtersAdded ) ; const engine = getEngine ( ) ; let updated = engine . updateFromDiff ( { added : filtersAdded } ) ; expect ( updated ) . to . be . true ; expect ( typedArrayEqual ( engine . serialize ( ) , getSerialized ( ) ) ) . to . be . false ; updated = engine . updateFromDiff ( { removed : filtersRemoved } ) ; expect ( updated ) . to . be . true ; expect ( typedArrayEqual ( engine . serialize ( ) , getSerialized ( ) ) ) . to . be . true ; } ) ; } ) ; } testUpdates ( 'empty engine' , [ ] ) ; testUpdates ( 'easylist engine' , loadEasyListFilters ( ) ) ; } ) ;