@pnpm/fetching.tarball-fetcher 1102.1.7 → 1102.1.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @pnpm/tarball-fetcher
2
2
 
3
+ ## 1102.1.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Large package downloads and large files inside gzip and bzip2 package archives now use bounded memory during installation. Package manifests and archive metadata larger than 64 MiB are rejected.
8
+
9
+ - Updated dependencies:
10
+ - @pnpm/error@1100.2.2
11
+ - @pnpm/exec.prepare-package@1100.0.42
12
+ - @pnpm/fs.graceful-fs@1100.2.5
13
+ - @pnpm/fs.packlist@1100.0.8
14
+ - @pnpm/network.fetch@1100.1.22
15
+ - @pnpm/store.index@1101.0.1
16
+
3
17
  ## 1102.1.7
4
18
 
5
19
  ### Patch Changes
@@ -1,15 +1,18 @@
1
1
  import path from 'node:path';
2
2
  import gfs from '@pnpm/fs.graceful-fs';
3
3
  import { addFilesFromTarball } from '@pnpm/worker';
4
+ import { MAX_BUFFERED_DOWNLOAD_SIZE } from './remoteTarballFetcher.js';
4
5
  const isAbsolutePath = /^\/|^[A-Z]:/i;
5
6
  export function createLocalTarballFetcher(storeIndex) {
6
7
  const fetch = async (cafs, resolution, opts) => {
7
8
  const tarball = resolvePath(opts.lockfileDir, resolution.tarball.slice(5));
8
- const buffer = gfs.readFileSync(tarball);
9
+ const source = gfs.statSync(tarball).size > MAX_BUFFERED_DOWNLOAD_SIZE
10
+ ? { tarballFile: tarball }
11
+ : { buffer: gfs.readFileSync(tarball) };
9
12
  const result = await addFilesFromTarball({
10
13
  storeDir: cafs.storeDir,
11
14
  storeIndex,
12
- buffer,
15
+ ...source,
13
16
  filesIndexFile: opts.filesIndexFile,
14
17
  integrity: resolution.integrity,
15
18
  readManifest: opts.readManifest,
@@ -3,6 +3,7 @@ import type { FetchOptions, FetchResult } from '@pnpm/fetching.fetcher-base';
3
3
  import type { FetchFromRegistry, GetAuthHeader, RetryTimeoutOptions } from '@pnpm/fetching.types';
4
4
  import type { Cafs } from '@pnpm/store.cafs-types';
5
5
  import type { StoreIndex } from '@pnpm/store.index';
6
+ export declare const MAX_BUFFERED_DOWNLOAD_SIZE: number;
6
7
  export interface HttpResponse {
7
8
  body: string;
8
9
  }
@@ -1,4 +1,6 @@
1
+ import { promises as fs } from 'node:fs';
1
2
  import { isIP } from 'node:net';
3
+ import path from 'node:path';
2
4
  import { requestRetryLogger } from '@pnpm/core-loggers';
3
5
  import { FetchError, FetchTimeoutError, isError, isFetchTimeoutError, redactUrlForDisplay } from '@pnpm/error';
4
6
  import { globalWarn } from '@pnpm/logger';
@@ -7,6 +9,7 @@ import { addFilesFromTarball } from '@pnpm/worker';
7
9
  import * as retry from '@zkochan/retry';
8
10
  import throttle from 'lodash.throttle';
9
11
  import { BadTarballError } from './errorTypes/index.js';
12
+ export const MAX_BUFFERED_DOWNLOAD_SIZE = 64 * 1024 * 1024;
10
13
  const BIG_TARBALL_SIZE = 1024 * 1024 * 5; // 5 MB
11
14
  export function createDownloader(fetchFromRegistry, gotOpts) {
12
15
  const defaultRetryOpts = {
@@ -88,25 +91,35 @@ async function fetchTarball(ctx) {
88
91
  fetchMinSpeedKiBps: ctx.fetchMinSpeedKiBps,
89
92
  onProgress,
90
93
  size,
94
+ storeDir: ctx.opts.cafs.storeDir,
91
95
  url: ctx.url,
92
96
  });
93
97
  }
94
98
  catch (err) {
95
99
  throw wrapFetchError(err, ctx.url, ctx.attempt, ctx.gotOpts.timeout);
96
100
  }
97
- return addFilesFromTarball({
98
- buffer: data,
99
- storeDir: ctx.opts.cafs.storeDir,
100
- storeIndex: ctx.opts.storeIndex,
101
- readManifest: ctx.opts.readManifest,
102
- integrity: ctx.opts.integrity,
103
- filesIndexFile: ctx.opts.filesIndexFile,
104
- pkgId: ctx.opts.pkgId,
105
- url: ctx.url,
106
- pkg: ctx.opts.pkg,
107
- appendManifest: ctx.opts.appendManifest,
108
- ignoreFilePattern: ctx.opts.ignoreFilePattern,
109
- });
101
+ return importDownloadedTarball(ctx, data);
102
+ }
103
+ async function importDownloadedTarball(ctx, data) {
104
+ try {
105
+ return await addFilesFromTarball({
106
+ buffer: data.buffer,
107
+ tarballFile: data.tarballFile,
108
+ storeDir: ctx.opts.cafs.storeDir,
109
+ storeIndex: ctx.opts.storeIndex,
110
+ readManifest: ctx.opts.readManifest,
111
+ integrity: ctx.opts.integrity,
112
+ filesIndexFile: ctx.opts.filesIndexFile,
113
+ pkgId: ctx.opts.pkgId,
114
+ url: ctx.url,
115
+ pkg: ctx.opts.pkg,
116
+ appendManifest: ctx.opts.appendManifest,
117
+ ignoreFilePattern: ctx.opts.ignoreFilePattern,
118
+ });
119
+ }
120
+ finally {
121
+ await data.cleanup?.();
122
+ }
110
123
  }
111
124
  function parseTarballContentLength(res) {
112
125
  const isEncoded = isContentEncoded(res.headers.get('content-encoding'));
@@ -116,54 +129,85 @@ function parseTarballContentLength(res) {
116
129
  }
117
130
  async function readResponseBody(opts) {
118
131
  const startTime = Date.now();
119
- let downloaded = 0;
120
- let data;
121
- if (opts.size !== null) {
122
- data = await readKnownSizeBody(opts.body, opts.size, opts.url, opts.onProgress);
123
- downloaded = opts.size;
132
+ if (opts.size != null && opts.size <= MAX_BUFFERED_DOWNLOAD_SIZE) {
133
+ const buffer = await readKnownSizeBody(opts);
134
+ checkDownloadSpeed(opts.size, startTime, opts.fetchMinSpeedKiBps, opts.url);
135
+ return { buffer };
124
136
  }
125
- else {
126
- const result = await readChunkedBody(opts.body, opts.onProgress);
127
- data = result.data;
128
- downloaded = result.downloaded;
137
+ const state = { chunks: [], downloaded: 0 };
138
+ try {
139
+ for await (const chunk of opts.body) {
140
+ const bytes = chunk;
141
+ state.downloaded += bytes.byteLength;
142
+ checkDownloadedSize(opts, state.downloaded, false);
143
+ await bufferDownloadChunk(state, bytes, opts.storeDir);
144
+ opts.onProgress?.(state.downloaded);
145
+ }
146
+ checkDownloadedSize(opts, state.downloaded, true);
147
+ checkDownloadSpeed(state.downloaded, startTime, opts.fetchMinSpeedKiBps, opts.url);
148
+ await state.file?.close();
149
+ return finishDownloadBuffer(state);
150
+ }
151
+ catch (err) {
152
+ await state.file?.close().catch(() => { });
153
+ if (state.directory)
154
+ await fs.rm(state.directory, { recursive: true, force: true });
155
+ throw err;
129
156
  }
130
- checkDownloadSpeed(downloaded, startTime, opts.fetchMinSpeedKiBps, opts.url);
131
- return data;
132
157
  }
133
- async function readKnownSizeBody(body, size, url, onProgress) {
134
- const data = Buffer.from(new SharedArrayBuffer(size));
158
+ async function readKnownSizeBody(opts) {
159
+ const buffer = Buffer.from(new SharedArrayBuffer(opts.size));
135
160
  let downloaded = 0;
136
- for await (const chunk of body) {
161
+ for await (const chunk of opts.body) {
137
162
  const bytes = chunk;
138
- const nextDownloaded = downloaded + bytes.byteLength;
139
- if (nextDownloaded > size) {
140
- throw new BadTarballError({ expectedSize: size, receivedSize: nextDownloaded, tarballUrl: url });
163
+ checkDownloadedSize(opts, downloaded + bytes.byteLength, false);
164
+ buffer.set(bytes, downloaded);
165
+ downloaded += bytes.byteLength;
166
+ opts.onProgress?.(downloaded);
167
+ }
168
+ checkDownloadedSize(opts, downloaded, true);
169
+ return buffer;
170
+ }
171
+ function checkDownloadedSize(opts, downloaded, finished) {
172
+ if (opts.size == null)
173
+ return;
174
+ if (downloaded > opts.size || (finished && downloaded !== opts.size)) {
175
+ throw new BadTarballError({ expectedSize: opts.size, receivedSize: downloaded, tarballUrl: opts.url });
176
+ }
177
+ }
178
+ async function bufferDownloadChunk(state, bytes, storeDir) {
179
+ if (!state.file && state.downloaded > MAX_BUFFERED_DOWNLOAD_SIZE) {
180
+ await fs.mkdir(storeDir, { recursive: true });
181
+ state.directory = await fs.mkdtemp(path.join(storeDir, 'download-'));
182
+ state.file = await fs.open(path.join(state.directory, 'archive'), 'wx', 0o600);
183
+ for (const buffered of state.chunks) {
184
+ // eslint-disable-next-line no-await-in-loop -- writes preserve tarball byte order
185
+ await state.file.writeFile(buffered);
141
186
  }
142
- data.set(bytes, downloaded);
143
- downloaded = nextDownloaded;
144
- onProgress?.(downloaded);
187
+ state.chunks.length = 0;
188
+ }
189
+ if (state.file) {
190
+ await state.file.writeFile(bytes);
145
191
  }
146
- if (size !== downloaded) {
147
- throw new BadTarballError({ expectedSize: size, receivedSize: downloaded, tarballUrl: url });
192
+ else {
193
+ state.chunks.push(bytes);
148
194
  }
149
- return data;
150
195
  }
151
- async function readChunkedBody(body, onProgress) {
152
- const chunks = [];
153
- let downloaded = 0;
154
- for await (const chunk of body) {
155
- const bytes = chunk;
156
- chunks.push(bytes);
157
- downloaded += bytes.byteLength;
158
- onProgress?.(downloaded);
196
+ function finishDownloadBuffer(state) {
197
+ if (state.directory) {
198
+ const tempDirectory = state.directory;
199
+ return {
200
+ tarballFile: path.join(tempDirectory, 'archive'),
201
+ cleanup: async () => fs.rm(tempDirectory, { recursive: true, force: true }),
202
+ };
159
203
  }
160
- const data = Buffer.from(new SharedArrayBuffer(downloaded));
204
+ const buffer = Buffer.from(new SharedArrayBuffer(state.downloaded));
161
205
  let offset = 0;
162
- for (const chunk of chunks) {
163
- data.set(chunk, offset);
206
+ for (const chunk of state.chunks) {
207
+ buffer.set(chunk, offset);
164
208
  offset += chunk.byteLength;
165
209
  }
166
- return { data, downloaded };
210
+ return { buffer };
167
211
  }
168
212
  function checkDownloadSpeed(downloaded, startTime, fetchMinSpeedKiBps, url) {
169
213
  const elapsedSec = (Date.now() - startTime) / 1000;
@@ -191,6 +235,7 @@ function handleDownloadRetry(opts) {
191
235
  status === 403 ||
192
236
  status === 404 ||
193
237
  opts.error.code === 'ERR_PNPM_PREPARE_PKG_FAILURE' ||
238
+ opts.error.code === 'ERR_PNPM_TARBALL_ENTRY_TOO_LARGE' ||
194
239
  isNonRetryableError(opts.error);
195
240
  if (isFatal) {
196
241
  opts.reject(opts.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/fetching.tarball-fetcher",
3
- "version": "1102.1.7",
3
+ "version": "1102.1.8",
4
4
  "description": "Fetcher for packages hosted as tarballs",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -30,14 +30,14 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@pnpm/core-loggers": "1101.0.2",
33
- "@pnpm/error": "1100.2.1",
34
- "@pnpm/exec.prepare-package": "1100.0.41",
33
+ "@pnpm/error": "1100.2.2",
34
+ "@pnpm/exec.prepare-package": "1100.0.42",
35
35
  "@pnpm/fetching.fetcher-base": "1100.2.12",
36
36
  "@pnpm/fetching.types": "1100.0.3",
37
- "@pnpm/fs.graceful-fs": "1100.2.4",
38
- "@pnpm/fs.packlist": "1100.0.7",
39
- "@pnpm/network.fetch": "1100.1.21",
40
- "@pnpm/store.index": "1101.0.0",
37
+ "@pnpm/fs.graceful-fs": "1100.2.5",
38
+ "@pnpm/fs.packlist": "1100.0.8",
39
+ "@pnpm/network.fetch": "1100.1.22",
40
+ "@pnpm/store.index": "1101.0.1",
41
41
  "@pnpm/types": "1102.1.1",
42
42
  "@zkochan/retry": "^0.2.0",
43
43
  "lodash.throttle": "4.1.1",
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@pnpm/logger": "^1100.0.0",
48
- "@pnpm/worker": "^1100.4.7"
48
+ "@pnpm/worker": "^1100.4.8"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@jest/globals": "30.4.1",
52
- "@pnpm/fetching.tarball-fetcher": "1102.1.7",
52
+ "@pnpm/fetching.tarball-fetcher": "1102.1.8",
53
53
  "@pnpm/logger": "1100.0.0",
54
54
  "@pnpm/store.cafs-types": "1100.1.1",
55
- "@pnpm/store.create-cafs-store": "1100.0.33",
55
+ "@pnpm/store.create-cafs-store": "1100.0.34",
56
56
  "@pnpm/test-fixtures": "1100.0.1",
57
57
  "@pnpm/text.ordinal-comparator": "1100.0.0",
58
58
  "@pnpm/types": "1102.1.1",