From 991f69c36b2c85432debbe790cfef74ba174a312 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sun, 12 May 2019 15:50:29 +0200
Subject: [PATCH] fix(bundler): refactor rubygems.org fetching to use CDN
 (#3669)

Refactors fetching of dependencies from Rubygems.org to use the /versions file instead of the official API. For now this means no metadata from Rubygems, so it will be added in a future PR.

Closes #3373
---
 lib/datasource/rubygems/get-rubygems-org.js   | 96 +++++++++++++++++++
 lib/datasource/rubygems/releases.js           |  8 +-
 .../rubygems/__snapshots__/index.spec.js.snap | 14 +++
 test/datasource/rubygems/index.spec.js        | 43 +++++++++
 4 files changed, 160 insertions(+), 1 deletion(-)
 create mode 100644 lib/datasource/rubygems/get-rubygems-org.js

diff --git a/lib/datasource/rubygems/get-rubygems-org.js b/lib/datasource/rubygems/get-rubygems-org.js
new file mode 100644
index 0000000000..55e1409766
--- /dev/null
+++ b/lib/datasource/rubygems/get-rubygems-org.js
@@ -0,0 +1,96 @@
+const got = require('../../util/got');
+
+module.exports = {
+  getRubygemsOrgDependency,
+};
+
+let lastSync = new Date('2000-01-01');
+let packageReleases = Object.create(null); // Because we might need a "constructor" key
+let contentLength = 0;
+
+async function updateRubyGemsVersions() {
+  const url = 'https://rubygems.org/versions';
+  const options = {
+    headers: {
+      'accept-encoding': 'identity',
+      range: `bytes=${contentLength}-`,
+    },
+  };
+  let newLines;
+  try {
+    logger.debug('Rubygems: Fetching rubygems.org versions');
+    newLines = (await got(url, options)).body;
+  } catch (err) /* istanbul ignore next */ {
+    if (err.statusCode === 416) {
+      logger.debug('Rubygems: No update');
+    } else {
+      logger.warn({ err }, 'Rubygems error - resetting cache');
+      contentLength = 0;
+      packageReleases = Object.create(null); // Because we might need a "constructor" key
+    }
+    lastSync = new Date();
+    return;
+  }
+
+  function processLine(line) {
+    let split;
+    let pkg;
+    let versions;
+    try {
+      const l = line.trim();
+      if (!l.length || l.startsWith('created_at:') || l === '---') {
+        return;
+      }
+      split = l.split(' ');
+      [pkg, versions] = split;
+      packageReleases[pkg] = packageReleases[pkg] || [];
+      const lineVersions = versions.split(',').map(version => version.trim());
+      for (const lineVersion of lineVersions) {
+        if (lineVersion.startsWith('-')) {
+          const deletedVersion = lineVersion.slice(1);
+          logger.trace({ pkg, deletedVersion }, 'Rubygems: Deleting version');
+          packageReleases[pkg] = packageReleases[pkg].filter(
+            version => version !== deletedVersion
+          );
+        } else {
+          packageReleases[pkg].push(lineVersion);
+        }
+      }
+    } catch (err) /* istanbul ignore next */ {
+      logger.warn(
+        { err, line, split, pkg, versions },
+        'Rubygems line parsing error'
+      );
+    }
+  }
+
+  for (const line of newLines.split('\n')) {
+    processLine(line);
+  }
+  lastSync = new Date();
+}
+
+function isDataStale() {
+  const minutesElapsed = Math.floor((new Date() - lastSync) / (60 * 1000));
+  return minutesElapsed >= 5;
+}
+
+async function syncVersions() {
+  if (isDataStale()) {
+    global.updateRubyGemsVersions =
+      global.updateRubyGemsVersions || updateRubyGemsVersions();
+    await global.updateRubyGemsVersions;
+  }
+}
+
+async function getRubygemsOrgDependency(lookupName) {
+  await syncVersions();
+  if (!packageReleases[lookupName]) {
+    return null;
+  }
+  const dep = {
+    name: lookupName,
+    releases: packageReleases[lookupName].map(version => ({ version })),
+  };
+  return dep;
+}
diff --git a/lib/datasource/rubygems/releases.js b/lib/datasource/rubygems/releases.js
index cb91619be3..3e1af26328 100644
--- a/lib/datasource/rubygems/releases.js
+++ b/lib/datasource/rubygems/releases.js
@@ -1,11 +1,17 @@
 const { nonEmptyArray } = require('@sindresorhus/is');
 const { getDependency } = require('./get');
+const { getRubygemsOrgDependency } = require('./get-rubygems-org');
 
 async function getPkgReleases({ lookupName, registryUrls }) {
   const registries = nonEmptyArray(registryUrls) ? registryUrls : [];
 
   for (const registry of registries) {
-    const pkg = await getDependency({ dependency: lookupName, registry });
+    let pkg;
+    if (registry.endsWith('rubygems.org')) {
+      pkg = await getRubygemsOrgDependency(lookupName);
+    } else {
+      pkg = await getDependency({ dependency: lookupName, registry });
+    }
     if (pkg) {
       return pkg;
     }
diff --git a/test/datasource/rubygems/__snapshots__/index.spec.js.snap b/test/datasource/rubygems/__snapshots__/index.spec.js.snap
index 8e5bc155be..1c1ef2c862 100644
--- a/test/datasource/rubygems/__snapshots__/index.spec.js.snap
+++ b/test/datasource/rubygems/__snapshots__/index.spec.js.snap
@@ -1,5 +1,19 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`datasource/rubygems getPkgReleases returns a dep for rubygems.org package hit 1`] = `
+Object {
+  "name": "1pass",
+  "releases": Array [
+    Object {
+      "version": "0.1.0",
+    },
+    Object {
+      "version": "0.1.1",
+    },
+  ],
+}
+`;
+
 exports[`datasource/rubygems getPkgReleases uses multiple source urls 1`] = `
 Object {
   "changelogUrl": null,
diff --git a/test/datasource/rubygems/index.spec.js b/test/datasource/rubygems/index.spec.js
index db8a65e141..cbe9db9f86 100644
--- a/test/datasource/rubygems/index.spec.js
+++ b/test/datasource/rubygems/index.spec.js
@@ -3,6 +3,25 @@ const railsInfo = require('./_fixtures/rails/info.json');
 const railsVersions = require('./_fixtures/rails/versions.json');
 const rubygems = require('../../../lib/datasource/rubygems/index.js');
 
+const rubygemsOrgVersions = `created_at: 2017-03-27T04:38:13+00:00
+---
+- 1 05d0116933ba44b0b5d0ee19bfd35ccc
+.cat 0.0.1 631fd60a806eaf5026c86fff3155c289
+0mq 0.1.0,0.1.1,0.1.2,0.2.0,0.2.1,0.3.0,0.4.0,0.4.1,0.5.0,0.5.1,0.5.2,0.5.3 6146193f8f7e944156b0b42ec37bad3e
+0xffffff 0.0.1,0.1.0 0a4a9aeae24152cdb467be02f40482f9
+10to1-crack 0.1.1,0.1.2,0.1.3 e7218e76477e2137355d2e7ded094925
+1234567890_ 1.0,1.1 233e818c2db65d2dad9f9ea9a27b1a30
+12_hour_time 0.0.2,0.0.3,0.0.4 4e58bc03e301f704950410b713c20b69
+16watts-fluently 0.3.0,0.3.1 555088e2b18e97e0293cab1d90dbb0d2
+189seg 0.0.1 c4d329f7d3eb88b6e602358968be0242
+196demo 0.0.0 e00c558565f7b03a438fbd93d854b7de
+1_as_identity_function 1.0.0,1.0.1 bee2f0fbbc3c5c83008c0b8fc64cb168
+1and1 1.1 1853e4495b036ddc5da2035523d48f0d
+1hdoc 0.1.3,0.2.0,0.2.2,0.2.3,0.2.4 7076f29c196df12047a3700c4d6e5915
+1pass 0.1.0,0.1.1,0.1.2 d209547aae4b8f3d67123f18f738ac99
+1pass -0.1.2 abcdef
+21-day-challenge-countdown 0.1.0,0.1.1,0.1.2 57e8873fe713063f4e54e85bbbd709bb`;
+
 jest.mock('../../../lib/util/got');
 
 describe('datasource/rubygems', () => {
@@ -24,6 +43,30 @@ describe('datasource/rubygems', () => {
       expect(await rubygems.getPkgReleases(params)).toBeNull();
     });
 
+    it('returns null for rubygems.org package miss', async () => {
+      const newparams = { ...params };
+      newparams.registryUrls = ['https://rubygems.org'];
+      got.mockReturnValueOnce({ body: rubygemsOrgVersions });
+      expect(await rubygems.getPkgReleases(newparams)).toBeNull();
+    });
+
+    it('returns a dep for rubygems.org package hit', async () => {
+      const newparams = {
+        lookupName: '1pass',
+        registryUrls: ['https://rubygems.org'],
+      };
+      got.mockReturnValueOnce({ body: rubygemsOrgVersions });
+      const res = await rubygems.getPkgReleases(newparams);
+      expect(res).not.toBeNull();
+      expect(res).toMatchSnapshot();
+      expect(
+        res.releases.find(release => release.version === '0.1.1')
+      ).toBeDefined();
+      expect(
+        res.releases.find(release => release.version === '0.1.2')
+      ).toBeUndefined();
+    });
+
     it('works with real data', async () => {
       got
         .mockReturnValueOnce({ body: railsInfo })
-- 
GitLab