2022-09-06 11:47:52 -06:00
|
|
|
/*******************************************************************************
|
|
|
|
|
|
|
|
uBlock Origin - a browser extension to block requests.
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
import fs from 'fs/promises';
|
2022-09-06 13:05:01 -06:00
|
|
|
import https from 'https';
|
2022-09-15 11:14:08 -06:00
|
|
|
import path from 'path';
|
2022-09-06 11:47:52 -06:00
|
|
|
import process from 'process';
|
2022-09-15 11:14:08 -06:00
|
|
|
import { createHash } from 'crypto';
|
2022-09-06 11:47:52 -06:00
|
|
|
import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js';
|
2022-09-13 15:44:24 -06:00
|
|
|
import { StaticFilteringParser } from './js/static-filtering-parser.js';
|
2022-09-20 06:24:01 -06:00
|
|
|
import { fnameFromFileId } from './js/utils.js';
|
2022-09-06 11:47:52 -06:00
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
const commandLineArgs = (( ) => {
|
|
|
|
const args = new Map();
|
|
|
|
let name, value;
|
|
|
|
for ( const arg of process.argv.slice(2) ) {
|
|
|
|
const pos = arg.indexOf('=');
|
|
|
|
if ( pos === -1 ) {
|
|
|
|
name = arg;
|
|
|
|
value = '';
|
|
|
|
} else {
|
|
|
|
name = arg.slice(0, pos);
|
|
|
|
value = arg.slice(pos+1);
|
|
|
|
}
|
|
|
|
args.set(name, value);
|
|
|
|
}
|
|
|
|
return args;
|
|
|
|
})();
|
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const outputDir = commandLineArgs.get('output') || '.';
|
|
|
|
const cacheDir = `${outputDir}/../mv3-data`;
|
|
|
|
const rulesetDir = `${outputDir}/rulesets`;
|
2022-09-18 07:31:44 -06:00
|
|
|
const scriptletDir = `${rulesetDir}/js`;
|
New cosmetic filter parser using CSSTree library
The new parser no longer uses the browser DOM to validate
that a cosmetic filter is valid or not, this is now done
through a JS library, CSSTree.
This means filter list authors will have to be more careful
to ensure that a cosmetic filter is really valid, as there is
no more guarantee that a cosmetic filter which works for a
given browser/version will still work properly on another
browser, or different version of the same browser.
This change has become necessary because of many reasons,
one of them being the flakiness of the previous parser as
exposed by many issues lately:
- https://github.com/uBlockOrigin/uBlock-issues/issues/2262
- https://github.com/uBlockOrigin/uBlock-issues/issues/2228
The new parser introduces breaking changes, there was no way
to do otherwise. Some current procedural cosmetic filters will
be shown as invalid with this change. This occurs because the
CSSTree library gets confused with some syntax which was
previously allowed by the previous parser because it was more
permissive.
Mainly the issue is with the arguments passed to some procedural
cosmetic filters, and these issues can be solved as follow:
Use quotes around the argument. You can use either single or
double-quotes, whichever is most convenient. If your argument
contains a single quote, use double-quotes, and vice versa.
Additionally, try to escape a quote inside an argument using
backslash. THis may work, but if not, use quotes around the
argument.
When the parser encounter quotes around an argument, it will
discard them before trying to process the argument, same with
escaped quotes inside the argument. Examples:
Breakage:
...##^script:has-text(toscr')
Fix:
...##^script:has-text(toscr\')
Breakage:
...##:xpath(//*[contains(text(),"VPN")]):upward(2)
Fix:
...##:xpath('//*[contains(text(),"VPN")]'):upward(2)
There are not many filters which break in the default set of
filter lists, so this should be workable for default lists.
Unfortunately those fixes will break the filter for previous
versions of uBO since these to not deal with quoted argument.
In such case, it may be necessary to keep the previous filter,
which will be discarded as broken on newer version of uBO.
THis was a necessary change as the old parser was becoming
more and more flaky after being constantly patched for new
cases arising, The new parser should be far more robust and
stay robist through expanding procedural cosmetic filter
syntax.
Additionally, in the MV3 version, filters are pre-compiled
using a Nodejs script, i.e. outside the browser, so validating
cosmetic filters using a live DOM no longer made sense.
This new parser will have to be tested throughly before stable
release.
2022-09-23 14:03:13 -06:00
|
|
|
const env = [ 'chromium', 'ubol', 'native_css_has' ];
|
2022-09-16 13:56:35 -06:00
|
|
|
|
2022-09-06 11:47:52 -06:00
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-18 15:07:02 -06:00
|
|
|
const jsonSetMapReplacer = (k, v) => {
|
|
|
|
if ( v instanceof Set || v instanceof Map ) {
|
|
|
|
if ( v.size === 0 ) { return; }
|
|
|
|
return Array.from(v);
|
|
|
|
}
|
|
|
|
return v;
|
|
|
|
};
|
|
|
|
|
2022-09-19 18:19:55 -06:00
|
|
|
const uidint32 = (s) => {
|
|
|
|
const h = createHash('sha256').update(s).digest('hex').slice(0,8);
|
|
|
|
return parseInt(h,16) & 0x7FFFFFFF;
|
|
|
|
};
|
2022-09-18 15:07:02 -06:00
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-13 15:44:24 -06:00
|
|
|
const isUnsupported = rule =>
|
|
|
|
rule._error !== undefined;
|
|
|
|
|
|
|
|
const isRegex = rule =>
|
|
|
|
rule.condition !== undefined &&
|
|
|
|
rule.condition.regexFilter !== undefined;
|
|
|
|
|
|
|
|
const isRedirect = rule =>
|
|
|
|
rule.action !== undefined &&
|
|
|
|
rule.action.type === 'redirect' &&
|
|
|
|
rule.action.redirect.extensionPath !== undefined;
|
|
|
|
|
|
|
|
const isCsp = rule =>
|
|
|
|
rule.action !== undefined &&
|
|
|
|
rule.action.type === 'modifyHeaders';
|
|
|
|
|
|
|
|
const isRemoveparam = rule =>
|
|
|
|
rule.action !== undefined &&
|
|
|
|
rule.action.type === 'redirect' &&
|
|
|
|
rule.action.redirect.transform !== undefined;
|
|
|
|
|
|
|
|
const isGood = rule =>
|
|
|
|
isUnsupported(rule) === false &&
|
|
|
|
isRedirect(rule) === false &&
|
|
|
|
isCsp(rule) === false &&
|
|
|
|
isRemoveparam(rule) === false;
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
const stdOutput = [];
|
|
|
|
|
|
|
|
const log = (text, silent = false) => {
|
|
|
|
stdOutput.push(text);
|
|
|
|
if ( silent === false ) {
|
|
|
|
console.log(text);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-15 11:14:08 -06:00
|
|
|
const urlToFileName = url => {
|
|
|
|
return url
|
|
|
|
.replace(/^https?:\/\//, '')
|
|
|
|
.replace(/\//g, '_')
|
|
|
|
;
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchList = (url, cacheDir) => {
|
2022-09-13 15:44:24 -06:00
|
|
|
return new Promise((resolve, reject) => {
|
2022-09-15 11:14:08 -06:00
|
|
|
const fname = urlToFileName(url);
|
|
|
|
fs.readFile(`${cacheDir}/${fname}`, { encoding: 'utf8' }).then(content => {
|
|
|
|
log(`\tFetched local ${url}`);
|
|
|
|
resolve({ url, content });
|
|
|
|
}).catch(( ) => {
|
|
|
|
log(`\tFetching remote ${url}`);
|
|
|
|
https.get(url, response => {
|
|
|
|
const data = [];
|
|
|
|
response.on('data', chunk => {
|
|
|
|
data.push(chunk.toString());
|
|
|
|
});
|
|
|
|
response.on('end', ( ) => {
|
|
|
|
const content = data.join('');
|
|
|
|
try {
|
|
|
|
writeFile(`${cacheDir}/${fname}`, content);
|
|
|
|
} catch (ex) {
|
|
|
|
}
|
|
|
|
resolve({ url, content });
|
|
|
|
});
|
|
|
|
}).on('error', error => {
|
|
|
|
reject(error);
|
2022-09-13 15:44:24 -06:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-15 11:14:08 -06:00
|
|
|
const writeFile = async (fname, data) => {
|
|
|
|
const dir = path.dirname(fname);
|
|
|
|
await fs.mkdir(dir, { recursive: true });
|
2022-09-16 13:56:35 -06:00
|
|
|
const promise = fs.writeFile(fname, data);
|
|
|
|
writeOps.push(promise);
|
|
|
|
return promise;
|
2022-09-15 11:14:08 -06:00
|
|
|
};
|
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const writeOps = [];
|
2022-09-15 11:14:08 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
/******************************************************************************/
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const ruleResources = [];
|
|
|
|
const rulesetDetails = [];
|
2022-09-18 07:31:44 -06:00
|
|
|
const scriptingDetails = new Map();
|
2022-09-13 15:44:24 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
/******************************************************************************/
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
async function fetchAsset(assetDetails) {
|
|
|
|
// Remember fetched URLs
|
|
|
|
const fetchedURLs = new Set();
|
|
|
|
|
|
|
|
// Fetch list and expand `!#include` directives
|
|
|
|
let parts = assetDetails.urls.map(url => ({ url }));
|
|
|
|
while ( parts.every(v => typeof v === 'string') === false ) {
|
|
|
|
const newParts = [];
|
|
|
|
for ( const part of parts ) {
|
|
|
|
if ( typeof part === 'string' ) {
|
|
|
|
newParts.push(part);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( fetchedURLs.has(part.url) ) {
|
|
|
|
newParts.push('');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
fetchedURLs.add(part.url);
|
|
|
|
newParts.push(
|
|
|
|
fetchList(part.url, cacheDir).then(details => {
|
|
|
|
const { url } = details;
|
|
|
|
const content = details.content.trim();
|
|
|
|
if ( typeof content === 'string' && content !== '' ) {
|
|
|
|
if (
|
|
|
|
content.startsWith('<') === false ||
|
|
|
|
content.endsWith('>') === false
|
|
|
|
) {
|
|
|
|
return { url, content };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log(`No valid content for ${details.name}`);
|
|
|
|
return { url, content: '' };
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
parts = await Promise.all(newParts);
|
|
|
|
parts = StaticFilteringParser.utils.preparser.expandIncludes(parts, env);
|
|
|
|
}
|
|
|
|
const text = parts.join('\n');
|
2022-09-10 12:20:07 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
if ( text === '' ) {
|
|
|
|
log('No filterset found');
|
2022-09-10 12:20:07 -06:00
|
|
|
}
|
2022-09-16 13:56:35 -06:00
|
|
|
return text;
|
|
|
|
}
|
2022-09-10 12:20:07 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
/******************************************************************************/
|
2022-09-10 12:20:07 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
async function processNetworkFilters(assetDetails, network) {
|
2022-09-06 11:47:52 -06:00
|
|
|
const replacer = (k, v) => {
|
|
|
|
if ( k.startsWith('__') ) { return; }
|
|
|
|
if ( Array.isArray(v) ) {
|
|
|
|
return v.sort();
|
|
|
|
}
|
|
|
|
if ( v instanceof Object ) {
|
|
|
|
const sorted = {};
|
|
|
|
for ( const kk of Object.keys(v).sort() ) {
|
|
|
|
sorted[kk] = v[kk];
|
|
|
|
}
|
|
|
|
return sorted;
|
|
|
|
}
|
|
|
|
return v;
|
|
|
|
};
|
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const { ruleset: rules } = network;
|
|
|
|
log(`Input filter count: ${network.filterCount}`);
|
|
|
|
log(`\tAccepted filter count: ${network.acceptedFilterCount}`);
|
|
|
|
log(`\tRejected filter count: ${network.rejectedFilterCount}`);
|
|
|
|
log(`Output rule count: ${rules.length}`);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const good = rules.filter(rule => isGood(rule) && isRegex(rule) === false);
|
|
|
|
log(`\tGood: ${good.length}`);
|
2022-09-13 15:44:24 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const regexes = rules.filter(rule => isGood(rule) && isRegex(rule));
|
|
|
|
log(`\tMaybe good (regexes): ${regexes.length}`);
|
2022-09-13 15:44:24 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const redirects = rules.filter(rule =>
|
|
|
|
isUnsupported(rule) === false &&
|
|
|
|
isRedirect(rule)
|
|
|
|
);
|
|
|
|
log(`\tredirect-rule= (discarded): ${redirects.length}`);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const headers = rules.filter(rule =>
|
|
|
|
isUnsupported(rule) === false &&
|
|
|
|
isCsp(rule)
|
|
|
|
);
|
|
|
|
log(`\tcsp= (discarded): ${headers.length}`);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const removeparams = rules.filter(rule =>
|
|
|
|
isUnsupported(rule) === false &&
|
|
|
|
isRemoveparam(rule)
|
|
|
|
);
|
|
|
|
log(`\tremoveparams= (discarded): ${removeparams.length}`);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
const bad = rules.filter(rule =>
|
|
|
|
isUnsupported(rule)
|
|
|
|
);
|
|
|
|
log(`\tUnsupported: ${bad.length}`);
|
|
|
|
log(
|
|
|
|
bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'),
|
|
|
|
true
|
|
|
|
);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
writeFile(
|
|
|
|
`${rulesetDir}/${assetDetails.id}.json`,
|
|
|
|
`${JSON.stringify(good, replacer)}\n`
|
|
|
|
);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
if ( regexes.length !== 0 ) {
|
|
|
|
writeFile(
|
|
|
|
`${rulesetDir}/${assetDetails.id}.regexes.json`,
|
|
|
|
`${JSON.stringify(regexes, replacer)}\n`
|
2022-09-06 11:47:52 -06:00
|
|
|
);
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
return {
|
|
|
|
total: rules.length,
|
|
|
|
accepted: good.length,
|
|
|
|
discarded: redirects.length + headers.length + removeparams.length,
|
|
|
|
rejected: bad.length,
|
|
|
|
regexes: regexes.length,
|
|
|
|
};
|
|
|
|
}
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-19 06:55:45 -06:00
|
|
|
// TODO: unify css/scriptlet processing code since now css styles are
|
|
|
|
// injected using scriptlet injection.
|
|
|
|
|
|
|
|
// Load all available scriptlets into a key-val map, where the key is the
|
|
|
|
// scriptlet token, and val is the whole content of the file.
|
|
|
|
|
|
|
|
const scriptletDealiasingMap = new Map();
|
|
|
|
let scriptletsMapPromise;
|
|
|
|
|
|
|
|
function loadAllSourceScriptlets() {
|
|
|
|
if ( scriptletsMapPromise !== undefined ) {
|
|
|
|
return scriptletsMapPromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
scriptletsMapPromise = fs.readdir('./scriptlets').then(files => {
|
|
|
|
const reScriptletNameOrAlias = /^\/\/\/\s+(?:name|alias)\s+(\S+)/gm;
|
|
|
|
const readPromises = [];
|
|
|
|
for ( const file of files ) {
|
|
|
|
readPromises.push(
|
|
|
|
fs.readFile(`./scriptlets/${file}`, { encoding: 'utf8' })
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return Promise.all(readPromises).then(results => {
|
|
|
|
const originalScriptletMap = new Map();
|
|
|
|
for ( const text of results ) {
|
|
|
|
const aliasSet = new Set();
|
|
|
|
for (;;) {
|
|
|
|
const match = reScriptletNameOrAlias.exec(text);
|
|
|
|
if ( match === null ) { break; }
|
|
|
|
aliasSet.add(match[1]);
|
|
|
|
}
|
|
|
|
if ( aliasSet.size === 0 ) { continue; }
|
|
|
|
const aliases = Array.from(aliasSet);
|
|
|
|
originalScriptletMap.set(aliases[0], text);
|
|
|
|
for ( let i = 0; i < aliases.length; i++ ) {
|
|
|
|
scriptletDealiasingMap.set(aliases[i], aliases[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return originalScriptletMap;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return scriptletsMapPromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
const globalPatchedScriptletsSet = new Set();
|
|
|
|
|
2022-09-19 18:19:55 -06:00
|
|
|
function addScriptingAPIResources(id, entry, prop, fid) {
|
2022-09-18 07:31:44 -06:00
|
|
|
if ( entry[prop] === undefined ) { return; }
|
|
|
|
for ( const hn of entry[prop] ) {
|
|
|
|
let details = scriptingDetails.get(id);
|
|
|
|
if ( details === undefined ) {
|
2022-09-20 06:24:01 -06:00
|
|
|
details = {};
|
2022-09-18 07:31:44 -06:00
|
|
|
scriptingDetails.set(id, details);
|
|
|
|
}
|
2022-09-20 06:24:01 -06:00
|
|
|
if ( details[prop] === undefined ) {
|
|
|
|
details[prop] = new Map();
|
|
|
|
}
|
2022-09-19 18:19:55 -06:00
|
|
|
let fids = details[prop].get(hn);
|
|
|
|
if ( fids === undefined ) {
|
2022-09-20 06:24:01 -06:00
|
|
|
details[prop].set(hn, fid);
|
|
|
|
} else if ( fids instanceof Set ) {
|
|
|
|
fids.add(fid);
|
|
|
|
} else if ( fid !== fids ) {
|
|
|
|
fids = new Set([ fids, fid ]);
|
2022-09-19 18:19:55 -06:00
|
|
|
details[prop].set(hn, fids);
|
2022-09-18 07:31:44 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
const toCSSFileId = s => (uidint32(s) & ~0b11) | 0b00;
|
|
|
|
const toJSFileId = s => (uidint32(s) & ~0b11) | 0b01;
|
|
|
|
const toProceduralFileId = s => (uidint32(s) & ~0b11) | 0b10;
|
|
|
|
|
2022-09-20 06:24:01 -06:00
|
|
|
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`;
|
2022-09-19 18:19:55 -06:00
|
|
|
|
2022-09-18 07:31:44 -06:00
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
const COSMETIC_FILES_PER_RULESET = 12;
|
|
|
|
const PROCEDURAL_FILES_PER_RULESET = 4;
|
2022-09-16 13:56:35 -06:00
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
// This merges selectors which are used by the same hostnames
|
|
|
|
|
|
|
|
function groupCosmeticByHostnames(mapin) {
|
|
|
|
if ( mapin === undefined ) { return []; }
|
|
|
|
const merged = new Map();
|
|
|
|
for ( const [ selector, details ] of mapin ) {
|
|
|
|
const json = JSON.stringify(details);
|
|
|
|
let entries = merged.get(json);
|
|
|
|
if ( entries === undefined ) {
|
|
|
|
entries = new Set();
|
|
|
|
merged.set(json, entries);
|
2022-09-18 15:07:02 -06:00
|
|
|
}
|
2022-09-24 09:33:04 -06:00
|
|
|
entries.add(selector);
|
|
|
|
}
|
|
|
|
const out = [];
|
|
|
|
for ( const [ json, entries ] of merged ) {
|
|
|
|
const details = JSON.parse(json);
|
|
|
|
details.selectors = Array.from(entries).sort();
|
|
|
|
out.push(details);
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This merges hostnames which have the same set of selectors.
|
|
|
|
//
|
|
|
|
// Also, we sort the hostnames to increase likelihood that selector with
|
|
|
|
// same hostnames will end up in same generated scriptlet.
|
|
|
|
|
|
|
|
function groupCosmeticBySelectors(arrayin) {
|
|
|
|
const contentMap = new Map();
|
|
|
|
for ( const entry of arrayin ) {
|
|
|
|
const id = uidint32(JSON.stringify(entry.selectors));
|
|
|
|
let details = contentMap.get(id);
|
2022-09-19 06:55:45 -06:00
|
|
|
if ( details === undefined ) {
|
2022-09-24 09:33:04 -06:00
|
|
|
details = { a: entry.selectors };
|
|
|
|
contentMap.set(id, details);
|
2022-09-17 06:26:41 -06:00
|
|
|
}
|
2022-09-18 07:31:44 -06:00
|
|
|
if ( entry.matches !== undefined ) {
|
2022-09-19 06:55:45 -06:00
|
|
|
if ( details.y === undefined ) {
|
|
|
|
details.y = new Set();
|
2022-09-18 07:31:44 -06:00
|
|
|
}
|
2022-09-17 06:26:41 -06:00
|
|
|
for ( const hn of entry.matches ) {
|
2022-09-19 06:55:45 -06:00
|
|
|
details.y.add(hn);
|
2022-09-17 06:26:41 -06:00
|
|
|
}
|
|
|
|
}
|
2022-09-18 07:31:44 -06:00
|
|
|
if ( entry.excludeMatches !== undefined ) {
|
2022-09-19 06:55:45 -06:00
|
|
|
if ( details.n === undefined ) {
|
|
|
|
details.n = new Set();
|
2022-09-18 07:31:44 -06:00
|
|
|
}
|
2022-09-17 06:26:41 -06:00
|
|
|
for ( const hn of entry.excludeMatches ) {
|
2022-09-19 06:55:45 -06:00
|
|
|
details.n.add(hn);
|
2022-09-17 06:26:41 -06:00
|
|
|
}
|
|
|
|
}
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
2022-09-24 09:33:04 -06:00
|
|
|
const hnSort = (a, b) =>
|
|
|
|
a.split('.').reverse().join('.').localeCompare(
|
|
|
|
b.split('.').reverse().join('.')
|
|
|
|
);
|
|
|
|
const out = Array.from(contentMap).map(a => [
|
|
|
|
a[0], {
|
|
|
|
a: a[1].a,
|
|
|
|
y: a[1].y ? Array.from(a[1].y).sort(hnSort) : undefined,
|
|
|
|
n: a[1].n ? Array.from(a[1].n) : undefined,
|
|
|
|
}
|
|
|
|
]).sort((a, b) => {
|
|
|
|
const ha = Array.isArray(a[1].y) ? a[1].y[0] : '*';
|
|
|
|
const hb = Array.isArray(b[1].y) ? b[1].y[0] : '*';
|
|
|
|
return hnSort(ha, hb);
|
|
|
|
});
|
|
|
|
return out;
|
|
|
|
}
|
2022-09-16 13:56:35 -06:00
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
const scriptletHostnameToIdMap = (hostnames, id, map) => {
|
|
|
|
for ( const hn of hostnames ) {
|
|
|
|
const existing = map.get(hn);
|
|
|
|
if ( existing === undefined ) {
|
|
|
|
map.set(hn, id);
|
|
|
|
} else if ( Array.isArray(existing) ) {
|
|
|
|
existing.push(id);
|
|
|
|
} else {
|
|
|
|
map.set(hn, [ existing, id ]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const scriptletJsonReplacer = (k, v) => {
|
|
|
|
if ( k === 'n' ) {
|
|
|
|
if ( v === undefined || v.size === 0 ) { return; }
|
|
|
|
return Array.from(v);
|
|
|
|
}
|
|
|
|
if ( v instanceof Set || v instanceof Map ) {
|
|
|
|
if ( v.size === 0 ) { return; }
|
|
|
|
return Array.from(v);
|
|
|
|
}
|
|
|
|
return v;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
async function processCosmeticFilters(assetDetails, mapin) {
|
|
|
|
if ( mapin === undefined ) { return 0; }
|
|
|
|
|
|
|
|
const contentArray = groupCosmeticBySelectors(
|
|
|
|
groupCosmeticByHostnames(mapin)
|
|
|
|
);
|
|
|
|
const contentPerFile =
|
|
|
|
Math.ceil(contentArray.length / COSMETIC_FILES_PER_RULESET);
|
|
|
|
|
|
|
|
// We do not want more than n CSS files per subscription, so we will
|
2022-09-19 06:55:45 -06:00
|
|
|
// group multiple unrelated selectors in the same file, and distinct
|
|
|
|
// css declarations will be injected programmatically according to the
|
|
|
|
// hostname of the current document.
|
|
|
|
//
|
|
|
|
// The cosmetic filters will be injected programmatically as content
|
|
|
|
// script and the decisions to activate the cosmetic filters will be
|
|
|
|
// done at injection time according to the document's hostname.
|
|
|
|
const originalScriptletMap = await loadAllSourceScriptlets();
|
2022-09-24 09:33:04 -06:00
|
|
|
const generatedFiles = [];
|
2022-09-19 06:55:45 -06:00
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
for ( let i = 0; i < contentArray.length; i += contentPerFile ) {
|
|
|
|
const slice = contentArray.slice(i, i + contentPerFile);
|
|
|
|
const argsMap = slice.map(entry => [
|
|
|
|
entry[0],
|
|
|
|
{
|
|
|
|
a: entry[1].a ? entry[1].a.join(',\n') : undefined,
|
|
|
|
n: entry[1].n
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
const hostnamesMap = new Map();
|
|
|
|
for ( const [ id, details ] of slice ) {
|
|
|
|
if ( details.y === undefined ) { continue; }
|
|
|
|
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
2022-09-19 06:55:45 -06:00
|
|
|
}
|
2022-09-24 09:33:04 -06:00
|
|
|
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
|
|
|
.replace(
|
|
|
|
'$rulesetId$',
|
|
|
|
assetDetails.id
|
|
|
|
).replace(
|
|
|
|
/\bself\.\$argsMap\$/m,
|
|
|
|
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
|
|
|
).replace(
|
|
|
|
/\bself\.\$hostnamesMap\$/m,
|
|
|
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
|
|
|
);
|
|
|
|
const fid = toCSSFileId(patchedScriptlet);
|
|
|
|
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
|
|
|
globalPatchedScriptletsSet.add(fid);
|
|
|
|
const fname = fnameFromFileId(fid);
|
|
|
|
writeFile(pathFromFileName(fname), patchedScriptlet, {});
|
|
|
|
generatedFiles.push(fname);
|
2022-09-19 06:55:45 -06:00
|
|
|
}
|
2022-09-24 09:33:04 -06:00
|
|
|
for ( const entry of slice ) {
|
|
|
|
addScriptingAPIResources(
|
|
|
|
assetDetails.id,
|
|
|
|
{ matches: entry[1].y },
|
|
|
|
'matches',
|
|
|
|
fid
|
|
|
|
);
|
|
|
|
addScriptingAPIResources(
|
|
|
|
assetDetails.id,
|
|
|
|
{ excludeMatches: entry[1].n },
|
|
|
|
'excludeMatches',
|
|
|
|
fid
|
|
|
|
);
|
2022-09-19 06:55:45 -06:00
|
|
|
}
|
2022-09-24 09:33:04 -06:00
|
|
|
}
|
2022-09-19 06:55:45 -06:00
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
if ( generatedFiles.length !== 0 ) {
|
|
|
|
log(`CSS-related distinct filters: ${contentArray.length} distinct combined selectors`);
|
|
|
|
log(`CSS-related injectable files: ${generatedFiles.length}`);
|
|
|
|
log(`\t${generatedFiles.join(', ')}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return contentArray.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
async function processProceduralCosmeticFilters(assetDetails, mapin) {
|
|
|
|
if ( mapin === undefined ) { return 0; }
|
|
|
|
|
|
|
|
const contentArray = groupCosmeticBySelectors(
|
|
|
|
groupCosmeticByHostnames(mapin)
|
|
|
|
);
|
|
|
|
const contentPerFile =
|
|
|
|
Math.ceil(contentArray.length / PROCEDURAL_FILES_PER_RULESET);
|
|
|
|
|
|
|
|
// We do not want more than n CSS files per subscription, so we will
|
|
|
|
// group multiple unrelated selectors in the same file, and distinct
|
|
|
|
// css declarations will be injected programmatically according to the
|
|
|
|
// hostname of the current document.
|
|
|
|
//
|
|
|
|
// The cosmetic filters will be injected programmatically as content
|
|
|
|
// script and the decisions to activate the cosmetic filters will be
|
|
|
|
// done at injection time according to the document's hostname.
|
|
|
|
const originalScriptletMap = await loadAllSourceScriptlets();
|
2022-09-19 09:08:34 -06:00
|
|
|
const generatedFiles = [];
|
2022-09-18 07:31:44 -06:00
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
for ( let i = 0; i < contentArray.length; i += contentPerFile ) {
|
|
|
|
const slice = contentArray.slice(i, i + contentPerFile);
|
2022-09-19 06:55:45 -06:00
|
|
|
const argsMap = slice.map(entry => [
|
2022-09-24 09:33:04 -06:00
|
|
|
entry[0],
|
|
|
|
{
|
|
|
|
a: entry[1].a ? entry[1].a.map(v => JSON.parse(v)) : undefined,
|
|
|
|
n: entry[1].n
|
|
|
|
}
|
2022-09-19 06:55:45 -06:00
|
|
|
]);
|
|
|
|
const hostnamesMap = new Map();
|
|
|
|
for ( const [ id, details ] of slice ) {
|
|
|
|
if ( details.y === undefined ) { continue; }
|
2022-09-24 09:33:04 -06:00
|
|
|
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
2022-09-19 06:55:45 -06:00
|
|
|
}
|
2022-09-24 09:33:04 -06:00
|
|
|
const patchedScriptlet = originalScriptletMap.get('css-specific-procedural')
|
2022-09-19 06:55:45 -06:00
|
|
|
.replace(
|
2022-09-24 09:33:04 -06:00
|
|
|
'$rulesetId$',
|
|
|
|
assetDetails.id
|
|
|
|
).replace(
|
2022-09-19 06:55:45 -06:00
|
|
|
/\bself\.\$argsMap\$/m,
|
2022-09-24 09:33:04 -06:00
|
|
|
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
2022-09-19 06:55:45 -06:00
|
|
|
).replace(
|
|
|
|
/\bself\.\$hostnamesMap\$/m,
|
2022-09-24 09:33:04 -06:00
|
|
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
2022-09-18 07:31:44 -06:00
|
|
|
);
|
2022-09-24 09:33:04 -06:00
|
|
|
const fid = toProceduralFileId(patchedScriptlet);
|
2022-09-19 18:19:55 -06:00
|
|
|
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
|
|
|
globalPatchedScriptletsSet.add(fid);
|
2022-09-20 06:24:01 -06:00
|
|
|
const fname = fnameFromFileId(fid);
|
|
|
|
writeFile(pathFromFileName(fname), patchedScriptlet, {});
|
2022-09-19 09:08:34 -06:00
|
|
|
generatedFiles.push(fname);
|
2022-09-18 07:31:44 -06:00
|
|
|
}
|
2022-09-19 06:55:45 -06:00
|
|
|
for ( const entry of slice ) {
|
|
|
|
addScriptingAPIResources(
|
|
|
|
assetDetails.id,
|
|
|
|
{ matches: entry[1].y },
|
|
|
|
'matches',
|
2022-09-19 18:19:55 -06:00
|
|
|
fid
|
2022-09-19 06:55:45 -06:00
|
|
|
);
|
|
|
|
addScriptingAPIResources(
|
|
|
|
assetDetails.id,
|
|
|
|
{ excludeMatches: entry[1].n },
|
|
|
|
'excludeMatches',
|
2022-09-19 18:19:55 -06:00
|
|
|
fid
|
2022-09-19 06:55:45 -06:00
|
|
|
);
|
|
|
|
}
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
2022-09-18 07:31:44 -06:00
|
|
|
|
2022-09-19 09:08:34 -06:00
|
|
|
if ( generatedFiles.length !== 0 ) {
|
2022-09-24 09:33:04 -06:00
|
|
|
log(`Pprocedural-related distinct filters: ${contentArray.length} distinct combined selectors`);
|
|
|
|
log(`Pprocedural-related injectable files: ${generatedFiles.length}`);
|
2022-09-19 09:53:04 -06:00
|
|
|
log(`\t${generatedFiles.join(', ')}`);
|
2022-09-19 09:08:34 -06:00
|
|
|
}
|
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
return contentArray.length;
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************/
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
async function processScriptletFilters(assetDetails, mapin) {
|
|
|
|
if ( mapin === undefined ) { return 0; }
|
|
|
|
|
2022-09-17 06:26:41 -06:00
|
|
|
// Load all available scriptlets into a key-val map, where the key is the
|
|
|
|
// scriptlet token, and val is the whole content of the file.
|
2022-09-18 15:07:02 -06:00
|
|
|
const originalScriptletMap = await loadAllSourceScriptlets();
|
2022-09-16 13:56:35 -06:00
|
|
|
|
|
|
|
const parseArguments = (raw) => {
|
|
|
|
const out = [];
|
|
|
|
let s = raw;
|
|
|
|
let len = s.length;
|
|
|
|
let beg = 0, pos = 0;
|
|
|
|
let i = 1;
|
|
|
|
while ( beg < len ) {
|
|
|
|
pos = s.indexOf(',', pos);
|
|
|
|
// Escaped comma? If so, skip.
|
|
|
|
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
|
|
|
|
s = s.slice(0, pos - 1) + s.slice(pos);
|
|
|
|
len -= 1;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( pos === -1 ) { pos = len; }
|
|
|
|
out.push(s.slice(beg, pos).trim());
|
|
|
|
beg = pos = pos + 1;
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
};
|
|
|
|
|
|
|
|
const parseFilter = (raw) => {
|
|
|
|
const filter = raw.slice(4, -1);
|
|
|
|
const end = filter.length;
|
|
|
|
let pos = filter.indexOf(',');
|
|
|
|
if ( pos === -1 ) { pos = end; }
|
|
|
|
const parts = filter.trim().split(',').map(s => s.trim());
|
2022-09-17 06:26:41 -06:00
|
|
|
const token = scriptletDealiasingMap.get(parts[0]) || '';
|
2022-09-16 13:56:35 -06:00
|
|
|
if ( token !== '' && originalScriptletMap.has(token) ) {
|
|
|
|
return {
|
|
|
|
token,
|
|
|
|
args: parseArguments(parts.slice(1).join(',').trim()),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-09-18 15:07:02 -06:00
|
|
|
// For each instance of distinct scriptlet, we will collect distinct
|
|
|
|
// instances of arguments, and for each distinct set of argument, we
|
|
|
|
// will collect the set of hostnames for which the scriptlet/args is meant
|
|
|
|
// to execute. This will allow us a single content script file and the
|
|
|
|
// scriptlets execution will depend on hostname testing against the
|
|
|
|
// URL of the document at scriptlet execution time. In the end, we
|
|
|
|
// should have no more generated content script per subscription than the
|
|
|
|
// number of distinct source scriptlets.
|
|
|
|
const scriptletDetails = new Map();
|
2022-09-17 06:26:41 -06:00
|
|
|
for ( const [ rawFilter, entry ] of mapin ) {
|
2022-09-16 13:56:35 -06:00
|
|
|
const normalized = parseFilter(rawFilter);
|
|
|
|
if ( normalized === undefined ) { continue; }
|
2022-09-18 15:07:02 -06:00
|
|
|
let argsDetails = scriptletDetails.get(normalized.token);
|
|
|
|
if ( argsDetails === undefined ) {
|
|
|
|
argsDetails = new Map();
|
|
|
|
scriptletDetails.set(normalized.token, argsDetails);
|
|
|
|
}
|
|
|
|
const argsHash = JSON.stringify(normalized.args);
|
|
|
|
let hostnamesDetails = argsDetails.get(argsHash);
|
|
|
|
if ( hostnamesDetails === undefined ) {
|
|
|
|
hostnamesDetails = {
|
|
|
|
a: normalized.args,
|
|
|
|
y: new Set(),
|
|
|
|
n: new Set(),
|
|
|
|
};
|
2022-09-19 09:08:34 -06:00
|
|
|
argsDetails.set(argsHash, hostnamesDetails);
|
2022-09-18 15:07:02 -06:00
|
|
|
}
|
|
|
|
if ( entry.matches ) {
|
|
|
|
for ( const hn of entry.matches ) {
|
|
|
|
hostnamesDetails.y.add(hn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( entry.excludeMatches ) {
|
|
|
|
for ( const hn of entry.excludeMatches ) {
|
|
|
|
hostnamesDetails.n.add(hn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-19 09:08:34 -06:00
|
|
|
const generatedFiles = [];
|
2022-09-18 15:07:02 -06:00
|
|
|
|
|
|
|
for ( const [ token, argsDetails ] of scriptletDetails ) {
|
2022-09-19 06:55:45 -06:00
|
|
|
const argsMap = Array.from(argsDetails).map(entry => [
|
2022-09-19 18:19:55 -06:00
|
|
|
uidint32(entry[0]),
|
2022-09-19 06:55:45 -06:00
|
|
|
{ a: entry[1].a, n: entry[1].n }
|
|
|
|
]);
|
2022-09-18 15:07:02 -06:00
|
|
|
const hostnamesMap = new Map();
|
|
|
|
for ( const [ argsHash, details ] of argsDetails ) {
|
2022-09-24 09:33:04 -06:00
|
|
|
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
|
2022-09-18 15:07:02 -06:00
|
|
|
}
|
|
|
|
const patchedScriptlet = originalScriptletMap.get(token)
|
|
|
|
.replace(
|
2022-09-24 09:33:04 -06:00
|
|
|
'$rulesetId$',
|
|
|
|
assetDetails.id
|
|
|
|
).replace(
|
2022-09-18 15:07:02 -06:00
|
|
|
/\bself\.\$argsMap\$/m,
|
2022-09-24 09:33:04 -06:00
|
|
|
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
2022-09-18 15:07:02 -06:00
|
|
|
).replace(
|
|
|
|
/\bself\.\$hostnamesMap\$/m,
|
2022-09-24 09:33:04 -06:00
|
|
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
2022-09-18 15:07:02 -06:00
|
|
|
);
|
2022-09-18 07:31:44 -06:00
|
|
|
// ends-with 1 = scriptlet resource
|
2022-09-19 18:19:55 -06:00
|
|
|
const fid = toJSFileId(patchedScriptlet);
|
|
|
|
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
|
|
|
globalPatchedScriptletsSet.add(fid);
|
2022-09-20 06:24:01 -06:00
|
|
|
const fname = fnameFromFileId(fid);
|
|
|
|
writeFile(pathFromFileName(fname), patchedScriptlet, {});
|
2022-09-19 09:08:34 -06:00
|
|
|
generatedFiles.push(fname);
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
2022-09-18 15:07:02 -06:00
|
|
|
for ( const details of argsDetails.values() ) {
|
|
|
|
addScriptingAPIResources(
|
|
|
|
assetDetails.id,
|
|
|
|
{ matches: details.y },
|
|
|
|
'matches',
|
2022-09-19 18:19:55 -06:00
|
|
|
fid
|
2022-09-18 15:07:02 -06:00
|
|
|
);
|
|
|
|
addScriptingAPIResources(
|
|
|
|
assetDetails.id,
|
|
|
|
{ excludeMatches: details.n },
|
|
|
|
'excludeMatches',
|
2022-09-19 18:19:55 -06:00
|
|
|
fid
|
2022-09-18 15:07:02 -06:00
|
|
|
);
|
|
|
|
}
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
2022-09-19 09:08:34 -06:00
|
|
|
if ( generatedFiles.length !== 0 ) {
|
|
|
|
const scriptletFilterCount = Array.from(scriptletDetails.values())
|
|
|
|
.reduce((a, b) => a + b.size, 0);
|
|
|
|
log(`Scriptlet-related distinct filters: ${scriptletFilterCount}`);
|
|
|
|
log(`Scriptlet-related injectable files: ${generatedFiles.length}`);
|
2022-09-19 09:53:04 -06:00
|
|
|
log(`\t${generatedFiles.join(', ')}`);
|
2022-09-19 09:08:34 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return generatedFiles.length;
|
2022-09-16 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-24 09:33:04 -06:00
|
|
|
const rulesetFromURLS = async function(assetDetails) {
|
|
|
|
log('============================');
|
|
|
|
log(`Listset for '${assetDetails.id}':`);
|
|
|
|
|
|
|
|
const text = await fetchAsset(assetDetails);
|
2022-09-24 10:23:01 -06:00
|
|
|
if ( text === '' ) { return; }
|
2022-09-24 09:33:04 -06:00
|
|
|
|
|
|
|
const results = await dnrRulesetFromRawLists(
|
|
|
|
[ { name: assetDetails.id, text } ],
|
|
|
|
{ env }
|
|
|
|
);
|
|
|
|
|
|
|
|
const netStats = await processNetworkFilters(
|
|
|
|
assetDetails,
|
|
|
|
results.network
|
|
|
|
);
|
|
|
|
|
|
|
|
// Split cosmetic filters into two groups: declarative and procedural
|
|
|
|
const declarativeCosmetic = new Map();
|
|
|
|
const proceduralCosmetic = new Map();
|
|
|
|
const rejectedCosmetic = [];
|
|
|
|
if ( results.cosmetic ) {
|
|
|
|
for ( const [ selector, details ] of results.cosmetic ) {
|
|
|
|
if ( details.rejected ) {
|
|
|
|
rejectedCosmetic.push(selector);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( selector.startsWith('{') === false ) {
|
|
|
|
declarativeCosmetic.set(selector, details);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const parsed = JSON.parse(selector);
|
|
|
|
parsed.raw = undefined;
|
|
|
|
proceduralCosmetic.set(JSON.stringify(parsed), details);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const cosmeticStats = await processCosmeticFilters(
|
|
|
|
assetDetails,
|
|
|
|
declarativeCosmetic
|
|
|
|
);
|
|
|
|
const proceduralStats = await processProceduralCosmeticFilters(
|
|
|
|
assetDetails,
|
|
|
|
proceduralCosmetic
|
|
|
|
);
|
|
|
|
if ( rejectedCosmetic.length !== 0 ) {
|
|
|
|
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
|
|
|
|
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'));
|
|
|
|
}
|
|
|
|
|
|
|
|
const scriptletStats = await processScriptletFilters(
|
|
|
|
assetDetails,
|
|
|
|
results.scriptlet
|
|
|
|
);
|
|
|
|
|
|
|
|
rulesetDetails.push({
|
|
|
|
id: assetDetails.id,
|
|
|
|
name: assetDetails.name,
|
|
|
|
enabled: assetDetails.enabled,
|
|
|
|
lang: assetDetails.lang,
|
|
|
|
homeURL: assetDetails.homeURL,
|
|
|
|
filters: {
|
|
|
|
total: results.network.filterCount,
|
|
|
|
accepted: results.network.acceptedFilterCount,
|
|
|
|
rejected: results.network.rejectedFilterCount,
|
|
|
|
},
|
|
|
|
rules: {
|
|
|
|
total: netStats.total,
|
|
|
|
accepted: netStats.accepted,
|
|
|
|
discarded: netStats.discarded,
|
|
|
|
rejected: netStats.rejected,
|
|
|
|
regexes: netStats.regexes,
|
|
|
|
},
|
|
|
|
css: {
|
|
|
|
specific: cosmeticStats,
|
|
|
|
procedural: proceduralStats,
|
|
|
|
},
|
|
|
|
scriptlets: {
|
|
|
|
total: scriptletStats,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
ruleResources.push({
|
|
|
|
id: assetDetails.id,
|
|
|
|
enabled: assetDetails.enabled,
|
|
|
|
path: `/rulesets/${assetDetails.id}.json`
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
async function main() {
|
|
|
|
|
|
|
|
// Get manifest content
|
|
|
|
const manifest = await fs.readFile(
|
|
|
|
`${outputDir}/manifest.json`,
|
|
|
|
{ encoding: 'utf8' }
|
|
|
|
).then(text =>
|
|
|
|
JSON.parse(text)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Create unique version number according to build time
|
|
|
|
let version = manifest.version;
|
|
|
|
{
|
|
|
|
const now = new Date();
|
|
|
|
const yearPart = now.getUTCFullYear() - 2000;
|
|
|
|
const monthPart = (now.getUTCMonth() + 1) * 1000;
|
|
|
|
const dayPart = now.getUTCDate() * 10;
|
|
|
|
const hourPart = Math.floor(now.getUTCHours() / 3) + 1;
|
|
|
|
version += `.${yearPart}.${monthPart + dayPart + hourPart}`;
|
|
|
|
}
|
|
|
|
log(`Version: ${version}`);
|
|
|
|
|
2022-09-13 15:44:24 -06:00
|
|
|
// Get assets.json content
|
|
|
|
const assets = await fs.readFile(
|
|
|
|
`./assets.json`,
|
|
|
|
{ encoding: 'utf8' }
|
|
|
|
).then(text =>
|
|
|
|
JSON.parse(text)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Assemble all default lists as the default ruleset
|
2022-09-16 14:36:09 -06:00
|
|
|
const contentURLs = [
|
|
|
|
'https://ublockorigin.pages.dev/filters/filters.txt',
|
|
|
|
'https://ublockorigin.pages.dev/filters/badware.txt',
|
|
|
|
'https://ublockorigin.pages.dev/filters/privacy.txt',
|
|
|
|
'https://ublockorigin.pages.dev/filters/resource-abuse.txt',
|
|
|
|
'https://ublockorigin.pages.dev/filters/unbreak.txt',
|
|
|
|
'https://ublockorigin.pages.dev/filters/quick-fixes.txt',
|
|
|
|
'https://secure.fanboy.co.nz/easylist.txt',
|
|
|
|
'https://secure.fanboy.co.nz/easyprivacy.txt',
|
|
|
|
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
|
|
|
|
];
|
2022-09-13 15:44:24 -06:00
|
|
|
await rulesetFromURLS({
|
|
|
|
id: 'default',
|
|
|
|
name: 'Ads, trackers, miners, and more' ,
|
|
|
|
enabled: true,
|
|
|
|
urls: contentURLs,
|
2022-09-15 11:14:08 -06:00
|
|
|
homeURL: 'https://github.com/uBlockOrigin/uAssets',
|
2022-09-13 15:44:24 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
// Regional rulesets
|
|
|
|
for ( const [ id, asset ] of Object.entries(assets) ) {
|
|
|
|
if ( asset.content !== 'filters' ) { continue; }
|
|
|
|
if ( asset.off !== true ) { continue; }
|
|
|
|
if ( typeof asset.lang !== 'string' ) { continue; }
|
|
|
|
|
|
|
|
const contentURL = Array.isArray(asset.contentURL)
|
|
|
|
? asset.contentURL[0]
|
|
|
|
: asset.contentURL;
|
|
|
|
await rulesetFromURLS({
|
|
|
|
id: id.toLowerCase(),
|
|
|
|
lang: asset.lang,
|
|
|
|
name: asset.title,
|
|
|
|
enabled: false,
|
|
|
|
urls: [ contentURL ],
|
2022-09-15 11:14:08 -06:00
|
|
|
homeURL: asset.supportURL,
|
2022-09-13 15:44:24 -06:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-09-15 11:14:08 -06:00
|
|
|
// Handpicked rulesets from assets.json
|
2022-09-13 15:44:24 -06:00
|
|
|
const handpicked = [ 'block-lan', 'dpollock-0' ];
|
|
|
|
for ( const id of handpicked ) {
|
|
|
|
const asset = assets[id];
|
|
|
|
if ( asset.content !== 'filters' ) { continue; }
|
|
|
|
|
|
|
|
const contentURL = Array.isArray(asset.contentURL)
|
|
|
|
? asset.contentURL[0]
|
|
|
|
: asset.contentURL;
|
|
|
|
await rulesetFromURLS({
|
|
|
|
id: id.toLowerCase(),
|
|
|
|
name: asset.title,
|
|
|
|
enabled: false,
|
|
|
|
urls: [ contentURL ],
|
2022-09-15 11:14:08 -06:00
|
|
|
homeURL: asset.supportURL,
|
2022-09-13 15:44:24 -06:00
|
|
|
});
|
2022-09-06 11:47:52 -06:00
|
|
|
}
|
|
|
|
|
2022-09-15 11:14:08 -06:00
|
|
|
// Handpicked rulesets from abroad
|
|
|
|
await rulesetFromURLS({
|
|
|
|
id: 'stevenblack-hosts',
|
|
|
|
name: 'Steven Black\'s hosts file',
|
|
|
|
enabled: false,
|
|
|
|
urls: [ 'https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts' ],
|
|
|
|
homeURL: 'https://github.com/StevenBlack/hosts#readme',
|
|
|
|
});
|
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
writeFile(
|
|
|
|
`${rulesetDir}/ruleset-details.json`,
|
|
|
|
`${JSON.stringify(rulesetDetails, null, 1)}\n`
|
2022-09-15 11:14:08 -06:00
|
|
|
);
|
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
writeFile(
|
2022-09-18 07:31:44 -06:00
|
|
|
`${rulesetDir}/scripting-details.json`,
|
2022-09-18 15:07:02 -06:00
|
|
|
`${JSON.stringify(scriptingDetails, jsonSetMapReplacer)}\n`
|
2022-09-16 13:56:35 -06:00
|
|
|
);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
2022-09-16 13:56:35 -06:00
|
|
|
await Promise.all(writeOps);
|
2022-09-06 11:47:52 -06:00
|
|
|
|
|
|
|
// Patch manifest
|
|
|
|
manifest.declarative_net_request = { rule_resources: ruleResources };
|
|
|
|
const now = new Date();
|
2022-09-07 08:15:36 -06:00
|
|
|
const yearPart = now.getUTCFullYear() - 2000;
|
|
|
|
const monthPart = (now.getUTCMonth() + 1) * 1000;
|
|
|
|
const dayPart = now.getUTCDate() * 10;
|
|
|
|
const hourPart = Math.floor(now.getUTCHours() / 3) + 1;
|
|
|
|
manifest.version = manifest.version + `.${yearPart}.${monthPart + dayPart + hourPart}`;
|
2022-09-06 11:47:52 -06:00
|
|
|
await fs.writeFile(
|
|
|
|
`${outputDir}/manifest.json`,
|
|
|
|
JSON.stringify(manifest, null, 2) + '\n'
|
|
|
|
);
|
|
|
|
|
|
|
|
// Log results
|
2022-09-13 15:44:24 -06:00
|
|
|
await fs.writeFile(`${outputDir}/log.txt`, stdOutput.join('\n') + '\n');
|
2022-09-06 11:47:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
main();
|
|
|
|
|
|
|
|
/******************************************************************************/
|