From c78d6510eabea18f0f9bca2ea29834d3835a5701 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 22 Oct 2019 08:48:40 +0200
Subject: [PATCH] refactor: use re2 for regex (#4687)

---
 lib/config/presets.ts                         |  3 +-
 lib/config/validation.ts                      | 51 ++++++++-----------
 lib/datasource/go/index.ts                    |  3 +-
 lib/manager/ansible/update.ts                 |  3 +-
 lib/manager/bazel/extract.ts                  |  3 +-
 lib/manager/bazel/update.ts                   |  5 +-
 lib/manager/buildkite/update.ts               |  3 +-
 lib/manager/bundler/extract.ts                | 15 +++---
 package.json                                  |  1 -
 .../__snapshots__/validation.spec.ts.snap     |  6 +--
 test/config/validation.spec.ts                | 17 ++++++-
 11 files changed, 58 insertions(+), 52 deletions(-)

diff --git a/lib/config/presets.ts b/lib/config/presets.ts
index 95933fa9d6..9af6b7b778 100644
--- a/lib/config/presets.ts
+++ b/lib/config/presets.ts
@@ -7,6 +7,7 @@ import * as npm from '../datasource/npm';
 import * as gitlab from '../datasource/gitlab';
 import { RenovateConfig } from './common';
 import { mergeChildConfig } from './utils';
+import { regEx } from '../util/regex';
 
 const datasources = {
   github,
@@ -126,7 +127,7 @@ export function replaceArgs(
   if (is.string(obj)) {
     let returnStr = obj;
     for (const [arg, argVal] of Object.entries(argMapping)) {
-      const re = new RegExp(`{{${arg}}}`, 'g');
+      const re = regEx(`{{${arg}}}`, 'g');
       returnStr = returnStr.replace(re, argVal);
     }
     return returnStr;
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index 91984b7cfe..c95f3a09f5 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -1,10 +1,10 @@
 import is from '@sindresorhus/is';
-import safe from 'safe-regex';
 import { getOptions, RenovateOptions } from './definitions';
 import { resolveConfigPresets } from './presets';
 import { hasValidSchedule, hasValidTimezone } from '../workers/branch/schedule';
 import * as managerValidator from './validation-helpers/managers';
 import { RenovateConfig, ValidationMessage } from './common';
+import { regEx } from '../util/regex';
 
 const options = getOptions();
 
@@ -178,41 +178,30 @@ export async function validateConfig(
                 }
               }
             }
-            if (
-              (key === 'packagePatterns' || key === 'excludePackagePatterns') &&
-              !(val && val.length === 1 && val[0] === '*')
-            ) {
-              try {
-                RegExp(val as any);
-                if (!safe(val as any)) {
-                  errors.push({
-                    depName: 'Configuration Error',
-                    message: `Unsafe regExp for ${currentPath}: \`${val}\``,
-                  });
-                }
-              } catch (e) {
-                errors.push({
-                  depName: 'Configuration Error',
-                  message: `Invalid regExp for ${currentPath}: \`${val}\``,
-                });
-              }
-            }
-            if (key === 'fileMatch') {
-              try {
-                for (const fileMatch of val) {
-                  RegExp(fileMatch);
-                  if (!safe(fileMatch)) {
+            if (key === 'packagePatterns' || key === 'excludePackagePatterns') {
+              for (const pattern of val) {
+                if (pattern !== '*') {
+                  try {
+                    regEx(pattern);
+                  } catch (e) {
                     errors.push({
                       depName: 'Configuration Error',
-                      message: `Unsafe regExp for ${currentPath}: \`${fileMatch}\``,
+                      message: `Invalid regExp for ${currentPath}: \`${pattern}\``,
                     });
                   }
                 }
-              } catch (e) {
-                errors.push({
-                  depName: 'Configuration Error',
-                  message: `Invalid regExp for ${currentPath}: \`${val}\``,
-                });
+              }
+            }
+            if (key === 'fileMatch') {
+              for (const fileMatch of val) {
+                try {
+                  regEx(fileMatch);
+                } catch (e) {
+                  errors.push({
+                    depName: 'Configuration Error',
+                    message: `Invalid regExp for ${currentPath}: \`${fileMatch}\``,
+                  });
+                }
               }
             }
             if (
diff --git a/lib/datasource/go/index.ts b/lib/datasource/go/index.ts
index 9bbfde96b5..b555235fce 100644
--- a/lib/datasource/go/index.ts
+++ b/lib/datasource/go/index.ts
@@ -2,6 +2,7 @@ import { logger } from '../../logger';
 import got from '../../util/got';
 import * as github from '../github';
 import { DigestConfig, PkgReleaseConfig, ReleaseResult } from '../common';
+import { regEx } from '../../util/regex';
 
 interface DataSource {
   datasource: string;
@@ -30,7 +31,7 @@ async function getDatasource(name: string): Promise<DataSource | null> {
       hostType: 'go',
     })).body;
     const sourceMatch = res.match(
-      new RegExp(`<meta\\s+name="go-source"\\s+content="${name}\\s+([^\\s]+)`)
+      regEx(`<meta\\s+name="go-source"\\s+content="${name}\\s+([^\\s]+)`)
     );
     if (sourceMatch) {
       const [, goSourceUrl] = sourceMatch;
diff --git a/lib/manager/ansible/update.ts b/lib/manager/ansible/update.ts
index a24b532877..8b7b2282f6 100644
--- a/lib/manager/ansible/update.ts
+++ b/lib/manager/ansible/update.ts
@@ -1,6 +1,7 @@
 import { logger } from '../../logger';
 import { getNewFrom } from '../dockerfile/update';
 import { Upgrade } from '../common';
+import { regEx } from '../../util/regex';
 
 export default function updateDependency(
   fileContent: string,
@@ -11,7 +12,7 @@ export default function updateDependency(
     logger.debug(`ansible.updateDependency(): ${newFrom}`);
     const lines = fileContent.split('\n');
     const lineToChange = lines[upgrade.managerData.lineNumber];
-    const imageLine = new RegExp(/^(\s*image:\s*'?"?)[^\s'"]+('?"?\s*)$/);
+    const imageLine = regEx(`^(\\s*image:\\s*'?"?)[^\\s'"]+('?"?\\s*)$`);
     if (!lineToChange.match(imageLine)) {
       logger.debug('No image line found');
       return null;
diff --git a/lib/manager/bazel/extract.ts b/lib/manager/bazel/extract.ts
index f97ba65b08..ae420a6d53 100644
--- a/lib/manager/bazel/extract.ts
+++ b/lib/manager/bazel/extract.ts
@@ -3,6 +3,7 @@ import parse from 'github-url-from-git';
 import { parse as _parse } from 'url';
 import { logger } from '../../logger';
 import { PackageDependency, PackageFile } from '../common';
+import { regEx } from '../../util/regex';
 
 interface UrlParsedResult {
   repo: string;
@@ -78,7 +79,7 @@ function parseContent(content: string): string[] {
     (acc, prefix) => [
       ...acc,
       ...content
-        .split(new RegExp(prefix + '\\s*\\(', 'g'))
+        .split(regEx(prefix + '\\s*\\(', 'g'))
         .slice(1)
         .map(base => {
           const ind = findBalancedParenIndex(base);
diff --git a/lib/manager/bazel/update.ts b/lib/manager/bazel/update.ts
index 3f55a4a21a..7f9f4e55d0 100644
--- a/lib/manager/bazel/update.ts
+++ b/lib/manager/bazel/update.ts
@@ -2,6 +2,7 @@ import { fromStream } from 'hasha';
 import got from '../../util/got';
 import { logger } from '../../logger';
 import { Upgrade } from '../common';
+import { regEx } from '../../util/regex';
 
 function updateWithNewVersion(
   content: string,
@@ -137,7 +138,7 @@ export async function updateDependency(
       const hash = await getHashFromUrl(url);
       newDef = setNewHash(upgrade.managerData.def, hash);
       newDef = newDef.replace(
-        new RegExp(`(strip_prefix\\s*=\\s*)"[^"]*"`),
+        regEx(`(strip_prefix\\s*=\\s*)"[^"]*"`),
         `$1"${shortRepo}-${upgrade.newDigest}"`
       );
       const match =
@@ -151,7 +152,7 @@ export async function updateDependency(
     if (newDef.endsWith('\n')) {
       existingRegExStr += '\n';
     }
-    const existingDef = new RegExp(existingRegExStr);
+    const existingDef = regEx(existingRegExStr);
     // istanbul ignore if
     if (!fileContent.match(existingDef)) {
       logger.info('Cannot match existing string');
diff --git a/lib/manager/buildkite/update.ts b/lib/manager/buildkite/update.ts
index 14e7da9691..943cd4e09d 100644
--- a/lib/manager/buildkite/update.ts
+++ b/lib/manager/buildkite/update.ts
@@ -1,5 +1,6 @@
 import { logger } from '../../logger';
 import { Upgrade } from '../common';
+import { regEx } from '../../util/regex';
 
 export function updateDependency(
   currentFileContent: string,
@@ -10,7 +11,7 @@ export function updateDependency(
     logger.debug(`buildkite.updateDependency: ${upgrade.newValue}`);
     const lines = currentFileContent.split('\n');
     const lineToChange = lines[lineIdx];
-    const depLine = new RegExp(/^(\s+[^#]+#)[^:]+(:.*)$/);
+    const depLine = regEx(`^(\\s+[^#]+#)[^:]+(:.*)$`);
     if (!lineToChange.match(depLine)) {
       logger.debug('No image line found');
       return null;
diff --git a/lib/manager/bundler/extract.ts b/lib/manager/bundler/extract.ts
index 7d7da5032c..3deb6481f9 100644
--- a/lib/manager/bundler/extract.ts
+++ b/lib/manager/bundler/extract.ts
@@ -2,6 +2,7 @@ import { logger } from '../../logger';
 import { isValid } from '../../versioning/ruby';
 import { PackageFile, PackageDependency } from '../common';
 import { platform } from '../../platform';
+import { regEx } from '../../util/regex';
 
 export { extractPackageFile };
 
@@ -22,7 +23,7 @@ async function extractPackageFile(
       sourceMatch =
         sourceMatch ||
         line.match(
-          new RegExp(`^source ${delimiter}([^${delimiter}]+)${delimiter}\\s*$`)
+          regEx(`^source ${delimiter}([^${delimiter}]+)${delimiter}\\s*$`)
         );
     }
     if (sourceMatch) {
@@ -32,9 +33,7 @@ async function extractPackageFile(
     for (const delimiter of delimiters) {
       rubyMatch =
         rubyMatch ||
-        line.match(
-          new RegExp(`^ruby ${delimiter}([^${delimiter}]+)${delimiter}`)
-        );
+        line.match(regEx(`^ruby ${delimiter}([^${delimiter}]+)${delimiter}`));
     }
     if (rubyMatch) {
       res.compatibility = { ruby: rubyMatch[1] };
@@ -43,9 +42,9 @@ async function extractPackageFile(
     let gemDelimiter: string;
     for (const delimiter of delimiters) {
       const gemMatchRegex = `^gem ${delimiter}([^${delimiter}]+)${delimiter}(,\\s+${delimiter}([^${delimiter}]+)${delimiter}){0,2}`;
-      if (line.match(new RegExp(gemMatchRegex))) {
+      if (line.match(regEx(gemMatchRegex))) {
         gemDelimiter = delimiter;
-        gemMatch = gemMatch || line.match(new RegExp(gemMatchRegex));
+        gemMatch = gemMatch || line.match(regEx(gemMatchRegex));
       }
     }
     if (gemMatch) {
@@ -56,7 +55,7 @@ async function extractPackageFile(
       if (gemMatch[3]) {
         dep.currentValue = gemMatch[0]
           .substring(`gem ${gemDelimiter}${dep.depName}${gemDelimiter},`.length)
-          .replace(new RegExp(gemDelimiter, 'g'), '')
+          .replace(regEx(gemDelimiter, 'g'), '')
           .trim();
         if (!isValid(dep.currentValue)) {
           dep.skipReason = 'invalid-value';
@@ -100,7 +99,7 @@ async function extractPackageFile(
     }
     for (const delimiter of delimiters) {
       const sourceBlockMatch = line.match(
-        new RegExp(`^source\\s+${delimiter}(.*?)${delimiter}\\s+do`)
+        regEx(`^source\\s+${delimiter}(.*?)${delimiter}\\s+do`)
       );
       if (sourceBlockMatch) {
         const repositoryUrl = sourceBlockMatch[1];
diff --git a/package.json b/package.json
index 1b18850013..e616755b4f 100644
--- a/package.json
+++ b/package.json
@@ -142,7 +142,6 @@
     "parse-link-header": "1.0.1",
     "pnpm": "3.8.1",
     "registry-auth-token": "4.0.0",
-    "safe-regex": "2.0.2",
     "semver": "6.3.0",
     "semver-stable": "3.0.0",
     "semver-utils": "1.1.4",
diff --git a/test/config/__snapshots__/validation.spec.ts.snap b/test/config/__snapshots__/validation.spec.ts.snap
index efe8cdf624..478ea19788 100644
--- a/test/config/__snapshots__/validation.spec.ts.snap
+++ b/test/config/__snapshots__/validation.spec.ts.snap
@@ -57,11 +57,11 @@ exports[`config/validation validateConfig(config) errors for unsafe fileMatches
 Array [
   Object {
     "depName": "Configuration Error",
-    "message": "Invalid regExp for npm.fileMatch: \`abc ([a-z]+) ([a-z]+))\`",
+    "message": "Invalid regExp for docker.fileMatch: \`x?+\`",
   },
   Object {
     "depName": "Configuration Error",
-    "message": "Unsafe regExp for docker.fileMatch: \`(x+x+)+y\`",
+    "message": "Invalid regExp for npm.fileMatch: \`abc ([a-z]+) ([a-z]+))\`",
   },
 ]
 `;
@@ -113,7 +113,7 @@ Array [
   },
   Object {
     "depName": "Configuration Error",
-    "message": "Unsafe regExp for packageRules[0].excludePackagePatterns: \`(x+x+)+y\`",
+    "message": "Invalid regExp for packageRules[0].excludePackagePatterns: \`abc ([a-z]+) ([a-z]+))\`",
   },
 ]
 `;
diff --git a/test/config/validation.spec.ts b/test/config/validation.spec.ts
index 38f4ad7be9..ca5bc93b46 100644
--- a/test/config/validation.spec.ts
+++ b/test/config/validation.spec.ts
@@ -20,7 +20,7 @@ describe('config/validation', () => {
         packageRules: [
           {
             packagePatterns: ['*'],
-            excludePackagePatterns: ['(x+x+)+y'],
+            excludePackagePatterns: ['abc ([a-z]+) ([a-z]+))'],
           },
         ],
         lockFileMaintenance: {
@@ -138,13 +138,14 @@ describe('config/validation', () => {
       expect(errors).toMatchSnapshot();
       expect(errors).toHaveLength(0);
     });
+
     it('errors for unsafe fileMatches', async () => {
       const config = {
         npm: {
           fileMatch: ['abc ([a-z]+) ([a-z]+))'],
         },
         docker: {
-          fileMatch: ['(x+x+)+y'],
+          fileMatch: ['x?+'],
         },
       };
       const { warnings, errors } = await configValidation.validateConfig(
@@ -154,5 +155,17 @@ describe('config/validation', () => {
       expect(errors).toHaveLength(2);
       expect(errors).toMatchSnapshot();
     });
+
+    it('validates regEx for each fileMatch', async () => {
+      const config = {
+        fileMatch: ['js', '***$}{]]['],
+      };
+      const { warnings, errors } = await configValidation.validateConfig(
+        config,
+        true
+      );
+      expect(warnings).toHaveLength(0);
+      expect(errors).toHaveLength(1);
+    });
   });
 });
-- 
GitLab