@renovatebot/osv-offline 3.0.11 → 3.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import { pipeline } from 'node:stream/promises';
4
4
  import { OsvOfflineDb } from '@renovatebot/osv-offline-db';
5
5
  import path from 'path';
6
6
  import { DateTime } from 'luxon';
7
- import AdmZip from 'adm-zip';
8
7
  import { failure, success } from "./types.js";
8
+ import { extractZip } from "./unzip.js";
9
9
  import debug from 'debug';
10
10
  const logger = debug('osv-offline:download');
11
11
  export async function tryDownloadDb() {
@@ -28,22 +28,34 @@ export async function tryDownloadDb() {
28
28
  return success();
29
29
  }
30
30
  // only download databases if local databases are missing or remote is newer
31
+ // download and extract into a temporary directory first, so a failed attempt leaves the current databases untouched
32
+ const tmpDir = await fs.mkdtemp(path.join(OsvOfflineDb.rootDirectory, '.download-'));
31
33
  try {
32
34
  logger('Downloading databases ...');
33
35
  const databaseUrl = process.env.OSV_OFFLINE_DATABASE_URL ??
34
36
  'https://github.com/renovatebot/osv-offline/releases/latest/download/osv-offline.zip';
35
37
  const stream = got.stream(databaseUrl);
36
- const zipPath = path.join(OsvOfflineDb.rootDirectory, 'osv-offline.zip');
37
- const writeStream = fs.createWriteStream(zipPath);
38
+ const tmpZipPath = path.join(tmpDir, 'osv-offline.zip');
39
+ const writeStream = fs.createWriteStream(tmpZipPath);
38
40
  await pipeline(stream, writeStream);
39
41
  logger('Downloading databases done.');
40
42
  logger('Extracting databases ...');
41
- const zip = new AdmZip(zipPath);
42
- zip.extractAllTo(OsvOfflineDb.rootDirectory);
43
+ const extractDir = path.join(tmpDir, 'extracted');
44
+ await fs.ensureDir(extractDir);
45
+ const files = await extractZip(tmpZipPath, extractDir);
46
+ // renaming replaces existing databases, while readers which already opened them keep reading the old files
47
+ for (const file of files) {
48
+ await fs.rename(path.join(extractDir, file), path.join(OsvOfflineDb.rootDirectory, file));
49
+ }
50
+ // the zip's modification time marks the databases as up-to-date, so it's moved last
51
+ await fs.rename(tmpZipPath, path.join(OsvOfflineDb.rootDirectory, 'osv-offline.zip'));
43
52
  logger('Extracting databases done.');
44
53
  }
45
54
  catch (err) {
46
55
  return failure(err);
47
56
  }
57
+ finally {
58
+ await fs.remove(tmpDir);
59
+ }
48
60
  return success();
49
61
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Extracts the files of a zip archive into `targetDir`, streaming each one to disk and verifying its CRC-32.
3
+ *
4
+ * Only supports files in the root of the archive.
5
+ *
6
+ * @returns the names of the extracted files
7
+ */
8
+ export declare function extractZip(zipPath: string, targetDir: string): Promise<string[]>;
@@ -0,0 +1,63 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Transform } from 'node:stream';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { crc32 } from 'node:zlib';
6
+ import { openPromise } from 'yauzl';
7
+ /**
8
+ * Extracts the files of a zip archive into `targetDir`, streaming each one to disk and verifying its CRC-32.
9
+ *
10
+ * Only supports files in the root of the archive.
11
+ *
12
+ * @returns the names of the extracted files
13
+ */
14
+ export async function extractZip(zipPath, targetDir) {
15
+ const zipFile = await openPromise(zipPath, {
16
+ lazyEntries: true,
17
+ autoClose: false,
18
+ });
19
+ try {
20
+ const files = [];
21
+ for await (const entry of zipFile.eachEntry()) {
22
+ if (entry.fileName.endsWith('/')) {
23
+ // directory entry
24
+ continue;
25
+ }
26
+ if (entry.fileName.includes('/')) {
27
+ throw new Error(`Unsupported zip entry name: ${entry.fileName}`);
28
+ }
29
+ await pipeline(await zipFile.openReadStreamPromise(entry), new Crc32Verifier(entry), createWriteStream(path.join(targetDir, entry.fileName)));
30
+ files.push(entry.fileName);
31
+ }
32
+ return files;
33
+ }
34
+ finally {
35
+ zipFile.close();
36
+ }
37
+ }
38
+ /**
39
+ * Passes the data through, and fails when its CRC-32 differs from the zip entry's.
40
+ *
41
+ * yauzl only verifies the size of an entry.
42
+ *
43
+ * @see https://github.com/thejoshwolfe/yauzl/issues/49
44
+ */
45
+ class Crc32Verifier extends Transform {
46
+ entry;
47
+ crc = 0;
48
+ constructor(entry) {
49
+ super();
50
+ this.entry = entry;
51
+ }
52
+ _transform(chunk, _encoding, callback) {
53
+ this.crc = crc32(chunk, this.crc);
54
+ callback(null, chunk);
55
+ }
56
+ _flush(callback) {
57
+ if (this.crc !== this.entry.crc32) {
58
+ callback(new Error(`CRC-32 mismatch for ${this.entry.fileName}`));
59
+ return;
60
+ }
61
+ callback();
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renovatebot/osv-offline",
3
- "version": "3.0.11",
3
+ "version": "3.0.13",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,19 +12,21 @@
12
12
  "url": "https://github.com/renovatebot/osv-offline.git"
13
13
  },
14
14
  "dependencies": {
15
- "@renovatebot/osv-offline-db": "3.0.11",
16
- "adm-zip": "~0.6.1",
15
+ "@renovatebot/osv-offline-db": "3.0.13",
17
16
  "debug": "^4.4.3",
18
- "fs-extra": "^11.4.0",
17
+ "fs-extra": "^11.4.1",
19
18
  "got": "^15.1.0",
20
- "luxon": "^3.7.2"
19
+ "luxon": "^3.7.2",
20
+ "yauzl": "^3.4.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/adm-zip": "0.5.8",
23
+ "@types/adm-zip": "0.6.0",
24
24
  "@types/debug": "4.1.13",
25
25
  "@types/fs-extra": "11.0.4",
26
- "@types/luxon": "3.7.5",
27
- "@types/node": "22.20.2"
26
+ "@types/luxon": "3.7.6",
27
+ "@types/node": "22.20.5",
28
+ "@types/yauzl": "3.4.0",
29
+ "adm-zip": "0.6.1"
28
30
  },
29
31
  "engines": {
30
32
  "node": ">=22.12.0",