[mv3] Generate at most two scriptlet-related files per rulesets

Related issue:
https://github.com/uBlockOrigin/uBOL-home/issues/557
This commit is contained in:
Raymond Hill 2025-12-04 12:57:25 -05:00
parent d5793b83f2
commit 6039ef2b6d
No known key found for this signature in database
GPG key ID: F5630CAE62A14316
5 changed files with 244 additions and 146 deletions

View file

@ -434,29 +434,24 @@ function registerScriptlet(context, scriptletDetails) {
];
for ( const rulesetId of rulesetsDetails.map(v => v.id) ) {
const scriptletList = scriptletDetails.get(rulesetId);
if ( scriptletList === undefined ) { continue; }
for ( const [ token, details ] of scriptletList ) {
const id = `${rulesetId}.${token}`;
const registered = before.get(id);
const worlds = scriptletDetails.get(rulesetId);
if ( worlds === undefined ) { continue; }
for ( const world of Object.keys(worlds) ) {
const id = `${rulesetId}.${world.toLowerCase()}`;
const matches = [];
const excludeMatches = [];
const hostnames = worlds[world];
let targetHostnames = [];
if ( hasBroadHostPermission ) {
excludeMatches.push(...permissionRevokedMatches);
if ( details.hostnames.length > 100 ) {
targetHostnames = [ '*' ];
} else {
targetHostnames = details.hostnames;
}
targetHostnames = hostnames;
} else if ( permissionGrantedHostnames.length !== 0 ) {
if ( details.hostnames.includes('*') ) {
if ( hostnames.includes('*') ) {
targetHostnames = permissionGrantedHostnames;
} else {
targetHostnames = ut.intersectHostnameIters(
details.hostnames,
hostnames,
permissionGrantedHostnames
);
}
@ -465,16 +460,17 @@ function registerScriptlet(context, scriptletDetails) {
matches.push(...ut.matchesFromHostnames(targetHostnames));
normalizeMatches(matches);
const registered = before.get(id);
before.delete(id); // Important!
const directive = {
id,
js: [ `/rulesets/scripting/scriptlet/${id}.js` ],
js: [ `/rulesets/scripting/scriptlet/${world.toLowerCase()}/${rulesetId}.js` ],
matches,
allFrames: true,
matchOriginAsFallback: true,
runAt: 'document_start',
world: details.world,
world,
};
if ( excludeMatches.length !== 0 ) {
directive.excludeMatches = excludeMatches;

View file

@ -101,6 +101,7 @@ const consoleLog = console.log;
const stdOutput = [];
const log = (text, silent = true) => {
silent = silent && text.startsWith('!!!') === false;
stdOutput.push(text);
if ( silent === false ) {
consoleLog(text);

View file

@ -27,24 +27,37 @@ import { safeReplace } from './safe-replace.js';
const resourceDetails = new Map();
const resourceAliases = new Map();
const scriptletFiles = new Map();
const worldTemplate = {
scriptletFunctions: new Map(),
allFunctions: new Map(),
args: new Map(),
arglists: new Map(),
hostnames: new Map(),
matches: new Set(),
hasEntities: false,
hasAncestors: false,
};
const worlds = {
ISOLATED: structuredClone(worldTemplate),
MAIN: structuredClone(worldTemplate),
};
/******************************************************************************/
function createScriptletCoreCode(scriptletToken) {
const details = resourceDetails.get(scriptletToken);
const components = new Map([ [ scriptletToken, details.code ] ]);
const dependencies = details.dependencies && details.dependencies.slice() || [];
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();
if ( components.has(token) ) { continue; }
const details = resourceDetails.get(token);
if ( details === undefined ) { continue; }
components.set(token, details.code);
if ( allFunctions.has(details.name) ) { continue; }
allFunctions.set(details.name, details.code);
if ( Array.isArray(details.dependencies) === false ) { continue; }
dependencies.push(...details.dependencies);
}
return Array.from(components.values()).join('\n\n');
}
/******************************************************************************/
@ -70,7 +83,8 @@ export function init() {
/******************************************************************************/
export function reset() {
scriptletFiles.clear();
worlds.ISOLATED = structuredClone(worldTemplate);
worlds.MAIN = structuredClone(worldTemplate);
}
/******************************************************************************/
@ -85,56 +99,58 @@ export function compile(assetDetails, details) {
const scriptletToken = details.args[0];
const resourceEntry = resourceDetails.get(scriptletToken);
if ( resourceEntry === undefined ) { return; }
const argsToken = JSON.stringify(details.args.slice(1));
if ( resourceEntry.requiresTrust && details.trustedSource !== true ) {
console.log(`Rejecting +js(${scriptletToken},${argsToken.slice(1,-1)}): ${assetDetails.id} is not trusted`);
console.log(`Rejecting +js(${details.args.join()}): ${assetDetails.id} 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(),
exceptions: new Map(),
hasEntities: false,
hasAncestors: false,
matches: new Set(),
});
const worldDetails = worlds[resourceEntry.world];
const { scriptletFunctions } = worldDetails;
if ( scriptletFunctions.has(resourceEntry.name) === false ) {
scriptletFunctions.set(resourceEntry.name, scriptletFunctions.size);
createScriptletCoreCode(worldDetails, resourceEntry);
}
const scriptletDetails = scriptletFiles.get(scriptletToken);
if ( scriptletDetails.args.has(argsToken) === false ) {
scriptletDetails.args.set(argsToken, scriptletDetails.args.size);
// 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 iArgs = scriptletDetails.args.get(argsToken);
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 ) {
const isEntity = hn.endsWith('.*') || hn.endsWith('.*>>');
scriptletDetails.hasEntities ||= isEntity;
worldDetails.hasEntities ||= isEntity;
const isAncestor = hn.endsWith('>>')
scriptletDetails.hasAncestors ||= isAncestor;
worldDetails.hasAncestors ||= isAncestor;
if ( isEntity || isAncestor ) {
scriptletDetails.matches.clear();
scriptletDetails.matches.add('*');
worldDetails.matches.clear();
worldDetails.matches.add('*');
}
if ( scriptletDetails.matches.has('*') === false ) {
scriptletDetails.matches.add(hn);
if ( worldDetails.matches.has('*') === false ) {
worldDetails.matches.add(hn);
}
if ( scriptletDetails.hostnames.has(hn) === false ) {
scriptletDetails.hostnames.set(hn, new Set());
if ( worldDetails.hostnames.has(hn) === false ) {
worldDetails.hostnames.set(hn, new Set());
}
scriptletDetails.hostnames.get(hn).add(iArgs);
worldDetails.hostnames.get(hn).add(arglistIndex);
}
} else {
scriptletDetails.matches.add('*');
worldDetails.matches.add('*');
}
if ( details.excludeMatches ) {
for ( const hn of details.excludeMatches ) {
if ( scriptletDetails.exceptions.has(hn) === false ) {
scriptletDetails.exceptions.set(hn, []);
if ( worldDetails.hostnames.has(hn) === false ) {
worldDetails.hostnames.set(hn, new Set());
}
scriptletDetails.exceptions.get(hn).push(iArgs);
worldDetails.hostnames.get(hn).add(~arglistIndex);
}
}
}
@ -146,51 +162,57 @@ export async function commit(rulesetId, path, writeFn) {
'./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);
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)]));
let content = safeReplace(scriptletTemplate, /\$rulesetId\$/, rulesetId, 0);
if ( worldDetails.hasEntities ) {
content = safeReplace(content,
'const $hasEntities$ = false;',
'const $hasEntities$ = true;'
);
}
if ( worldDetails.hasAncestors ) {
content = safeReplace(content,
'const $hasAncestors$ = false;',
'const $hasAncestors$ = true;'
);
};
content = safeReplace(content,
'self.$argsList$',
JSON.stringify(Array.from(details.args.keys()).map(a => JSON.parse(a)))
'const $scriptletHostnames$ = [];',
`const $scriptletHostnames$ = /* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[0]))};`
);
content = safeReplace(content,
'self.$hostnamesMap$',
JSON.stringify(patchHnMap(details.hostnames))
'const $scriptletArglistRefs$ = [];',
`const $scriptletArglistRefs$ = /* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[1]).join(';'))};`
);
content = safeReplace(content,
'self.$hasEntities$',
JSON.stringify(details.hasEntities)
'const $scriptletArglists$ = [];',
`const $scriptletArglists$ = /* ${arglists.size} */ ${JSON.stringify(Array.from(arglists.keys()).join(';'))};`
);
content = safeReplace(content,
'self.$hasAncestors$',
JSON.stringify(details.hasAncestors)
'const $scriptletArgs$ = [];',
`const $scriptletArgs$ = /* ${args.size} */ ${JSON.stringify(Array.from(args.keys()).join('\n'))};`
);
content = safeReplace(content,
'self.$exceptionsMap$',
JSON.stringify(Array.from(details.exceptions))
'const $scriptletFunctions$ = [];',
`const $scriptletFunctions$ = /* ${scriptletFunctions.size} */\n[${Array.from(scriptletFunctions.keys()).join(',')}];`
);
writeFn(`${path}/${rulesetId}.${name}`, content);
scriptletStats.push([
name.slice(0, -3), {
hostnames: Array.from(details.matches).sort(),
world: details.world,
}
]);
content = safeReplace(content,
'function $scriptletCode$(){} // eslint-disable-line',
Array.from(allFunctions.values()).join('\n\n')
);
writeFn(`${path}/${world.toLowerCase()}/${rulesetId}.js`, content);
stats[world] = Array.from(worldDetails.matches).sort();
}
return scriptletStats;
return stats;
}
/******************************************************************************/

View file

@ -26,53 +26,30 @@
// Isolate from global scope
// Start of local scope
(function uBOL_$scriptletName$() {
(function uBOL_scriptlets() {
/******************************************************************************/
function $scriptletName$(){}
function $scriptletCode$(){} // eslint-disable-line
/******************************************************************************/
const scriptletGlobals = {}; // eslint-disable-line
const argsList = self.$argsList$;
const hostnamesMap = new Map(self.$hostnamesMap$);
const exceptionsMap = new Map(self.$exceptionsMap$);
const hasEntities = self.$hasEntities$;
const hasAncestors = self.$hasAncestors$;
const collectArgIndices = (hn, map, out) => {
let argsIndices = map.get(hn);
if ( argsIndices === undefined ) { return; }
if ( typeof argsIndices !== 'number' ) {
for ( const argsIndex of argsIndices ) {
out.add(argsIndex);
}
} else {
out.add(argsIndices);
}
};
const $scriptletFunctions$ = [];
const indicesFromHostname = (hostname, suffix = '') => {
const hnParts = hostname.split('.');
const hnpartslen = hnParts.length;
if ( hnpartslen === 0 ) { return; }
for ( let i = 0; i < hnpartslen; i++ ) {
const hn = `${hnParts.slice(i).join('.')}${suffix}`;
collectArgIndices(hn, hostnamesMap, todoIndices);
collectArgIndices(hn, exceptionsMap, tonotdoIndices);
}
if ( hasEntities ) {
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('.')}.*${suffix}`;
collectArgIndices(en, hostnamesMap, todoIndices);
collectArgIndices(en, exceptionsMap, tonotdoIndices);
}
}
}
};
const $scriptletArgs$ = [];
const $scriptletArglists$ = [];
const $scriptletArglistRefs$ = [];
const $scriptletHostnames$ = [];
const $hasEntities$ = false;
const $hasAncestors$ = false;
/******************************************************************************/
const entries = (( ) => {
const docloc = document.location;
@ -81,31 +58,107 @@ const entries = (( ) => {
origins.push(...docloc.ancestorOrigins);
}
return origins.map((origin, i) => {
const beg = origin.lastIndexOf('://');
const beg = origin.indexOf('://');
if ( beg === -1 ) { return; }
const hn = origin.slice(beg+3)
const end = hn.indexOf(':');
return { hn: end === -1 ? hn : hn.slice(0, end), i };
const hn1 = origin.slice(beg+3)
const end = hn1.indexOf(':');
const hn2 = end === -1 ? hn1 : hn1.slice(0, end);
const hnParts = hn2.split('.');
if ( hn2.length === 0 ) { return; }
const hns = [];
for ( let i = 0; i < hnParts.length; i++ ) {
hns.push(`${hnParts.slice(i).join('.')}`);
}
const ens = [];
if ( $hasEntities$ ) {
const n = hnParts.length - 1;
for ( let i = 0; i < n; i++ ) {
for ( let j = n; j > i; j-- ) {
ens.push(`${hnParts.slice(i,j).join('.')}.*`);
}
}
ens.sort((a, b) => {
const d = b.length - a.length;
if ( d !== 0 ) { return d; }
return a > b ? -1 : 1;
});
}
return { hns, ens, i };
}).filter(a => a !== undefined);
})();
if ( entries.length === 0 ) { return; }
const todoIndices = new Set();
const tonotdoIndices = new Set();
const collectArglistRefIndices = (out, hn, r) => {
let l = 0, i = 0, d = 0;
let candidate = '';
while ( l < r ) {
i = l + r >>> 1;
candidate = $scriptletHostnames$[i];
d = hn.length - candidate.length;
if ( d === 0 ) {
if ( hn === candidate ) {
out.add(i); break;
}
d = hn < candidate ? -1 : 1;
}
if ( d < 0 ) {
r = i;
} else {
l = i + 1;
}
}
return i;
};
indicesFromHostname(entries[0].hn);
if ( hasAncestors ) {
const indicesFromHostname = (out, hnDetails, suffix = '') => {
if ( hnDetails.hns.length === 0 ) { return; }
let r = $scriptletHostnames$.length;
for ( const hn of hnDetails.hns ) {
r = collectArglistRefIndices(out, `${hn}${suffix}`, r);
}
if ( $hasEntities$ ) {
let r = $scriptletHostnames$.length;
for ( const en of hnDetails.ens ) {
r = collectArglistRefIndices(out, `${en}${suffix}`, r);
}
}
};
const todoIndices = new Set();
indicesFromHostname(todoIndices, entries[0]);
if ( $hasAncestors$ ) {
for ( const entry of entries ) {
if ( entry.i === 0 ) { continue; }
indicesFromHostname(entry.hn, '>>');
indicesFromHostname(todoIndices, entry, '>>');
}
}
$scriptletHostnames$.length = 0;
if ( todoIndices.size === 0 ) { return; }
// Collect arglist references
const todo = new Set();
{
const arglistRefs = $scriptletArglistRefs$.split(';');
for ( const i of todoIndices ) {
for ( const ref of JSON.parse(`[${arglistRefs[i]}]`) ) {
todo.add(ref);
}
}
}
// Apply scriplets
for ( const i of todoIndices ) {
if ( tonotdoIndices.has(i) ) { continue; }
try { $scriptletName$(...argsList[i]); }
catch { }
// Execute scriplets
{
const arglists = $scriptletArglists$.split(';');
const args = $scriptletArgs$.split('\n');
for ( const ref of todo ) {
if ( ref < 0 ) { continue; }
if ( todo.has(~ref) ) { continue; }
const arglist = JSON.parse(`[${arglists[ref]}]`);
const fn = $scriptletFunctions$[arglist[0]];
try { fn(...arglist.slice(1).map(a => args[a])); }
catch { }
}
}
/******************************************************************************/

View file

@ -47,6 +47,7 @@ export function setLocalStorageItemFn(
trusted = false,
key = '',
value = '',
options = {}
) {
if ( key === '' ) { return; }
@ -86,6 +87,8 @@ export function setLocalStorageItemFn(
}
}
let modified = false;
try {
const storage = self[`${which}Storage`];
if ( value === '$remove$' ) {
@ -96,14 +99,25 @@ export function setLocalStorageItemFn(
const key = storage.key(i);
if ( pattern.test(key) ) { toRemove.push(key); }
}
modified = toRemove.length !== 0;
for ( const key of toRemove ) {
storage.removeItem(key);
}
} else {
storage.setItem(key, `${value}`);
const before = storage.getItem(key);
const after = `${value}`;
modified = after !== before;
if ( modified ) {
storage.setItem(key, after);
}
}
} catch {
}
if ( modified && typeof options.reload === 'number' ) {
setTimeout(( ) => { window.location.reload(); }, options.reload);
}
}
registerScriptlet(setLocalStorageItemFn, {
name: 'set-local-storage-item.fn',
@ -178,23 +192,29 @@ registerScriptlet(removeCacheStorageItem, {
**/
export function setLocalStorageItem(key = '', value = '') {
setLocalStorageItemFn('local', false, key, value);
const safe = safeSelf();
const options = safe.getExtraArgs(Array.from(arguments), 2)
setLocalStorageItemFn('local', false, key, value, options);
}
registerScriptlet(setLocalStorageItem, {
name: 'set-local-storage-item.js',
world: 'ISOLATED',
dependencies: [
safeSelf,
setLocalStorageItemFn,
],
});
export function setSessionStorageItem(key = '', value = '') {
setLocalStorageItemFn('session', false, key, value);
const safe = safeSelf();
const options = safe.getExtraArgs(Array.from(arguments), 2)
setLocalStorageItemFn('session', false, key, value, options);
}
registerScriptlet(setSessionStorageItem, {
name: 'set-session-storage-item.js',
world: 'ISOLATED',
dependencies: [
safeSelf,
setLocalStorageItemFn,
],
});
@ -211,25 +231,31 @@ registerScriptlet(setSessionStorageItem, {
**/
export function trustedSetLocalStorageItem(key = '', value = '') {
setLocalStorageItemFn('local', true, key, value);
const safe = safeSelf();
const options = safe.getExtraArgs(Array.from(arguments), 2)
setLocalStorageItemFn('local', true, key, value, options);
}
registerScriptlet(trustedSetLocalStorageItem, {
name: 'trusted-set-local-storage-item.js',
requiresTrust: true,
world: 'ISOLATED',
dependencies: [
safeSelf,
setLocalStorageItemFn,
],
});
export function trustedSetSessionStorageItem(key = '', value = '') {
setLocalStorageItemFn('session', true, key, value);
const safe = safeSelf();
const options = safe.getExtraArgs(Array.from(arguments), 2)
setLocalStorageItemFn('session', true, key, value, options);
}
registerScriptlet(trustedSetSessionStorageItem, {
name: 'trusted-set-session-storage-item.js',
requiresTrust: true,
world: 'ISOLATED',
dependencies: [
safeSelf,
setLocalStorageItemFn,
],
});