From 2c75fac1a5bc92977dd22423e5f30d9eb611cb01 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Sat, 29 Apr 2023 19:19:35 +0300
Subject: [PATCH] fix(rubygems): Copy strings via buffers instead of slice hack
 (#21876)

---
 .../rubygems/versions-datasource.ts           | 35 +++++++++++++++----
 1 file changed, 29 insertions(+), 6 deletions(-)

diff --git a/lib/modules/datasource/rubygems/versions-datasource.ts b/lib/modules/datasource/rubygems/versions-datasource.ts
index e398bd0a5c..5122bd759a 100644
--- a/lib/modules/datasource/rubygems/versions-datasource.ts
+++ b/lib/modules/datasource/rubygems/versions-datasource.ts
@@ -45,12 +45,22 @@ const Lines = z
             }
           }
           return { packageName, deletedVersions, addedVersions };
-        })
+        }),
+      {
+        onError: ({ error: err, input }) => {
+          logger.debug(
+            { err, input },
+            'Rubygems: failed to parse some version lines'
+          );
+        },
+      }
     )
   );
 type Lines = z.infer<typeof Lines>;
 
 export class VersionsDatasource extends Datasource {
+  private isInitialFetch = true;
+
   constructor(override readonly id: string) {
     super(id);
   }
@@ -96,10 +106,22 @@ export class VersionsDatasource extends Datasource {
   }
 
   /**
-   * https://bugs.chromium.org/p/v8/issues/detail?id=2869
+   * Since each `/versions` reponse exceed 10MB,
+   * there is potential for a memory leak if we construct slices
+   * of the response body and cache them long-term:
+   *
+   *   https://bugs.chromium.org/p/v8/issues/detail?id=2869
+   *
+   * This method meant to be called for `version` and `packageName`
+   * before storing them in the cache.
    */
-  private static copystr(x: string): string {
-    return (' ' + x).slice(1);
+  private copystr(x: string): string {
+    const len = Buffer.byteLength(x, 'utf8');
+    const buf = this.isInitialFetch
+      ? Buffer.allocUnsafe(len) // allocate from pre-allocated buffer
+      : Buffer.allocUnsafeSlow(len); // allocate standalone buffer
+    buf.write(x, 'utf8');
+    return buf.toString('utf8');
   }
 
   private updatePackageReleases(
@@ -107,7 +129,7 @@ export class VersionsDatasource extends Datasource {
     lines: Lines
   ): void {
     for (const line of lines) {
-      const packageName = VersionsDatasource.copystr(line.packageName);
+      const packageName = this.copystr(line.packageName);
       let versions = packageReleases.get(packageName) ?? [];
 
       const { deletedVersions, addedVersions } = line;
@@ -120,7 +142,7 @@ export class VersionsDatasource extends Datasource {
         const existingVersions = new Set(versions);
         for (const addedVersion of addedVersions) {
           if (!existingVersions.has(addedVersion)) {
-            const version = VersionsDatasource.copystr(addedVersion);
+            const version = this.copystr(addedVersion);
             versions.push(version);
           }
         }
@@ -169,6 +191,7 @@ export class VersionsDatasource extends Datasource {
 
     const lines = Lines.parse(newLines);
     this.updatePackageReleases(regCache.packageReleases, lines);
+    this.isInitialFetch = false;
   }
 
   private updateRubyGemsVersionsPromise: Promise<void> | null = null;
-- 
GitLab