Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/cli/src/collect/collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ const {
killProcessTree,
} = require('@lhci/utils/src/child-process-helper.js');

/**
* Escapes special characters in a string so it can be used as a literal pattern in a RegExp.
* @param {string} value
* @return {string}
*/
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
* @param {import('yargs').Argv} yargs
*/
Expand Down Expand Up @@ -159,7 +168,8 @@ async function startServerAndDetermineUrls(options) {

let close = async () => undefined;
if (options.startServerCommand) {
const regexPattern = new RegExp(options.startServerReadyPattern, 'i');
const safePattern = escapeRegExp(String(options.startServerReadyPattern || ''));
const regexPattern = new RegExp(safePattern, 'i');
const {child, patternMatch, stdout, stderr} = await runCommandAndWaitForPattern(
options.startServerCommand,
regexPattern,
Expand Down
12 changes: 11 additions & 1 deletion packages/utils/src/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,17 @@ function getCategoryAssertionResults(auditProperty, assertionOptions, lhrs) {
* @return {boolean}
*/
function doesLHRMatchPattern(pattern, lhr) {
return new RegExp(pattern).test(lhr.finalUrl);
let regex;
try {
regex = new RegExp(pattern);
} catch (err) {
// Surface a clearer error message when the provided pattern is not a valid regular expression.
throw new Error(
`Invalid matchingUrlPattern "${pattern}": ${(/** @type {Error} */ (err)).message}`
);
}

return regex.test(lhr.finalUrl);
}

/**
Expand Down
12 changes: 11 additions & 1 deletion packages/utils/src/build-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
const crypto = require('crypto');
const childProcess = require('child_process');

/**
* Escape special characters in a string so it can be used safely inside a RegExp.
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/** @param {Array<string>} namesInPriorityOrder @return {string|undefined} */
function getEnvVarIfSet(namesInPriorityOrder) {
for (const name of namesInPriorityOrder) {
Expand Down Expand Up @@ -372,7 +381,8 @@ function getGitHubRepoSlug(apiHost = undefined) {
if (remote && apiHost && !apiHost.includes('github.com')) {
const hostMatch = apiHost.match(/:\/\/(.*?)(\/|$)/);
if (!hostMatch) return undefined;
const remoteRegex = new RegExp(`${hostMatch[1]}(:|\\/)([^/]+\\/.+)\\.git`);
const safeHost = escapeRegExp(hostMatch[1]);
const remoteRegex = new RegExp(`${safeHost}(:|\\/)([^/]+\\/.+)\\.git`);
const remoteMatch = remote.match(remoteRegex);
if (remoteMatch) return remoteMatch[2];
}
Expand Down
39 changes: 36 additions & 3 deletions packages/utils/src/saved-reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ const LH_HTML_REPORT_REGEX = /^lhr-\d+\.html$/;
const ASSERTION_RESULTS_PATH = path.join(LHCI_DIR, 'assertion-results.json');
const URL_LINK_MAP_PATH = path.join(LHCI_DIR, 'links.json');

/**
* Escape special characters in a string to be used in a RegExp as a literal.
* This mirrors lodash's _.escapeRegExp behavior.
* @param {string} string
* @return {string}
*/
function escapeRegExp(string) {
return string.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}

function ensureDirectoryExists(baseDir = LHCI_DIR) {
if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, {recursive: true});
}
Expand Down Expand Up @@ -101,10 +111,33 @@ function replaceUrlPatterns(url, sedLikeReplacementPatterns) {
let replaced = url;

for (const pattern of sedLikeReplacementPatterns) {
const match = pattern.match(/^s(.)(.*)\1(.*)\1([gim]*)$/);
// sed-like syntax: s<delim>needle<delim>replacement<delim>[flags]
// Supports standard JS flags g/i/m and an optional custom flag "L"
// which makes the pattern literal (needle is escaped before use).
const match = pattern.match(/^s(.)(.*)\1(.*)\1([gimL]*)$/);
if (!match) throw new Error(`Invalid URL replacement pattern "${pattern}"`);
const [needle, replacement, flags] = match.slice(2);
const regex = new RegExp(needle, flags);
let [needle, replacement, flags] = match.slice(2);

const flagChars = flags.split('');
const literalIndex = flagChars.indexOf('L');
const isLiteral = literalIndex !== -1;
if (isLiteral) flagChars.splice(literalIndex, 1);

const allowedFlags = ['g', 'i', 'm'];
const seen = new Set();
for (const flag of flagChars) {
if (!allowedFlags.includes(flag) || seen.has(flag)) {
throw new Error(`Invalid flags in URL replacement pattern "${pattern}"`);
}
seen.add(flag);
}
const finalFlags = flagChars.join('');

if (isLiteral) {
needle = escapeRegExp(needle);
}

const regex = new RegExp(needle, finalFlags);
replaced = replaced.replace(regex, replacement);
}

Expand Down