summaryrefslogtreecommitdiffstats
path: root/platform/mv3/make-scriptlets.js
diff options
context:
space:
mode:
Diffstat (limited to 'platform/mv3/make-scriptlets.js')
-rw-r--r--platform/mv3/make-scriptlets.js193
1 files changed, 193 insertions, 0 deletions
diff --git a/platform/mv3/make-scriptlets.js b/platform/mv3/make-scriptlets.js
new file mode 100644
index 0000000..d276a56
--- /dev/null
+++ b/platform/mv3/make-scriptlets.js
@@ -0,0 +1,193 @@
+/*******************************************************************************
+
+ 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
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+import fs from 'fs/promises';
+import { builtinScriptlets } from './scriptlets.js';
+import { safeReplace } from './safe-replace.js';
+
+/******************************************************************************/
+
+const resourceDetails = new Map();
+const resourceAliases = new Map();
+const scriptletFiles = new Map();
+
+/******************************************************************************/
+
+function createScriptletCoreCode(scriptletToken) {
+ const details = resourceDetails.get(scriptletToken);
+ const components = new Map([ [ scriptletToken, details.code ] ]);
+ const dependencies = details.dependencies && details.dependencies.slice() || [];
+ while ( dependencies.length !== 0 ) {
+ const token = dependencies.shift();
+ if ( components.has(token) ) { continue; }
+ const details = resourceDetails.get(token);
+ if ( details === undefined ) { continue; }
+ components.set(token, details.code);
+ if ( Array.isArray(details.dependencies) === false ) { continue; }
+ dependencies.push(...details.dependencies);
+ }
+ return Array.from(components.values()).join('\n\n');
+}
+
+/******************************************************************************/
+
+export 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);
+ }
+ }
+}
+
+/******************************************************************************/
+
+export function reset() {
+ scriptletFiles.clear();
+}
+
+/******************************************************************************/
+
+export function compile(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 ${scriptletToken}: source is not trusted`);
+ return;
+ }
+ if ( scriptletFiles.has(scriptletToken) === false ) {
+ scriptletFiles.set(scriptletToken, {
+ name: resourceEntry.name,
+ code: createScriptletCoreCode(scriptletToken),
+ world: resourceEntry.world,
+ args: new Map(),
+ hostnames: new Map(),
+ entities: new Map(),
+ exceptions: new Map(),
+ matches: new Set(),
+ });
+ }
+ const scriptletDetails = scriptletFiles.get(scriptletToken);
+ const argsToken = JSON.stringify(details.args.slice(1));
+ if ( scriptletDetails.args.has(argsToken) === false ) {
+ scriptletDetails.args.set(argsToken, scriptletDetails.args.size);
+ }
+ const iArgs = scriptletDetails.args.get(argsToken);
+ if ( details.matches ) {
+ for ( const hn of details.matches ) {
+ if ( hn.endsWith('.*') ) {
+ scriptletDetails.matches.clear();
+ scriptletDetails.matches.add('*');
+ const entity = hn.slice(0, -2);
+ if ( scriptletDetails.entities.has(entity) === false ) {
+ scriptletDetails.entities.set(entity, new Set());
+ }
+ scriptletDetails.entities.get(entity).add(iArgs);
+ } else {
+ if ( scriptletDetails.matches.has('*') === false ) {
+ scriptletDetails.matches.add(hn);
+ }
+ if ( scriptletDetails.hostnames.has(hn) === false ) {
+ scriptletDetails.hostnames.set(hn, new Set());
+ }
+ scriptletDetails.hostnames.get(hn).add(iArgs);
+ }
+ }
+ } else {
+ scriptletDetails.matches.add('*');
+ }
+ if ( details.excludeMatches ) {
+ for ( const hn of details.excludeMatches ) {
+ if ( scriptletDetails.exceptions.has(hn) === false ) {
+ scriptletDetails.exceptions.set(hn, []);
+ }
+ scriptletDetails.exceptions.get(hn).push(iArgs);
+ }
+ }
+}
+
+/******************************************************************************/
+
+export async function commit(rulesetId, path, writeFn) {
+ const scriptletTemplate = await fs.readFile(
+ './scriptlets/scriptlet.template.js',
+ { encoding: 'utf8' }
+ );
+ const patchHnMap = hnmap => {
+ const out = Array.from(hnmap);
+ out.forEach(a => {
+ const values = Array.from(a[1]);
+ a[1] = values.length === 1 ? values[0] : values;
+ });
+ return out;
+ };
+ const scriptletStats = [];
+ for ( const [ name, details ] of scriptletFiles ) {
+ let content = safeReplace(scriptletTemplate,
+ 'function $scriptletName$(){}',
+ details.code
+ );
+ content = safeReplace(content, /\$rulesetId\$/, rulesetId, 0);
+ content = safeReplace(content, /\$scriptletName\$/, details.name, 0);
+ content = safeReplace(content, '$world$', details.world);
+ content = safeReplace(content,
+ 'self.$argsList$',
+ JSON.stringify(Array.from(details.args.keys()).map(a => JSON.parse(a)))
+ );
+ content = safeReplace(content,
+ 'self.$hostnamesMap$',
+ JSON.stringify(patchHnMap(details.hostnames))
+ );
+ content = safeReplace(content,
+ 'self.$entitiesMap$',
+ JSON.stringify(patchHnMap(details.entities))
+ );
+ content = safeReplace(content,
+ 'self.$exceptionsMap$',
+ JSON.stringify(Array.from(details.exceptions))
+ );
+ writeFn(`${path}/${rulesetId}.${name}`, content);
+ scriptletStats.push([ name.slice(0, -3), Array.from(details.matches).sort() ]);
+ }
+ return scriptletStats;
+}
+
+/******************************************************************************/