mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
243 lines
9.8 KiB
JavaScript
243 lines
9.8 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin - a comprehensive, efficient content blocker
|
|
Copyright (C) 2017-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
|
|
*/
|
|
|
|
import { builtinScriptlets } from './js/resources/scriptlets.js';
|
|
import fs from 'fs/promises';
|
|
import { literalStrFromRegex } from './js/regex-analyzer.js';
|
|
import { safeReplace } from './safe-replace.js';
|
|
|
|
/******************************************************************************/
|
|
|
|
const resourceDetails = new Map();
|
|
const resourceAliases = new Map();
|
|
const worldTemplate = {
|
|
scriptletFunctions: new Map(),
|
|
allFunctions: new Map(),
|
|
args: new Map(),
|
|
arglists: new Map(),
|
|
hostnames: new Map(),
|
|
regexesOrPaths: new Map(),
|
|
matches: new Set(),
|
|
hasEntities: false,
|
|
hasAncestors: false,
|
|
};
|
|
const worlds = {
|
|
ISOLATED: structuredClone(worldTemplate),
|
|
MAIN: structuredClone(worldTemplate),
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
function createScriptletCoreCode(worldDetails, resourceEntry) {
|
|
const { allFunctions } = worldDetails;
|
|
allFunctions.set(resourceEntry.name, resourceEntry.code);
|
|
const dependencies = resourceEntry.dependencies &&
|
|
resourceEntry.dependencies.slice() || [];
|
|
while ( dependencies.length !== 0 ) {
|
|
const token = dependencies.shift();
|
|
const details = resourceDetails.get(token);
|
|
if ( details === undefined ) { continue; }
|
|
if ( allFunctions.has(details.name) ) { continue; }
|
|
allFunctions.set(details.name, details.code);
|
|
if ( Array.isArray(details.dependencies) === false ) { continue; }
|
|
dependencies.push(...details.dependencies);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export function reset() {
|
|
worlds.ISOLATED = structuredClone(worldTemplate);
|
|
worlds.MAIN = structuredClone(worldTemplate);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export function compile(assetDetails, details) {
|
|
if ( details.args[0].endsWith('.js') === false ) {
|
|
details.args[0] += '.js';
|
|
}
|
|
if ( resourceAliases.has(details.args[0]) ) {
|
|
details.args[0] = resourceAliases.get(details.args[0]);
|
|
}
|
|
const scriptletToken = details.args[0];
|
|
const resourceEntry = resourceDetails.get(scriptletToken);
|
|
if ( resourceEntry === undefined ) { return; }
|
|
if ( resourceEntry.requiresTrust && details.trustedSource !== true ) {
|
|
console.log(`Rejecting +js(${details.args.join()}): ${assetDetails.id} is not trusted`);
|
|
return;
|
|
}
|
|
const worldDetails = worlds[resourceEntry.world];
|
|
const { scriptletFunctions } = worldDetails;
|
|
if ( scriptletFunctions.has(resourceEntry.name) === false ) {
|
|
scriptletFunctions.set(resourceEntry.name, scriptletFunctions.size);
|
|
createScriptletCoreCode(worldDetails, resourceEntry);
|
|
}
|
|
// Convert args to arg indices
|
|
const arglist = details.args.slice();
|
|
arglist[0] = scriptletFunctions.get(resourceEntry.name);
|
|
for ( let i = 1; i < arglist.length; i++ ) {
|
|
const arg = arglist[i];
|
|
if ( worldDetails.args.has(arg) === false ) {
|
|
worldDetails.args.set(arg, worldDetails.args.size);
|
|
}
|
|
arglist[i] = worldDetails.args.get(arg);
|
|
}
|
|
const arglistKey = JSON.stringify(arglist).slice(1, -1);
|
|
if ( worldDetails.arglists.has(arglistKey) === false ) {
|
|
worldDetails.arglists.set(arglistKey, worldDetails.arglists.size);
|
|
}
|
|
const arglistIndex = worldDetails.arglists.get(arglistKey);
|
|
if ( details.matches ) {
|
|
for ( const hn of details.matches ) {
|
|
if ( hn.includes('/') ) {
|
|
worldDetails.matches.clear();
|
|
worldDetails.matches.add('*');
|
|
if ( worldDetails.regexesOrPaths.has(hn) === false ) {
|
|
worldDetails.regexesOrPaths.set(hn, new Set());
|
|
}
|
|
worldDetails.regexesOrPaths.get(hn).add(arglistIndex);
|
|
continue;
|
|
}
|
|
const isEntity = hn.endsWith('.*') || hn.endsWith('.*>>');
|
|
worldDetails.hasEntities ||= isEntity;
|
|
const isAncestor = hn.endsWith('>>')
|
|
worldDetails.hasAncestors ||= isAncestor;
|
|
if ( isEntity || isAncestor ) {
|
|
worldDetails.matches.clear();
|
|
worldDetails.matches.add('*');
|
|
}
|
|
if ( worldDetails.matches.has('*') === false ) {
|
|
worldDetails.matches.add(hn);
|
|
}
|
|
if ( worldDetails.hostnames.has(hn) === false ) {
|
|
worldDetails.hostnames.set(hn, new Set());
|
|
}
|
|
worldDetails.hostnames.get(hn).add(arglistIndex);
|
|
}
|
|
} else {
|
|
worldDetails.matches.add('*');
|
|
}
|
|
if ( details.excludeMatches ) {
|
|
for ( const hn of details.excludeMatches ) {
|
|
if ( hn.includes('/') ) {
|
|
if ( worldDetails.regexesOrPaths.has(hn) === false ) {
|
|
worldDetails.regexesOrPaths.set(hn, new Set());
|
|
}
|
|
worldDetails.regexesOrPaths.get(hn).add(~arglistIndex);
|
|
continue;
|
|
}
|
|
if ( worldDetails.hostnames.has(hn) === false ) {
|
|
worldDetails.hostnames.set(hn, new Set());
|
|
}
|
|
worldDetails.hostnames.get(hn).add(~arglistIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
export async function commit(rulesetId, path, writeFn) {
|
|
const scriptletTemplate = await fs.readFile(
|
|
'./scriptlets/scriptlet.template.js',
|
|
{ encoding: 'utf8' }
|
|
);
|
|
const stats = {};
|
|
for ( const world of Object.keys(worlds) ) {
|
|
const worldDetails = worlds[world];
|
|
const { scriptletFunctions, allFunctions, args, arglists } = worldDetails;
|
|
if ( scriptletFunctions.size === 0 ) { continue; }
|
|
const hostnames = Array.from(worldDetails.hostnames).toSorted((a, b) => {
|
|
const d = a[0].length - b[0].length;
|
|
if ( d !== 0 ) { return d; }
|
|
return a[0] < b[0] ? -1 : 1;
|
|
}).map(a => ([ a[0], JSON.stringify(Array.from(a[1]).map(a => JSON.parse(a))).slice(1,-1)]));
|
|
const scriptletFromRegexes = Array.from(worldDetails.regexesOrPaths)
|
|
.filter(a => a[0].startsWith('/') && a[0].endsWith('/'))
|
|
.map(a => {
|
|
const restr = a[0].slice(1,-1);
|
|
return [
|
|
literalStrFromRegex(restr).slice(0,8),
|
|
restr,
|
|
JSON.stringify(Array.from(a[1])).slice(1,-1),
|
|
];
|
|
}).flat();
|
|
let content = safeReplace(scriptletTemplate, 'self.$hasEntities$', JSON.stringify(worldDetails.hasEntities));
|
|
content = safeReplace(content, 'self.$hasAncestors$', JSON.stringify(worldDetails.hasAncestors));
|
|
content = safeReplace(content, 'self.$hasRegexes$', JSON.stringify(scriptletFromRegexes.length !== 0));
|
|
content = safeReplace(content,
|
|
'self.$scriptletFromRegexes$',
|
|
`/* ${worldDetails.regexesOrPaths.size} */ ${JSON.stringify(scriptletFromRegexes)}`
|
|
);
|
|
content = safeReplace(content,
|
|
'self.$scriptletHostnames$',
|
|
`/* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[0]))}`
|
|
);
|
|
content = safeReplace(content,
|
|
'self.$scriptletArglistRefs$',
|
|
`/* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[1]).join(';'))}`
|
|
);
|
|
content = safeReplace(content,
|
|
'self.$scriptletArglists$',
|
|
`/* ${arglists.size} */ ${JSON.stringify(Array.from(arglists.keys()).join(';'))}`
|
|
);
|
|
content = safeReplace(content,
|
|
'self.$scriptletArgs$',
|
|
`/* ${args.size} */ ${JSON.stringify(Array.from(args.keys()))}`
|
|
);
|
|
content = safeReplace(content,
|
|
'self.$scriptletFunctions$',
|
|
`/* ${scriptletFunctions.size} */\n[${Array.from(scriptletFunctions.keys()).join(',')}]`
|
|
);
|
|
content = safeReplace(content,
|
|
'self.$scriptletCode$',
|
|
Array.from(allFunctions.values()).sort().join('\n\n')
|
|
);
|
|
content = safeReplace(content, /\$rulesetId\$/, rulesetId, 0);
|
|
writeFn(`${path}/${world.toLowerCase()}/${rulesetId}.js`, content);
|
|
stats[world] = Array.from(worldDetails.matches).sort();
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function init() {
|
|
for ( const scriptlet of builtinScriptlets ) {
|
|
const { name, aliases, fn } = scriptlet;
|
|
const entry = {
|
|
name: fn.name,
|
|
code: fn.toString(),
|
|
world: scriptlet.world || 'MAIN',
|
|
dependencies: scriptlet.dependencies,
|
|
requiresTrust: scriptlet.requiresTrust === true,
|
|
};
|
|
resourceDetails.set(name, entry);
|
|
if ( Array.isArray(aliases) === false ) { continue; }
|
|
for ( const alias of aliases ) {
|
|
resourceAliases.set(alias, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
init();
|
|
|
|
/******************************************************************************/
|