diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index b11a2f6a6..1ca2528ea 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -179,6 +179,7 @@ function registerGeneric(context, genericDetails) { if ( js.length === 0 ) { return; } + js.unshift('/js/scripting/isolated-api.js'); js.push('/js/scripting/css-generic.js'); const { none, basic, optimal, complete } = filteringModeDetails; @@ -252,6 +253,7 @@ function registerProcedural(context) { ]; if ( matches.length === 0 ) { return; } + js.unshift('/js/scripting/isolated-api.js'); js.push('/js/scripting/css-procedural.js'); const excludeMatches = []; @@ -311,6 +313,7 @@ function registerDeclarative(context) { ]; if ( matches.length === 0 ) { return; } + js.unshift('/js/scripting/isolated-api.js'); js.push('/js/scripting/css-declarative.js'); const excludeMatches = []; @@ -370,6 +373,7 @@ function registerSpecific(context) { ]; if ( matches.length === 0 ) { return; } + js.unshift('/js/scripting/isolated-api.js'); js.push('/js/scripting/css-specific.js'); const excludeMatches = []; diff --git a/platform/mv3/extension/js/scripting/css-declarative.js b/platform/mv3/extension/js/scripting/css-declarative.js index 539c97d51..9f63fe558 100644 --- a/platform/mv3/extension/js/scripting/css-declarative.js +++ b/platform/mv3/extension/js/scripting/css-declarative.js @@ -27,73 +27,41 @@ const declarativeImports = self.declarativeImports || []; self.declarativeImports = undefined; -delete self.declarativeImports; /******************************************************************************/ -const hnParts = []; -try { hnParts.push(...document.location.hostname.split('.')); } -catch { } -const hnpartslen = hnParts.length; -if ( hnpartslen === 0 ) { return; } - const selectors = []; +const exceptions = []; -for ( const { argsList, exceptionsMap, hostnamesMap, entitiesMap } of declarativeImports ) { - const todoIndices = new Set(); - const tonotdoIndices = []; - // Exceptions - if ( exceptionsMap.size !== 0 ) { - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = hnParts.slice(i).join('.'); - const excepted = exceptionsMap.get(hn); - if ( excepted ) { tonotdoIndices.push(...excepted); } - } - exceptionsMap.clear(); +const lookupHostname = (hostname, details, out) => { + let seqi = details.hostnamesMap.get(hostname); + if ( seqi === undefined ) { return; } + const { argsList, argsSeqs } = details; + for (;;) { + const argi = argsSeqs[seqi++]; + const done = argi > 0; + out.push(...argsList[done ? argi : -argi].split('\n')); + if ( done ) { break; } } - // Hostname-based - if ( hostnamesMap.size !== 0 ) { - const collectArgIndices = hn => { - let argsIndices = hostnamesMap.get(hn); - if ( argsIndices === undefined ) { return; } - if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } - for ( const argsIndex of argsIndices ) { - if ( tonotdoIndices.includes(argsIndex) ) { continue; } - todoIndices.add(argsIndex); - } - }; - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = hnParts.slice(i).join('.'); - collectArgIndices(hn); - } - collectArgIndices('*'); - hostnamesMap.clear(); +}; + +const lookupAll = hostname => { + for ( const details of declarativeImports ) { + lookupHostname(hostname, details, selectors); + lookupHostname(`~${hostname}`, details, exceptions); } - // Entity-based - if ( entitiesMap.size !== 0 ) { - const n = hnpartslen - 1; - for ( let i = 0; i < n; i++ ) { - for ( let j = n; j > i; j-- ) { - const en = hnParts.slice(i,j).join('.'); - let argsIndices = entitiesMap.get(en); - if ( argsIndices === undefined ) { continue; } - if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } - for ( const argsIndex of argsIndices ) { - if ( tonotdoIndices.includes(argsIndex) ) { continue; } - todoIndices.add(argsIndex); - } - } - } - entitiesMap.clear(); - } - for ( const i of todoIndices ) { - selectors.push(...argsList[i].map(json => JSON.parse(json))); - } - argsList.length = 0; -} +}; + +self.isolatedAPI.forEachHostname(lookupAll, { + hasEntities: declarativeImports.some(a => a.hasEntities) +}); + declarativeImports.length = 0; -if ( selectors.length === 0 ) { return; } +const exceptedSelectors = exceptions.length !== 0 + ? selectors.filter(a => exceptions.includes(a) === false) + : selectors; +if ( exceptedSelectors.length === 0 ) { return; } /******************************************************************************/ @@ -126,8 +94,9 @@ const cssRuleFromProcedural = details => { }; const sheetText = []; -for ( const selector of selectors ) { - const ruleText = cssRuleFromProcedural(selector); +for ( const selector of exceptedSelectors ) { + const details = JSON.parse(selector); + const ruleText = cssRuleFromProcedural(details); if ( ruleText === undefined ) { continue; } sheetText.push(ruleText); } @@ -138,7 +107,7 @@ if ( sheetText.length === 0 ) { return; } chrome.runtime.sendMessage({ what: 'insertCSS', css }).catch(( ) => { count -= 1; if ( count === 0 ) { return; } - uBOL_injectCSS(css, count - 1); + uBOL_injectCSS(css, count); }); })(sheetText.join('\n')); diff --git a/platform/mv3/extension/js/scripting/css-generic.js b/platform/mv3/extension/js/scripting/css-generic.js index b00ea03bd..5206e92a7 100644 --- a/platform/mv3/extension/js/scripting/css-generic.js +++ b/platform/mv3/extension/js/scripting/css-generic.js @@ -26,10 +26,12 @@ (function uBOL_cssGeneric() { const genericSelectorMap = self.genericSelectorMap || new Map(); -delete self.genericSelectorMap; - +self.genericSelectorMap = undefined; if ( genericSelectorMap.size === 0 ) { return; } +const genericExceptionMap = self.genericExceptionMap || new Map(); +self.genericExceptionMap = undefined; + /******************************************************************************/ const maxSurveyTimeSlice = 4; @@ -76,6 +78,7 @@ const uBOL_idFromNode = (node, out) => { const selectorList = genericSelectorMap.get(hash); if ( selectorList === undefined ) { return; } genericSelectorMap.delete(hash); + if ( isExcepted(hash) ) { return; } out.push(selectorList); }; @@ -96,10 +99,20 @@ const uBOL_classesFromNode = (node, out) => { const selectorList = genericSelectorMap.get(hash); if ( selectorList === undefined ) { continue; } genericSelectorMap.delete(hash); + if ( isExcepted(hash) ) { continue; } out.push(selectorList); } }; +const isExcepted = hash => { + const hostnames = genericExceptionMap.get(hash); + if ( hostnames === undefined ) { return; } + const hasEntities = hostnames.includes('.*'); + return self.isolatedAPI.forEachHostname((hostname) => { + if ( hostnames.includes(` ${hostname} `) ) { return true; } + }, { hasEntities }); +} + /******************************************************************************/ const pendingNodes = { @@ -191,12 +204,26 @@ const uBOL_injectCSS = (css, count = 10) => { chrome.runtime.sendMessage({ what: 'insertCSS', css }).catch(( ) => { count -= 1; if ( count === 0 ) { return; } - uBOL_injectCSS(css, count - 1); + uBOL_injectCSS(css, count); }); }; /******************************************************************************/ +const stopAll = reason => { + if ( domChangeTimer !== undefined ) { + self.clearTimeout(domChangeTimer); + domChangeTimer = undefined; + } + domMutationObserver.disconnect(); + domMutationObserver.takeRecords(); + domMutationObserver = undefined; + genericSelectorMap.clear(); + console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`); +}; + +/******************************************************************************/ + pendingNodes.add(document); uBOL_processNodes(); @@ -219,20 +246,6 @@ needDomChangeObserver(); /******************************************************************************/ -const stopAll = reason => { - if ( domChangeTimer !== undefined ) { - self.clearTimeout(domChangeTimer); - domChangeTimer = undefined; - } - domMutationObserver.disconnect(); - domMutationObserver.takeRecords(); - domMutationObserver = undefined; - genericSelectorMap.clear(); - console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`); -}; - -/******************************************************************************/ - })(); /******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting/css-procedural.js b/platform/mv3/extension/js/scripting/css-procedural.js index da27a6d22..101aa7ee7 100644 --- a/platform/mv3/extension/js/scripting/css-procedural.js +++ b/platform/mv3/extension/js/scripting/css-procedural.js @@ -27,73 +27,41 @@ const proceduralImports = self.proceduralImports || []; self.proceduralImports = undefined; -delete self.proceduralImports; /******************************************************************************/ -const hnParts = []; -try { hnParts.push(...document.location.hostname.split('.')); } -catch { } -const hnpartslen = hnParts.length; -if ( hnpartslen === 0 ) { return; } - const selectors = []; +const exceptions = []; -for ( const { argsList, exceptionsMap, hostnamesMap, entitiesMap } of proceduralImports ) { - const todoIndices = new Set(); - const tonotdoIndices = []; - // Exceptions - if ( exceptionsMap.size !== 0 ) { - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = hnParts.slice(i).join('.'); - const excepted = exceptionsMap.get(hn); - if ( excepted ) { tonotdoIndices.push(...excepted); } - } - exceptionsMap.clear(); +const lookupHostname = (hostname, details, out) => { + let seqi = details.hostnamesMap.get(hostname); + if ( seqi === undefined ) { return; } + const { argsList, argsSeqs } = details; + for (;;) { + const argi = argsSeqs[seqi++]; + const done = argi > 0; + out.push(...argsList[done ? argi : -argi]); + if ( done ) { break; } } - // Hostname-based - if ( hostnamesMap.size !== 0 ) { - const collectArgIndices = hn => { - let argsIndices = hostnamesMap.get(hn); - if ( argsIndices === undefined ) { return; } - if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } - for ( const argsIndex of argsIndices ) { - if ( tonotdoIndices.includes(argsIndex) ) { continue; } - todoIndices.add(argsIndex); - } - }; - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = hnParts.slice(i).join('.'); - collectArgIndices(hn); - } - collectArgIndices('*'); - hostnamesMap.clear(); +}; + +const lookupAll = hostname => { + for ( const details of proceduralImports ) { + lookupHostname(hostname, details, selectors); + lookupHostname(`~${hostname}`, details, exceptions); } - // Entity-based - if ( entitiesMap.size !== 0 ) { - const n = hnpartslen - 1; - for ( let i = 0; i < n; i++ ) { - for ( let j = n; j > i; j-- ) { - const en = hnParts.slice(i,j).join('.'); - let argsIndices = entitiesMap.get(en); - if ( argsIndices === undefined ) { continue; } - if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } - for ( const argsIndex of argsIndices ) { - if ( tonotdoIndices.includes(argsIndex) ) { continue; } - todoIndices.add(argsIndex); - } - } - } - entitiesMap.clear(); - } - for ( const i of todoIndices ) { - selectors.push(...argsList[i].map(json => JSON.parse(json))); - } - argsList.length = 0; -} +}; + +self.isolatedAPI.forEachHostname(lookupAll, { + hasEntities: proceduralImports.some(a => a.hasEntities) +}); + proceduralImports.length = 0; -if ( selectors.length === 0 ) { return; } +const exceptedSelectors = exceptions.length !== 0 + ? selectors.filter(a => exceptions.includes(a) === false) + : selectors; +if ( exceptedSelectors.length === 0 ) { return; } /******************************************************************************/ @@ -101,7 +69,7 @@ const uBOL_injectCSS = (css, count = 10) => { chrome.runtime.sendMessage({ what: 'insertCSS', css }).catch(( ) => { count -= 1; if ( count === 0 ) { return; } - uBOL_injectCSS(css, count - 1); + uBOL_injectCSS(css, count); }); }; @@ -646,7 +614,7 @@ class ProceduralFilterer { this.uBOL_commitNow(); } - addSelectors() { + addSelectors(selectors) { for ( const selector of selectors ) { const pselector = new PSelectorRoot(selector); this.primeProceduralSelector(pselector); @@ -787,7 +755,9 @@ class ProceduralFilterer { /******************************************************************************/ -const proceduralFilterer = new ProceduralFilterer(selectors); +const proceduralFilterer = new ProceduralFilterer( + exceptedSelectors.map(a => JSON.parse(a)) +); const observer = new MutationObserver(mutations => { let domChanged = false; diff --git a/platform/mv3/extension/js/scripting/css-specific.js b/platform/mv3/extension/js/scripting/css-specific.js index c58ebd9ac..661cca616 100644 --- a/platform/mv3/extension/js/scripting/css-specific.js +++ b/platform/mv3/extension/js/scripting/css-specific.js @@ -27,73 +27,41 @@ const specificImports = self.specificImports || []; self.specificImports = undefined; -delete self.specificImports; /******************************************************************************/ -const hnParts = []; -try { hnParts.push(...document.location.hostname.split('.')); } -catch { } -const hnpartslen = hnParts.length; -if ( hnpartslen === 0 ) { return; } - const selectors = []; +const exceptions = []; -for ( const { argsList, exceptionsMap, hostnamesMap, entitiesMap } of specificImports ) { - const todoIndices = new Set(); - const tonotdoIndices = []; - // Exceptions - if ( exceptionsMap.size !== 0 ) { - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = hnParts.slice(i).join('.'); - const excepted = exceptionsMap.get(hn); - if ( excepted ) { tonotdoIndices.push(...excepted); } - } - exceptionsMap.clear(); +const lookupHostname = (hostname, details, out) => { + let seqi = details.hostnamesMap.get(hostname); + if ( seqi === undefined ) { return; } + const { argsList, argsSeqs } = details; + for (;;) { + const argi = argsSeqs[seqi++]; + const done = argi > 0; + out.push(...argsList[done ? argi : -argi].split('\n')); + if ( done ) { break; } } - // Hostname-based - if ( hostnamesMap.size !== 0 ) { - const collectArgIndices = hn => { - let argsIndices = hostnamesMap.get(hn); - if ( argsIndices === undefined ) { return; } - if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } - for ( const argsIndex of argsIndices ) { - if ( tonotdoIndices.includes(argsIndex) ) { continue; } - todoIndices.add(argsIndex); - } - }; - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = hnParts.slice(i).join('.'); - collectArgIndices(hn); - } - collectArgIndices('*'); - hostnamesMap.clear(); +}; + +const lookupAll = hostname => { + for ( const details of specificImports ) { + lookupHostname(hostname, details, selectors); + lookupHostname(`~${hostname}`, details, exceptions); } - // Entity-based - if ( entitiesMap.size !== 0 ) { - const n = hnpartslen - 1; - for ( let i = 0; i < n; i++ ) { - for ( let j = n; j > i; j-- ) { - const en = hnParts.slice(i,j).join('.'); - let argsIndices = entitiesMap.get(en); - if ( argsIndices === undefined ) { continue; } - if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } - for ( const argsIndex of argsIndices ) { - if ( tonotdoIndices.includes(argsIndex) ) { continue; } - todoIndices.add(argsIndex); - } - } - } - entitiesMap.clear(); - } - for ( const i of todoIndices ) { - selectors.push(argsList[i]); - } - argsList.length = 0; -} +}; + +self.isolatedAPI.forEachHostname(lookupAll, { + hasEntities: specificImports.some(a => a.hasEntities) +}); + specificImports.length = 0; -if ( selectors.length === 0 ) { return; } +const exceptedSelectors = exceptions.length !== 0 + ? selectors.filter(a => exceptions.includes(a) === false) + : selectors; +if ( exceptedSelectors.length === 0 ) { return; } /******************************************************************************/ @@ -101,9 +69,9 @@ if ( selectors.length === 0 ) { return; } chrome.runtime.sendMessage({ what: 'insertCSS', css }).catch(( ) => { count -= 1; if ( count === 0 ) { return; } - uBOL_injectCSS(css, count - 1); + uBOL_injectCSS(css, count); }); -})(`${selectors.join(',')}{display:none!important;}`); +})(`${exceptedSelectors.join(',')}{display:none!important;}`); /******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting/isolated-api.js b/platform/mv3/extension/js/scripting/isolated-api.js new file mode 100644 index 000000000..9c867bc41 --- /dev/null +++ b/platform/mv3/extension/js/scripting/isolated-api.js @@ -0,0 +1,80 @@ +/******************************************************************************* + + uBlock Origin Lite - a comprehensive, MV3-compliant content blocker + Copyright (C) 2022-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/ + +*/ + +/******************************************************************************/ + +(api => { + if ( typeof api === 'object' ) { return; } + + const isolatedAPI = self.isolatedAPI = {}; + + const hostnameStack = (( ) => { + const docloc = document.location; + const origins = [ docloc.origin ]; + if ( docloc.ancestorOrigins ) { + origins.push(...docloc.ancestorOrigins); + } + return origins.map((origin, i) => { + const beg = origin.lastIndexOf('://'); + if ( beg === -1 ) { return; } + const hn1 = origin.slice(beg+3) + const end = hn1.indexOf(':'); + const hn2 = end === -1 ? hn1 : hn1.slice(0, end); + return { hnparts: hn2.split('.'), i }; + }).filter(a => a !== undefined); + })(); + + const forEachHostname = (entry, callback, details) => { + const hnparts = entry.hnparts; + const hnpartslen = hnparts.length; + if ( hnpartslen === 0 ) { return; } + for ( let i = 0; i < hnpartslen; i++ ) { + const r = callback(`${hnparts.slice(i).join('.')}`, details); + if ( r !== undefined ) { return r; } + } + if ( details.hasEntities !== true ) { return; } + const n = hnpartslen - 1; + for ( let i = 0; i < n; i++ ) { + for ( let j = n; j > i; j-- ) { + const r = callback(`${hnparts.slice(i,j).join('.')}.*`, details); + if ( r !== undefined ) { return r; } + } + } + }; + + isolatedAPI.forEachHostname = (callback, details) => { + if ( hostnameStack.length === 0 ) { return; } + return forEachHostname(hostnameStack[0], callback, details); + }; + + isolatedAPI.forEachHostnameAncestors = (callback, details) => { + for ( const entry of hostnameStack ) { + if ( entry.i === 0 ) { continue; } + const r = forEachHostname(entry, callback, details); + if ( r !== undefined ) { return r; } + } + }; +})(self.isolatedAPI); + +/******************************************************************************/ + +void 0; \ No newline at end of file diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 9e5c93908..f4e380886 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -675,36 +675,69 @@ function loadAllSourceScriptlets() { /******************************************************************************/ -async function processGenericCosmeticFilters(assetDetails, bucketsMap, exceptionSet) { - if ( bucketsMap === undefined ) { return 0; } - if ( exceptionSet ) { - for ( const [ hash, selectors ] of bucketsMap ) { - let i = selectors.length; - while ( i-- ) { - const selector = selectors[i]; - if ( exceptionSet.has(selector) === false ) { continue; } - selectors.splice(i, 1); - //log(`\tRemoving excepted generic filter ##${selector}`); - } - if ( selectors.length === 0 ) { - bucketsMap.delete(hash); +// http://www.cse.yorku.ca/~oz/hash.html#djb2 +// Must mirror content script surveyor's version + +async function processGenericCosmeticFilters( + assetDetails, + selectorList, + exceptionList, + declarativeMap +) { + const exceptionSet = new Set( + exceptionList && + exceptionList.filter(a => a.key !== undefined).map(a => a.selector) + ); + + const genericSelectorMap = new Map(); + if ( selectorList ) { + for ( const { key, selector } of selectorList ) { + if ( key === undefined ) { continue; } + if ( exceptionSet.has(selector) ) { continue; } + const type = key.charCodeAt(0); + const hash = hashFromStr(type, key.slice(1)); + const selectors = genericSelectorMap.get(hash); + if ( selectors === undefined ) { + genericSelectorMap.set(hash, selector) + } else { + genericSelectorMap.set(hash, `${selectors},${selector}`) } } } - if ( bucketsMap.size === 0 ) { return 0; } - const bucketsList = Array.from(bucketsMap); - const count = bucketsList.reduce((a, v) => a += v[1].length, 0); - if ( count === 0 ) { return 0; } - const selectorLists = bucketsList.map(v => [ v[0], v[1].join(',') ]); - const originalScriptletMap = await loadAllSourceScriptlets(); + // Specific exceptions + const genericExceptionMap = new Map(); + if ( declarativeMap ) { + for ( const details of declarativeMap.values() ) { + if ( details.rejected ) { continue; } + if ( details.key === undefined ) { continue; } + if ( details.matches !== undefined ) { continue; } + if ( details.excludeMatches === undefined ) { continue; } + const type = details.key.charCodeAt(0); + const hash = hashFromStr(type, details.key.slice(1)); + const hostnames = genericExceptionMap.get(hash) ?? ''; + genericExceptionMap.set(hash, + `${hostnames} ${details.excludeMatches.join(' ')} ` + ); + } + } + + if ( genericSelectorMap.size === 0 ) { + if ( genericExceptionMap.size === 0 ) { return 0; } + } + + const originalScriptletMap = await loadAllSourceScriptlets(); let patchedScriptlet = originalScriptletMap.get('css-generic').replace( '$rulesetId$', assetDetails.id ); patchedScriptlet = safeReplace(patchedScriptlet, /\bself\.\$genericSelectorMap\$/, - `${JSON.stringify(selectorLists, scriptletJsonReplacer)}` + `${JSON.stringify(genericSelectorMap, scriptletJsonReplacer)}` + ); + patchedScriptlet = safeReplace(patchedScriptlet, + /\bself\.\$genericExceptionMap\$/, + `${JSON.stringify(genericExceptionMap, scriptletJsonReplacer)}` ); writeFile( @@ -712,24 +745,49 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exception patchedScriptlet ); - log(`CSS-generic: ${count} plain CSS selectors`); + log(`CSS-generic: ${genericSelectorMap.size} plain CSS selectors`); + log(`CSS-generic: ${genericExceptionMap.size} specific CSS exceptions`); - return count; + return genericSelectorMap.size + genericExceptionMap.size; } +const hashFromStr = (type, s) => { + const len = s.length; + const step = len + 7 >>> 3; + let hash = (type << 5) + type ^ len; + for ( let i = 0; i < len; i += step ) { + hash = (hash << 5) + hash ^ s.charCodeAt(i); + } + return hash & 0xFFFFFF; +}; + /******************************************************************************/ -async function processGenericHighCosmeticFilters(assetDetails, selectorSet, exceptionSet) { - if ( selectorSet === undefined ) { return 0; } - if ( exceptionSet ) { - for ( const selector of selectorSet ) { - if ( exceptionSet.has(selector) === false ) { continue; } - selectorSet.delete(selector); - //log(`\tRemoving excepted generic filter ##${selector}`); +async function processGenericHighCosmeticFilters( + assetDetails, + genericSelectorList, + genericExceptionList +) { + if ( genericSelectorList === undefined ) { return 0; } + const genericSelectorSet = new Set( + genericSelectorList + .filter(a => a.key === undefined) + .map(a => a.selector) + ); + if ( genericExceptionList ) { + const genericExceptionSet = new Set( + genericExceptionList + .filter(a => a.key === undefined) + .map(a => a.selector) + ); + for ( const selector of genericExceptionSet ) { + if ( genericSelectorSet.has(selector) === false ) { continue; } + genericSelectorSet.delete(selector); + log(`\tRemoving excepted highly generic filter ##${selector}`); } } - if ( selectorSet.size === 0 ) { return 0; } - const selectorLists = Array.from(selectorSet).sort().join(',\n'); + if ( genericSelectorSet.size === 0 ) { return 0; } + const selectorLists = Array.from(genericSelectorSet).sort().join(',\n'); const originalScriptletMap = await loadAllSourceScriptlets(); let patchedScriptlet = originalScriptletMap.get('css-generichigh').replace( @@ -746,9 +804,9 @@ async function processGenericHighCosmeticFilters(assetDetails, selectorSet, exce patchedScriptlet ); - log(`CSS-generic-high: ${selectorSet.size} plain CSS selectors`); + log(`CSS-generic-high: ${genericSelectorSet.size} plain CSS selectors`); - return selectorSet.size; + return genericSelectorSet.size; } /******************************************************************************/ @@ -846,22 +904,33 @@ const scriptletJsonReplacer = (k, v) => { /******************************************************************************/ function argsMap2List(argsMap, hostnamesMap) { - const argsList = []; + const argsList = [ '' ]; const indexMap = new Map(); for ( const [ id, details ] of argsMap ) { indexMap.set(id, argsList.length); argsList.push(details); } + const argsSeqs = [ 0 ]; + const argsSeqsIndices = new Map(); for ( const [ hn, ids ] of hostnamesMap ) { + const seqKey = JSON.stringify(ids); + if ( argsSeqsIndices.has(seqKey) ) { + hostnamesMap.set(hn, argsSeqsIndices.get(seqKey)); + continue; + } + const seqIndex = argsSeqs.length; + argsSeqsIndices.set(seqKey, seqIndex); + hostnamesMap.set(hn, seqIndex); if ( typeof ids === 'number' ) { - hostnamesMap.set(hn, indexMap.get(ids)); + argsSeqs.push(indexMap.get(ids)); continue; } for ( let i = 0; i < ids.length; i++ ) { - ids[i] = indexMap.get(ids[i]); + argsSeqs.push(-indexMap.get(ids[i])); } + argsSeqs[argsSeqs.length-1] = -argsSeqs[argsSeqs.length-1]; } - return argsList; + return { argsList, argsSeqs }; } /******************************************************************************/ @@ -885,38 +954,21 @@ async function processCosmeticFilters(assetDetails, mapin) { const argsMap = domainBasedEntries.map(entry => [ entry[0], - { - a: entry[1].a ? entry[1].a.join(',\n') : undefined, - n: entry[1].n - } + entry[1].a ? entry[1].a.join('\n') : undefined, ]); const hostnamesMap = new Map(); + let hasEntities = false; for ( const [ id, details ] of domainBasedEntries ) { - if ( details.y === undefined ) { continue; } - scriptletHostnameToIdMap(details.y, id, hostnamesMap); - } - const argsList = argsMap2List(argsMap, hostnamesMap); - const entitiesMap = new Map(); - for ( const [ hn, details ] of hostnamesMap ) { - if ( hn.endsWith('.*') === false ) { continue; } - hostnamesMap.delete(hn); - entitiesMap.set(hn.slice(0, -2), details); - } - - // Extract exceptions from argsList, simplify argsList entries - const exceptionsMap = new Map(); - for ( let i = 0; i < argsList.length; i++ ) { - const details = argsList[i]; - if ( details.n ) { - for ( const hn of details.n ) { - if ( exceptionsMap.has(hn) === false ) { - exceptionsMap.set(hn, []); - } - exceptionsMap.get(hn).push(i); - } + if ( details.y ) { + scriptletHostnameToIdMap(details.y, id, hostnamesMap); + hasEntities ||= details.y.some(a => a.endsWith('.*')); + } + if ( details.n ) { + scriptletHostnameToIdMap(details.n.map(a => `~${a}`), id, hostnamesMap); + hasEntities ||= details.n.some(a => a.endsWith('.*')); } - argsList[i] = details.a; } + const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap); const originalScriptletMap = await loadAllSourceScriptlets(); let patchedScriptlet = originalScriptletMap.get('css-specific').replace( @@ -927,17 +979,17 @@ async function processCosmeticFilters(assetDetails, mapin) { /\bself\.\$argsList\$/, `${JSON.stringify(argsList, scriptletJsonReplacer)}` ); + patchedScriptlet = safeReplace(patchedScriptlet, + /\bself\.\$argsSeqs\$/, + `${JSON.stringify(argsSeqs, scriptletJsonReplacer)}` + ); patchedScriptlet = safeReplace(patchedScriptlet, /\bself\.\$hostnamesMap\$/, `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$entitiesMap\$/, - `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}` - ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$exceptionsMap\$/, - `${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}` + 'self.$hasEntities$', + JSON.stringify(hasEntities) ); writeFile(`${scriptletDir}/specific/${assetDetails.id}.js`, patchedScriptlet); generatedFiles.push(`${assetDetails.id}`); @@ -945,10 +997,9 @@ async function processCosmeticFilters(assetDetails, mapin) { if ( generatedFiles.length !== 0 ) { log(`CSS-specific: ${mapin.size} distinct filters`); log(`\tCombined into ${hostnamesMap.size} distinct hostnames`); - log(`\tCombined into ${entitiesMap.size} distinct entities`); } - return hostnamesMap.size + entitiesMap.size; + return hostnamesMap.size; } /******************************************************************************/ @@ -973,38 +1024,21 @@ async function processDeclarativeCosmeticFilters(assetDetails, mapin) { const argsMap = contentArray.map(entry => [ entry[0], - { - a: entry[1].a, - n: entry[1].n, - } + entry[1].a ? entry[1].a.join('\n') : undefined, ]); const hostnamesMap = new Map(); + let hasEntities = false; for ( const [ id, details ] of contentArray ) { - if ( details.y === undefined ) { continue; } - scriptletHostnameToIdMap(details.y, id, hostnamesMap); - } - const argsList = argsMap2List(argsMap, hostnamesMap); - const entitiesMap = new Map(); - for ( const [ hn, details ] of hostnamesMap ) { - if ( hn.endsWith('.*') === false ) { continue; } - hostnamesMap.delete(hn); - entitiesMap.set(hn.slice(0, -2), details); - } - - // Extract exceptions from argsList, simplify argsList entries - const exceptionsMap = new Map(); - for ( let i = 0; i < argsList.length; i++ ) { - const details = argsList[i]; - if ( details.n ) { - for ( const hn of details.n ) { - if ( exceptionsMap.has(hn) === false ) { - exceptionsMap.set(hn, []); - } - exceptionsMap.get(hn).push(i); - } + if ( details.y ) { + scriptletHostnameToIdMap(details.y, id, hostnamesMap); + hasEntities ||= details.y.some(a => a.endsWith('.*')); + } + if ( details.n ) { + scriptletHostnameToIdMap(details.n.map(a => `~${a}`), id, hostnamesMap); + hasEntities ||= details.n.some(a => a.endsWith('.*')); } - argsList[i] = details.a; } + const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap); const originalScriptletMap = await loadAllSourceScriptlets(); let patchedScriptlet = originalScriptletMap.get('css-declarative').replace( @@ -1015,27 +1049,26 @@ async function processDeclarativeCosmeticFilters(assetDetails, mapin) { /\bself\.\$argsList\$/, `${JSON.stringify(argsList, scriptletJsonReplacer)}` ); + patchedScriptlet = safeReplace(patchedScriptlet, + /\bself\.\$argsSeqs\$/, + `${JSON.stringify(argsSeqs, scriptletJsonReplacer)}` + ); patchedScriptlet = safeReplace(patchedScriptlet, /\bself\.\$hostnamesMap\$/, `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$entitiesMap\$/, - `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}` - ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$exceptionsMap\$/, - `${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}` + 'self.$hasEntities$', + JSON.stringify(hasEntities) ); writeFile(`${scriptletDir}/declarative/${assetDetails.id}.js`, patchedScriptlet); if ( contentArray.length !== 0 ) { log(`CSS-declarative: ${declaratives.size} distinct filters`); log(`\tCombined into ${hostnamesMap.size} distinct hostnames`); - log(`\tCombined into ${entitiesMap.size} distinct entities`); } - return hostnamesMap.size + entitiesMap.size; + return hostnamesMap.size; } /******************************************************************************/ @@ -1059,39 +1092,21 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) { const argsMap = contentArray.map(entry => [ entry[0], - { - a: entry[1].a, - n: entry[1].n, - } + entry[1].a, ]); const hostnamesMap = new Map(); + let hasEntities = false; for ( const [ id, details ] of contentArray ) { - if ( details.y === undefined ) { continue; } - scriptletHostnameToIdMap(details.y, id, hostnamesMap); - } - const argsList = argsMap2List(argsMap, hostnamesMap); - const entitiesMap = new Map(); - for ( const [ hn, details ] of hostnamesMap ) { - if ( hn.endsWith('.*') === false ) { continue; } - hostnamesMap.delete(hn); - entitiesMap.set(hn.slice(0, -2), details); - } - - // Extract exceptions from argsList, simplify argsList entries - const exceptionsMap = new Map(); - for ( let i = 0; i < argsList.length; i++ ) { - const details = argsList[i]; - if ( details.n ) { - for ( const hn of details.n ) { - if ( exceptionsMap.has(hn) === false ) { - exceptionsMap.set(hn, []); - } - exceptionsMap.get(hn).push(i); - } + if ( details.y ) { + scriptletHostnameToIdMap(details.y, id, hostnamesMap); + hasEntities ||= details.y.some(a => a.endsWith('.*')); + } + if ( details.n ) { + scriptletHostnameToIdMap(details.n.map(a => `~${a}`), id, hostnamesMap); + hasEntities ||= details.n.some(a => a.endsWith('.*')); } - argsList[i] = details.a; } - + const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap); const originalScriptletMap = await loadAllSourceScriptlets(); let patchedScriptlet = originalScriptletMap.get('css-procedural').replace( '$rulesetId$', @@ -1101,27 +1116,26 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) { /\bself\.\$argsList\$/, `${JSON.stringify(argsList, scriptletJsonReplacer)}` ); + patchedScriptlet = safeReplace(patchedScriptlet, + /\bself\.\$argsSeqs\$/, + `${JSON.stringify(argsSeqs, scriptletJsonReplacer)}` + ); patchedScriptlet = safeReplace(patchedScriptlet, /\bself\.\$hostnamesMap\$/, `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$entitiesMap\$/, - `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}` - ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$exceptionsMap\$/, - `${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}` + 'self.$hasEntities$', + JSON.stringify(hasEntities) ); writeFile(`${scriptletDir}/procedural/${assetDetails.id}.js`, patchedScriptlet); if ( contentArray.length !== 0 ) { log(`Procedural-related distinct filters: ${procedurals.size} distinct combined selectors`); log(`\tCombined into ${hostnamesMap.size} distinct hostnames`); - log(`\tCombined into ${entitiesMap.size} distinct entities`); } - return hostnamesMap.size + entitiesMap.size; + return hostnamesMap.size; } /******************************************************************************/ @@ -1233,13 +1247,14 @@ async function rulesetFromURLs(assetDetails) { const genericCosmeticStats = await processGenericCosmeticFilters( assetDetails, - results.genericCosmetic, - results.genericCosmeticExceptions + results.genericCosmeticFilters, + results.genericCosmeticExceptions, + declarativeCosmetic ); const genericHighCosmeticStats = await processGenericHighCosmeticFilters( assetDetails, - results.genericHighCosmetic, - results.genericCosmeticExceptions + results.genericCosmeticFilters, + results.genericCosmeticExceptions, ); const specificCosmeticStats = await processCosmeticFilters( assetDetails, diff --git a/platform/mv3/scriptlets/css-declarative.template.js b/platform/mv3/scriptlets/css-declarative.template.js index a4ac2a280..f7d467600 100644 --- a/platform/mv3/scriptlets/css-declarative.template.js +++ b/platform/mv3/scriptlets/css-declarative.template.js @@ -28,15 +28,12 @@ /******************************************************************************/ const argsList = self.$argsList$; - +const argsSeqs = self.$argsSeqs$; const hostnamesMap = new Map(self.$hostnamesMap$); - -const entitiesMap = new Map(self.$entitiesMap$); - -const exceptionsMap = new Map(self.$exceptionsMap$); +const hasEntities = self.$hasEntities$; self.declarativeImports = self.declarativeImports || []; -self.declarativeImports.push({ argsList, hostnamesMap, entitiesMap, exceptionsMap }); +self.declarativeImports.push({ argsList, argsSeqs, hostnamesMap, hasEntities }); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-generic.template.js b/platform/mv3/scriptlets/css-generic.template.js index 5c06ead0c..4b535e981 100644 --- a/platform/mv3/scriptlets/css-generic.template.js +++ b/platform/mv3/scriptlets/css-generic.template.js @@ -27,26 +27,39 @@ /******************************************************************************/ -const toImport = self.$genericSelectorMap$; +const selectorsToImport = self.$genericSelectorMap$; +const exceptionsToImport = self.$genericExceptionMap$; -const genericSelectorMap = self.genericSelectorMap || new Map(); +if ( selectorsToImport ) { + const map = self.genericSelectorMap = + self.genericSelectorMap || new Map(); -if ( genericSelectorMap.size === 0 ) { - self.genericSelectorMap = new Map(toImport); - return; + if ( map.size !== 0 ) { + for ( const entry of selectorsToImport ) { + const before = map.get(entry[0]); + map.set(entry[0], + before === undefined ? entry[1] : `${before},${entry[1]}` + ); + } + } else { + self.genericSelectorMap = new Map(selectorsToImport); + } + selectorsToImport.length = 0; } -for ( const toImportEntry of toImport ) { - const existing = genericSelectorMap.get(toImportEntry[0]); - genericSelectorMap.set( - toImportEntry[0], - existing === undefined - ? toImportEntry[1] - : `${existing},${toImportEntry[1]}` - ); -} +if ( exceptionsToImport ) { + const map = self.genericExceptionMap = + self.genericExceptionMap || new Map(); -self.genericSelectorMap = genericSelectorMap; + if ( map.size !== 0 ) { + for ( const entry of exceptionsToImport ) { + map.set(entry[0], `${map.get(entry[0]) || ''}${entry[1]}`); + } + } else { + self.genericExceptionMap = new Map(exceptionsToImport); + } + exceptionsToImport.length = 0; +} /******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-procedural.template.js b/platform/mv3/scriptlets/css-procedural.template.js index f9f5682d5..1928abdd4 100644 --- a/platform/mv3/scriptlets/css-procedural.template.js +++ b/platform/mv3/scriptlets/css-procedural.template.js @@ -28,15 +28,12 @@ /******************************************************************************/ const argsList = self.$argsList$; - +const argsSeqs = self.$argsSeqs$; const hostnamesMap = new Map(self.$hostnamesMap$); - -const entitiesMap = new Map(self.$entitiesMap$); - -const exceptionsMap = new Map(self.$exceptionsMap$); +const hasEntities = self.$hasEntities$; self.proceduralImports = self.proceduralImports || []; -self.proceduralImports.push({ argsList, hostnamesMap, entitiesMap, exceptionsMap }); +self.proceduralImports.push({ argsList, argsSeqs, hostnamesMap, hasEntities }); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-specific.template.js b/platform/mv3/scriptlets/css-specific.template.js index e164e2e58..f4c93cdf0 100644 --- a/platform/mv3/scriptlets/css-specific.template.js +++ b/platform/mv3/scriptlets/css-specific.template.js @@ -28,15 +28,12 @@ /******************************************************************************/ const argsList = self.$argsList$; - +const argsSeqs = self.$argsSeqs$; const hostnamesMap = new Map(self.$hostnamesMap$); - -const entitiesMap = new Map(self.$entitiesMap$); - -const exceptionsMap = new Map(self.$exceptionsMap$); +const hasEntities = self.$hasEntities$; self.specificImports = self.specificImports || []; -self.specificImports.push({ argsList, hostnamesMap, entitiesMap, exceptionsMap }); +self.specificImports.push({ argsList, argsSeqs, hostnamesMap, hasEntities }); /******************************************************************************/ diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index 2e75633eb..3f8081c7e 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -31,19 +31,6 @@ import staticNetFilteringEngine from './static-net-filtering.js'; /******************************************************************************/ -// http://www.cse.yorku.ca/~oz/hash.html#djb2 -// Must mirror content script surveyor's version - -const hashFromStr = (type, s) => { - const len = s.length; - const step = len + 7 >>> 3; - let hash = (type << 5) + type ^ len; - for ( let i = 0; i < len; i += step ) { - hash = (hash << 5) + hash ^ s.charCodeAt(i); - } - return hash & 0xFFFFFF; -}; - const isRegex = hn => hn.startsWith('/') && hn.endsWith('/'); /******************************************************************************/ @@ -93,32 +80,19 @@ const keyFromSelector = selector => { function addGenericCosmeticFilter(context, selector, isException) { if ( selector === undefined ) { return; } if ( selector.length <= 1 ) { return; } - if ( isException ) { - if ( context.genericCosmeticExceptions === undefined ) { - context.genericCosmeticExceptions = new Set(); - } - context.genericCosmeticExceptions.add(selector); - return; - } if ( selector.charCodeAt(0) === 0x7B /* '{' */ ) { return; } const key = keyFromSelector(selector); - if ( key === undefined ) { - if ( context.genericHighCosmeticFilters === undefined ) { - context.genericHighCosmeticFilters = new Set(); + if ( isException ) { + if ( context.genericCosmeticExceptions === undefined ) { + context.genericCosmeticExceptions = []; } - context.genericHighCosmeticFilters.add(selector); + context.genericCosmeticExceptions.push({ key, selector }); return; } - const type = key.charCodeAt(0); - const hash = hashFromStr(type, key.slice(1)); if ( context.genericCosmeticFilters === undefined ) { - context.genericCosmeticFilters = new Map(); + context.genericCosmeticFilters = []; } - let bucket = context.genericCosmeticFilters.get(hash); - if ( bucket === undefined ) { - context.genericCosmeticFilters.set(hash, bucket = []); - } - bucket.push(selector); + context.genericCosmeticFilters.push({ key, selector }); } /******************************************************************************/ @@ -255,7 +229,10 @@ function addExtendedToDNR(context, parser) { if ( details === undefined ) { context.specificCosmeticFilters.set(compiled, details = {}); } - if ( exception ) { + if ( compiled.startsWith('{') === false ) { + details.key = keyFromSelector(compiled); + } + if ( exception || not ) { if ( details.excludeMatches === undefined ) { details.excludeMatches = []; } @@ -497,8 +474,7 @@ async function dnrRulesetFromRawLists(lists, options = {}) { await Promise.all(toLoad); const result = { network: staticNetFilteringEngine.dnrFromCompiled('end', context), - genericCosmetic: context.genericCosmeticFilters, - genericHighCosmetic: context.genericHighCosmeticFilters, + genericCosmeticFilters: context.genericCosmeticFilters, genericCosmeticExceptions: context.genericCosmeticExceptions, specificCosmetic: context.specificCosmeticFilters, scriptlet: context.scriptletFilters,