[mv3] Improve generic cosmetic filtering

Specifically, properly exclude generic cosmetic filters according to
specific cosmetic exceptions.

Related issue:
https://github.com/uBlockOrigin/uBOL-home/issues/181
This commit is contained in:
Raymond Hill 2025-03-13 13:15:27 -04:00
parent 22fdf8fc1d
commit a009623d97
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
12 changed files with 413 additions and 414 deletions

View file

@ -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 = [];

View file

@ -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'));

View file

@ -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}`);
};
/******************************************************************************/
})();
/******************************************************************************/

View file

@ -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;

View file

@ -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;}`);
/******************************************************************************/

View file

@ -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;

View file

@ -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,

View file

@ -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 });
/******************************************************************************/

View file

@ -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;
}
/******************************************************************************/

View file

@ -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 });
/******************************************************************************/

View file

@ -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 });
/******************************************************************************/

View file

@ -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,