From 1567386f50a7c57f6c322dceef1c528e04f615d7 Mon Sep 17 00:00:00 2001
From: Jamie Magee <jamie.magee@gmail.com>
Date: Sat, 11 Mar 2023 21:54:12 -0800
Subject: [PATCH] refactor: safely parse `Pipfile.lock` (#20825)

---
 lib/modules/manager/pipenv/artifacts.ts | 40 ++++++++++++++++---------
 lib/modules/manager/pipenv/schema.ts    | 24 +++++++++++++++
 2 files changed, 50 insertions(+), 14 deletions(-)
 create mode 100644 lib/modules/manager/pipenv/schema.ts

diff --git a/lib/modules/manager/pipenv/artifacts.ts b/lib/modules/manager/pipenv/artifacts.ts
index 0e7e6909da..e695998471 100644
--- a/lib/modules/manager/pipenv/artifacts.ts
+++ b/lib/modules/manager/pipenv/artifacts.ts
@@ -15,11 +15,12 @@ import type {
   UpdateArtifactsConfig,
   UpdateArtifactsResult,
 } from '../types';
+import { PipfileLockSchema } from './schema';
 
 function getPythonConstraint(
   existingLockFileContent: string,
   config: UpdateArtifactsConfig
-): string | undefined | null {
+): string | undefined {
   const { constraints = {} } = config;
   const { python } = constraints;
 
@@ -28,14 +29,20 @@ function getPythonConstraint(
     return python;
   }
   try {
-    const pipfileLock = JSON.parse(existingLockFileContent);
-    if (pipfileLock?._meta?.requires?.python_version) {
-      const pythonVersion: string = pipfileLock._meta.requires.python_version;
+    const result = PipfileLockSchema.safeParse(
+      JSON.parse(existingLockFileContent)
+    );
+    // istanbul ignore if: not easily testable
+    if (!result.success) {
+      logger.warn({ error: result.error }, 'Invalid Pipfile.lock');
+      return undefined;
+    }
+    if (result.data._meta?.requires?.python_version) {
+      const pythonVersion = result.data._meta.requires.python_version;
       return `== ${pythonVersion}.*`;
     }
-    if (pipfileLock?._meta?.requires?.python_full_version) {
-      const pythonFullVersion: string =
-        pipfileLock._meta.requires.python_full_version;
+    if (result.data._meta?.requires?.python_full_version) {
+      const pythonFullVersion = result.data._meta.requires.python_full_version;
       return `== ${pythonFullVersion}`;
     }
   } catch (err) {
@@ -56,14 +63,19 @@ function getPipenvConstraint(
     return pipenv;
   }
   try {
-    const pipfileLock = JSON.parse(existingLockFileContent);
-    if (pipfileLock?.default?.pipenv?.version) {
-      const pipenvVersion: string = pipfileLock.default.pipenv.version;
-      return pipenvVersion;
+    const result = PipfileLockSchema.safeParse(
+      JSON.parse(existingLockFileContent)
+    );
+    // istanbul ignore if: not easily testable
+    if (!result.success) {
+      logger.warn({ error: result.error }, 'Invalid Pipfile.lock');
+      return '';
+    }
+    if (result.data.default?.pipenv?.version) {
+      return result.data.default.pipenv.version;
     }
-    if (pipfileLock?.develop?.pipenv?.version) {
-      const pipenvVersion: string = pipfileLock.develop.pipenv.version;
-      return pipenvVersion;
+    if (result.data.develop?.pipenv?.version) {
+      return result.data.develop.pipenv.version;
     }
   } catch (err) {
     // Do nothing
diff --git a/lib/modules/manager/pipenv/schema.ts b/lib/modules/manager/pipenv/schema.ts
new file mode 100644
index 0000000000..a57cac2148
--- /dev/null
+++ b/lib/modules/manager/pipenv/schema.ts
@@ -0,0 +1,24 @@
+import { z } from 'zod';
+
+const PipfileLockEntrySchema = z
+  .record(
+    z.string(),
+    z.object({
+      version: z.string().optional(),
+    })
+  )
+  .optional();
+export const PipfileLockSchema = z.object({
+  _meta: z
+    .object({
+      requires: z
+        .object({
+          python_version: z.string().optional(),
+          python_full_version: z.string().optional(),
+        })
+        .optional(),
+    })
+    .optional(),
+  default: PipfileLockEntrySchema,
+  develop: PipfileLockEntrySchema,
+});
-- 
GitLab