[mv3] Avoid String.replace() to safely replace templates

String.replace() has side effects which are unwelcomed when
replacing template scriplets with code.
This commit is contained in:
Raymond Hill 2023-06-05 20:17:50 -04:00
parent 7e712246a9
commit 5874312b35
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
3 changed files with 111 additions and 78 deletions

View file

@ -32,6 +32,7 @@ import redirectResourcesMap from './js/redirect-resources.js';
import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js';
import * as sfp from './js/static-filtering-parser.js';
import * as makeScriptlet from './make-scriptlets.js';
import { safeReplace } from './safe-replace.js';
/******************************************************************************/
@ -400,14 +401,14 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion
const selectorLists = bucketsList.map(v => [ v[0], v[1].join(',') ]);
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-generic')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$genericSelectorMap\$/m,
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
);
let patchedScriptlet = originalScriptletMap.get('css-generic').replace(
'$rulesetId$',
assetDetails.id
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$genericSelectorMap\$/,
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
);
writeFile(
`${scriptletDir}/generic/${assetDetails.id}.js`,
@ -593,23 +594,26 @@ async function processCosmeticFilters(assetDetails, mapin) {
}
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-specific')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$argsList\$/m,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace(
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
).replace(
/\bself\.\$entitiesMap\$/m,
`${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
).replace(
/\bself\.\$exceptionsMap\$/m,
`${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}`
);
let patchedScriptlet = originalScriptletMap.get('css-specific').replace(
'$rulesetId$',
assetDetails.id
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsList\$/,
`${JSON.stringify(argsList, 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)}`
);
writeFile(`${scriptletDir}/specific/${assetDetails.id}.js`, patchedScriptlet);
generatedFiles.push(`${assetDetails.id}`);
@ -677,23 +681,26 @@ async function processDeclarativeCosmeticFilters(assetDetails, mapin) {
}
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-declarative')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$argsList\$/m,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace(
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
).replace(
/\bself\.\$entitiesMap\$/m,
`${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
).replace(
/\bself\.\$exceptionsMap\$/m,
`${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}`
);
let patchedScriptlet = originalScriptletMap.get('css-declarative').replace(
'$rulesetId$',
assetDetails.id
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsList\$/,
`${JSON.stringify(argsList, 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)}`
);
writeFile(`${scriptletDir}/declarative/${assetDetails.id}.js`, patchedScriptlet);
if ( contentArray.length !== 0 ) {
@ -760,23 +767,26 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
}
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-procedural')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$argsList\$/m,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
).replace(
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
).replace(
/\bself\.\$entitiesMap\$/m,
`${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
).replace(
/\bself\.\$exceptionsMap\$/m,
`${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}`
);
let patchedScriptlet = originalScriptletMap.get('css-procedural').replace(
'$rulesetId$',
assetDetails.id
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsList\$/,
`${JSON.stringify(argsList, 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)}`
);
writeFile(`${scriptletDir}/procedural/${assetDetails.id}.js`, patchedScriptlet);
if ( contentArray.length !== 0 ) {

View file

@ -25,6 +25,7 @@
import fs from 'fs/promises';
import { builtinScriptlets } from './scriptlets.js';
import { safeReplace } from './safe-replace.js';
/******************************************************************************/
@ -52,25 +53,6 @@ function createScriptletCoreCode(scriptletToken) {
/******************************************************************************/
function safeReplace(text, pattern, replacement, count = 1) {
const rePattern = typeof pattern === 'string'
? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
: pattern;
let out = text;
for (;;) {
const match = rePattern.exec(out);
if ( match === null ) { break; }
out = out.slice(0, match.index) +
replacement +
out.slice(match.index + match[0].length);
count -= 1;
if ( count === 0 ) { break; }
}
return out;
}
/******************************************************************************/
export function init() {
for ( const scriptlet of builtinScriptlets ) {
const { name, aliases, fn } = scriptlet;

View file

@ -0,0 +1,41 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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';
/******************************************************************************/
export function safeReplace(text, pattern, replacement, count = 1) {
const rePattern = typeof pattern === 'string'
? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
: pattern;
let out = text;
for (;;) {
const match = rePattern.exec(out);
if ( match === null ) { break; }
out = out.slice(0, match.index) +
replacement +
out.slice(match.index + match[0].length);
count -= 1;
if ( count === 0 ) { break; }
}
return out;
}