@renovatebot/osv-offline-db 2.1.1 → 2.2.0

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.
package/dist/lib/db.d.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import { Ecosystem } from './ecosystem';
2
- import type { Osv } from '..';
2
+ import type { Vulnerability } from './osv';
3
3
  export declare class OsvOfflineDb {
4
+ private disposed;
4
5
  static readonly rootDirectory: string;
5
- private db;
6
+ private _data;
7
+ private _db;
8
+ private _pendingAcs;
9
+ private readonly _exitHandler;
6
10
  private constructor();
7
11
  private _load;
8
12
  private _init;
13
+ private _buildIndex;
9
14
  static create(): OsvOfflineDb;
10
- query(ecosystem: Ecosystem, packageName: string): Promise<Osv.Vulnerability[]>;
15
+ query(ecosystem: Ecosystem, packageName: string): Promise<Vulnerability[]>;
16
+ [Symbol.dispose](): void;
17
+ private _unload;
11
18
  }
package/dist/lib/db.js CHANGED
@@ -4,48 +4,176 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.OsvOfflineDb = void 0;
7
- const os_1 = require("os");
8
- const path_1 = __importDefault(require("path"));
9
- const nedb_1 = __importDefault(require("@seald-io/nedb"));
10
- const ecosystem_1 = require("./ecosystem");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_fs_1 = require("node:fs");
9
+ const node_util_1 = require("node:util");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_os_1 = require("node:os");
12
+ const node_readline_1 = __importDefault(require("node:readline"));
11
13
  const purl_helper_1 = require("./purl-helper");
12
14
  const debug_1 = __importDefault(require("debug"));
13
15
  const logger = (0, debug_1.default)('osv-offline:db');
16
+ const readAsync = (0, node_util_1.promisify)(node_fs_1.read);
14
17
  class OsvOfflineDb {
15
- static rootDirectory = process.env.OSV_OFFLINE_ROOT_DIR ?? path_1.default.join((0, os_1.tmpdir)(), 'osv-offline');
16
- db = {};
17
- // eslint-disable-next-line @typescript-eslint/no-empty-function
18
- constructor() { }
19
- _load(ecosystem) {
20
- return (this.db[ecosystem] ??= this._init(ecosystem));
18
+ disposed = false;
19
+ static rootDirectory = process.env.OSV_OFFLINE_ROOT_DIR ?? node_path_1.default.join((0, node_os_1.tmpdir)(), 'osv-offline');
20
+ _data = {};
21
+ _db = {};
22
+ _pendingAcs = new Set();
23
+ _exitHandler = () => {
24
+ if (this.disposed) {
25
+ return;
26
+ }
27
+ logger('Databases are not disposed! Please explicitly dispose.');
28
+ this[Symbol.dispose]();
29
+ };
30
+ constructor() {
31
+ process.on('exit', this._exitHandler);
32
+ }
33
+ async _load(ecosystem) {
34
+ const cached = this._data[ecosystem];
35
+ if (cached) {
36
+ const fileStat = await (0, promises_1.stat)(cached.filePath).catch(() => null);
37
+ // Check if the data currently in memory is still the same object we checked against
38
+ // If not, another request has already handled the reload/unload
39
+ if (this._data[ecosystem] !== cached) {
40
+ return (this._db[ecosystem] ??= this._init(ecosystem));
41
+ }
42
+ if (fileStat?.mtimeMs !== cached.mtime) {
43
+ logger(fileStat
44
+ ? `File changed for ecosystem '${ecosystem}', reloading ...`
45
+ : `File removed for ecosystem '${ecosystem}', unloading ...`);
46
+ // Delete references before unloading to prevent concurrent callers from picking up stale data
47
+ delete this._data[ecosystem];
48
+ delete this._db[ecosystem];
49
+ this._unload(cached);
50
+ }
51
+ }
52
+ return (this._db[ecosystem] ??= this._init(ecosystem));
21
53
  }
22
54
  async _init(ecosystem) {
23
- if (!ecosystem_1.ecosystems.includes(ecosystem)) {
24
- throw new Error(`Ecosystem not supported`);
55
+ logger(`Initializing ecosystem '${ecosystem}' ...`);
56
+ const ac = new AbortController();
57
+ this._pendingAcs.add(ac);
58
+ try {
59
+ const filePath = node_path_1.default.join(OsvOfflineDb.rootDirectory, `${ecosystem.toLowerCase()}.nedb`);
60
+ const fileStat = await (0, promises_1.stat)(filePath).catch(() => null);
61
+ if (!fileStat) {
62
+ logger(`Missing data for ecosystem '${ecosystem}'`);
63
+ delete this._db[ecosystem];
64
+ return null;
65
+ }
66
+ const data = {
67
+ filePath,
68
+ fd: (0, node_fs_1.openSync)(filePath, 'r'),
69
+ index: new Map(),
70
+ mtime: fileStat.mtimeMs,
71
+ };
72
+ await this._buildIndex(ac, data);
73
+ if (ac.signal.aborted) {
74
+ logger(`Initializing ecosystem '${ecosystem}' aborted.`);
75
+ delete this._db[ecosystem];
76
+ this._unload(data);
77
+ return null;
78
+ }
79
+ this._data[ecosystem] = data;
80
+ logger(`Initializing ecosystem '${ecosystem}' done.`);
81
+ return data;
25
82
  }
26
- logger(`Initializing database for ecosystem: ${ecosystem}`);
27
- const db = new nedb_1.default({
28
- filename: path_1.default.join(OsvOfflineDb.rootDirectory, `${ecosystem.toLowerCase()}.nedb`),
83
+ finally {
84
+ this._pendingAcs.delete(ac);
85
+ }
86
+ }
87
+ async _buildIndex(ac, data) {
88
+ const fileStream = (0, node_fs_1.createReadStream)(data.filePath);
89
+ const rl = node_readline_1.default.createInterface({
90
+ input: fileStream,
91
+ crlfDelay: Infinity,
29
92
  });
30
- await db.loadDatabaseAsync();
31
- return db;
93
+ let currentOffset = 0;
94
+ const affectedPackageNames = new Set();
95
+ for await (const line of rl) {
96
+ if (ac.signal.aborted) {
97
+ fileStream.destroy();
98
+ return;
99
+ }
100
+ const lineByteLength = Buffer.byteLength(line, 'utf8');
101
+ try {
102
+ const record = JSON.parse(line);
103
+ if (record.affected) {
104
+ for (const affected of record.affected) {
105
+ const packageName = affected.package?.name;
106
+ if (packageName && !affectedPackageNames.has(packageName)) {
107
+ affectedPackageNames.add(packageName);
108
+ let pointers = data.index.get(packageName);
109
+ if (!pointers) {
110
+ pointers = [];
111
+ data.index.set(packageName, pointers);
112
+ }
113
+ pointers.push({ offset: currentOffset, length: lineByteLength });
114
+ }
115
+ }
116
+ affectedPackageNames.clear();
117
+ }
118
+ }
119
+ catch {
120
+ // Skip malformed lines
121
+ }
122
+ currentOffset += lineByteLength + 1;
123
+ }
32
124
  }
33
125
  static create() {
34
126
  const osvOfflineDb = new OsvOfflineDb();
35
127
  return osvOfflineDb;
36
128
  }
37
129
  async query(ecosystem, packageName) {
38
- return await (await this._load(ecosystem)).findAsync({
39
- affected: {
40
- $elemMatch: {
41
- package: {
42
- name: packageName,
43
- ecosystem,
44
- purl: (0, purl_helper_1.packageToPurl)(ecosystem, packageName),
45
- },
46
- },
47
- },
48
- });
130
+ if (this.disposed) {
131
+ throw new Error('Database disposed');
132
+ }
133
+ const data = await this._load(ecosystem);
134
+ if (!data) {
135
+ return [];
136
+ }
137
+ const pointers = data.index.get(packageName);
138
+ if (!pointers || pointers.length === 0) {
139
+ return [];
140
+ }
141
+ const advisories = await Promise.all(pointers.map(async ({ offset, length }) => {
142
+ const buffer = Buffer.allocUnsafe(length);
143
+ const { bytesRead } = await readAsync(data.fd, buffer, 0, length, offset);
144
+ return JSON.parse(buffer.toString('utf8', 0, bytesRead));
145
+ }));
146
+ if (this.disposed)
147
+ return [];
148
+ const targetPurl = (0, purl_helper_1.packageToPurl)(ecosystem, packageName);
149
+ return advisories.filter((vuln) => vuln.affected?.some((a) => a.package?.name === packageName &&
150
+ a.package.ecosystem === ecosystem &&
151
+ a.package.purl === targetPurl));
152
+ }
153
+ [Symbol.dispose]() {
154
+ if (this.disposed) {
155
+ return;
156
+ }
157
+ logger(`Disposing databases ...`);
158
+ this.disposed = true;
159
+ process.off('exit', this._exitHandler);
160
+ for (const ac of this._pendingAcs) {
161
+ ac.abort();
162
+ }
163
+ this._pendingAcs.clear();
164
+ for (const d of Object.values(this._data)) {
165
+ this._unload(d);
166
+ }
167
+ this._db = {};
168
+ this._data = {};
169
+ }
170
+ _unload(d) {
171
+ try {
172
+ (0, node_fs_1.closeSync)(d.fd);
173
+ }
174
+ catch {
175
+ // ignore
176
+ }
49
177
  }
50
178
  }
51
179
  exports.OsvOfflineDb = OsvOfflineDb;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renovatebot/osv-offline-db",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "MIT",
@@ -13,7 +13,6 @@
13
13
  "url": "https://github.com/renovatebot/osv-offline.git"
14
14
  },
15
15
  "dependencies": {
16
- "@seald-io/nedb": "^4.1.2",
17
16
  "debug": "^4.4.3"
18
17
  },
19
18
  "devDependencies": {
@@ -21,7 +20,7 @@
21
20
  "@tsconfig/strictest": "2.0.8",
22
21
  "@types/debug": "4.1.12",
23
22
  "@types/fs-extra": "11.0.4",
24
- "@types/node": "22.19.11",
23
+ "@types/node": "22.19.13",
25
24
  "fs-extra": "11.3.3"
26
25
  },
27
26
  "engines": {