zip-lib 1.2.2 → 1.3.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/lib/unzip.js CHANGED
@@ -4,6 +4,7 @@ exports.Unzip = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const fs = require("node:fs/promises");
6
6
  const path = require("node:path");
7
+ const promises_1 = require("node:stream/promises");
7
8
  const yauzl = require("yauzl");
8
9
  const cancelable_1 = require("./cancelable");
9
10
  const exfs = require("./fs");
@@ -41,6 +42,7 @@ class EntryContext {
41
42
  this.symlinkAsFileOnWindows = symlinkAsFileOnWindows;
42
43
  this._symlinkFileNames = [];
43
44
  this._symlinkFolders = [];
45
+ this._inspectedFolders = [];
44
46
  this._ensuredFolders = [];
45
47
  }
46
48
  get decodeEntryFileName() {
@@ -64,25 +66,38 @@ class EntryContext {
64
66
  this._symlinkFolders.push({ folder, realpath });
65
67
  }
66
68
  }
67
- isOutside(baseDir, targetPath) {
68
- const absoluteBase = path.resolve(baseDir);
69
- const absoluteTarget = path.resolve(targetPath);
70
- const relative = path.relative(absoluteBase, absoluteTarget);
71
- return relative.startsWith("..") || path.isAbsolute(relative);
72
- }
73
69
  getFilePath() {
74
70
  return path.resolve(path.join(this.targetFolder, this.decodeEntryFileName));
75
71
  }
72
+ async inspectFolder(folder) {
73
+ if (this._inspectedFolders.includes(folder) || folder === path.dirname(folder)) {
74
+ return;
75
+ }
76
+ await this.inspectFolder(path.dirname(folder));
77
+ const folderStat = await exfs.statFolder(folder);
78
+ if (!folderStat) {
79
+ return;
80
+ }
81
+ if (folderStat.isSymbolicLink) {
82
+ this.addSymlinkFolder(folder, folderStat.realpath);
83
+ }
84
+ this._inspectedFolders.push(folder);
85
+ }
76
86
  async ensureFolder(folder) {
77
87
  if (this._ensuredFolders.includes(folder)) {
78
88
  return;
79
89
  }
90
+ await this.inspectFolder(path.dirname(folder));
80
91
  const folderStat = await exfs.ensureFolder(folder);
81
92
  if (folderStat.isSymbolicLink) {
82
93
  this.addSymlinkFolder(folder, folderStat.realpath);
83
94
  }
84
95
  this._ensuredFolders.push(folder);
85
96
  }
97
+ isSymlinkTargetOutsideTargetFolder(linkTarget, linkFilePath) {
98
+ const fileDir = path.dirname(linkFilePath);
99
+ return exfs.isOutside(this.realTargetFolder, path.resolve(fileDir, linkTarget));
100
+ }
86
101
  async isOutsideTargetFolder(tpath) {
87
102
  if (this._symlinkFileNames.length === 0 && this._symlinkFolders.length === 0) {
88
103
  return false;
@@ -92,7 +107,7 @@ class EntryContext {
92
107
  }
93
108
  for (const { folder, realpath } of this._symlinkFolders) {
94
109
  if (tpath.includes(folder)) {
95
- if (this.isOutside(this.realTargetFolder, realpath)) {
110
+ if (exfs.isOutside(this.realTargetFolder, realpath)) {
96
111
  return true;
97
112
  }
98
113
  }
@@ -100,7 +115,7 @@ class EntryContext {
100
115
  for (const fileName of this._symlinkFileNames) {
101
116
  if (tpath.includes(fileName)) {
102
117
  const realFilePath = await exfs.realpath(tpath);
103
- if (this.isOutside(this.realTargetFolder, realFilePath)) {
118
+ if (exfs.isOutside(this.realTargetFolder, realFilePath)) {
104
119
  return true;
105
120
  }
106
121
  }
@@ -119,98 +134,124 @@ class Unzip extends cancelable_1.Cancelable {
119
134
  super();
120
135
  this.options = options;
121
136
  }
122
- /**
123
- * Extract the zip file to the specified location.
124
- * @param zipFile
125
- * @param targetFolder
126
- * @param options
127
- */
128
- async extract(zipFile, targetFolder) {
129
- let extractedEntriesCount = 0;
137
+ async extract(zipFileOrBuffer, targetFolder) {
130
138
  const token = new cancelable_1.CancellationToken();
131
139
  this.token = token;
140
+ const { zfile, realTargetFolder } = await this.prepareExtraction(zipFileOrBuffer, targetFolder, token);
141
+ this.zipFile = zfile;
142
+ await this.processEntries(zfile, targetFolder, realTargetFolder, token);
143
+ }
144
+ async prepareExtraction(zipFileOrBuffer, targetFolder, token) {
132
145
  if (this.isOverwrite()) {
133
146
  await exfs.rimraf(targetFolder);
134
147
  }
135
148
  if (token.isCancelled) {
136
- return Promise.reject(this.canceledError());
149
+ throw this.canceledError();
137
150
  }
138
151
  await exfs.ensureFolder(targetFolder);
139
152
  const realTargetFolder = await exfs.realpath(targetFolder);
140
- const zfile = await this.openZip(zipFile, token);
141
- this.zipFile = zfile;
142
- zfile.readEntry();
143
- return new Promise((c, e) => {
153
+ const zfile = await this.openZip(zipFileOrBuffer, token);
154
+ return { zfile, realTargetFolder };
155
+ }
156
+ async processEntries(zfile, targetFolder, realTargetFolder, token) {
157
+ return await new Promise((resolve, reject) => {
158
+ let extractedEntriesCount = 0;
144
159
  let anyError = null;
145
160
  const total = zfile.entryCount;
161
+ const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile());
162
+ const entryEvent = new EntryEvent(total);
163
+ const settle = this.createPromiseSettler(resolve, reject);
164
+ const disposeCancel = token.onCancelled(() => {
165
+ this.closeZip();
166
+ settle.reject(this.canceledError());
167
+ });
146
168
  zfile.once("error", (err) => {
169
+ disposeCancel();
170
+ anyError = this.wrapError(err, token.isCancelled);
147
171
  this.closeZip();
148
- e(this.wrapError(err, token.isCancelled));
172
+ settle.reject(anyError);
149
173
  });
150
174
  zfile.once("close", () => {
175
+ disposeCancel();
151
176
  this.zipFile = null;
152
177
  if (anyError) {
153
- e(this.wrapError(anyError, token.isCancelled));
178
+ settle.reject(this.wrapError(anyError, token.isCancelled));
154
179
  }
155
- else {
156
- if (token.isCancelled) {
157
- e(this.canceledError());
158
- }
159
- // If the zip content is empty, it will not receive the `zfile.on("entry")` event.
160
- else if (total === 0) {
161
- c(void 0);
162
- }
180
+ else if (token.isCancelled) {
181
+ settle.reject(this.canceledError());
182
+ }
183
+ else if (extractedEntriesCount >= total) {
184
+ // If the zip content is empty, it will not receive the zfile.on("entry") event.
185
+ settle.resolve();
163
186
  }
164
187
  });
165
- // Because openZip is an asynchronous method, openZip may not be completed when calling cancel,
166
- // so we need to check if it has been canceled after the openZip method returns.
167
- if (token.isCancelled) {
168
- this.closeZip();
169
- return;
170
- }
171
- const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile());
172
- const entryEvent = new EntryEvent(total);
173
188
  zfile.on("entry", async (entry) => {
174
- // use UTF-8 in all situations
175
- // see https://github.com/thejoshwolfe/yauzl/issues/84
176
- const rawName = entry.fileName.toString("utf8");
177
- // allow backslash
178
- const fileName = rawName.replace(/\\/g, "/");
179
- // Because `decodeStrings` is `false`, we need to manually verify the entryname
180
- // see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename
181
- const errorMessage = yauzl.validateFileName(fileName);
182
- if (errorMessage != null) {
183
- anyError = new Error(errorMessage);
184
- this.closeZip();
185
- e(anyError);
186
- return;
187
- }
188
- entryEvent.entryName = fileName;
189
- this.onEntryCallback(entryEvent);
190
- entryContext.decodeEntryFileName = fileName;
191
189
  try {
192
- if (entryEvent.isPrevented) {
193
- entryEvent.reset();
194
- zfile.readEntry();
195
- }
196
- else {
197
- await this.handleEntry(zfile, entry, entryContext, token);
198
- }
190
+ await this.handleZipEntry(zfile, entry, entryContext, entryEvent, token);
199
191
  extractedEntriesCount++;
200
- if (extractedEntriesCount === total) {
201
- c();
192
+ if (extractedEntriesCount >= total) {
193
+ settle.resolve();
202
194
  }
203
195
  }
204
196
  catch (error) {
205
197
  anyError = this.wrapError(error, token.isCancelled);
206
198
  this.closeZip();
207
- e(anyError);
199
+ settle.reject(anyError);
208
200
  }
209
201
  });
202
+ this.readNextEntry(zfile, token);
210
203
  });
211
204
  }
205
+ readNextEntry(zfile, token) {
206
+ if (token.isCancelled) {
207
+ this.closeZip();
208
+ return;
209
+ }
210
+ zfile.readEntry();
211
+ }
212
+ createPromiseSettler(resolve, reject) {
213
+ let settled = false;
214
+ return {
215
+ resolve: () => {
216
+ if (settled) {
217
+ return;
218
+ }
219
+ settled = true;
220
+ resolve();
221
+ },
222
+ reject: (error) => {
223
+ if (settled) {
224
+ return;
225
+ }
226
+ settled = true;
227
+ reject(error);
228
+ },
229
+ };
230
+ }
231
+ async handleZipEntry(zfile, entry, entryContext, entryEvent, token) {
232
+ // use UTF-8 in all situations
233
+ // see https://github.com/thejoshwolfe/yauzl/issues/84
234
+ const rawName = entry.fileName.toString("utf8");
235
+ // allow backslash
236
+ const fileName = rawName.replace(/\\/g, "/");
237
+ // Because decodeStrings is false, we need to manually verify the entry name
238
+ // see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename
239
+ const errorMessage = yauzl.validateFileName(fileName);
240
+ if (errorMessage != null) {
241
+ throw new Error(errorMessage);
242
+ }
243
+ entryEvent.entryName = fileName;
244
+ this.onEntryCallback(entryEvent);
245
+ entryContext.decodeEntryFileName = fileName;
246
+ if (entryEvent.isPrevented) {
247
+ entryEvent.reset();
248
+ this.readNextEntry(zfile, token);
249
+ return;
250
+ }
251
+ await this.handleEntry(zfile, entry, entryContext, token);
252
+ }
212
253
  /**
213
- * Cancel decompression.
254
+ * Cancel extraction.
214
255
  * If the cancel method is called after the extract is complete, nothing will happen.
215
256
  */
216
257
  cancel() {
@@ -226,29 +267,41 @@ class Unzip extends cancelable_1.Cancelable {
226
267
  this.zipFile = null;
227
268
  }
228
269
  }
229
- openZip(zipFile, token) {
230
- return new Promise((c, e) => {
231
- yauzl.open(zipFile, {
232
- lazyEntries: true,
233
- // see https://github.com/thejoshwolfe/yauzl/issues/84
234
- decodeStrings: false,
235
- }, (err, zfile) => {
236
- if (err) {
237
- e(this.wrapError(err, token.isCancelled));
270
+ async openZip(zipFileOrBuffer, token) {
271
+ const options = {
272
+ lazyEntries: true,
273
+ // see https://github.com/thejoshwolfe/yauzl/issues/84
274
+ decodeStrings: false,
275
+ };
276
+ try {
277
+ return await new Promise((resolve, reject) => {
278
+ const callback = (err, zfile) => {
279
+ if (err) {
280
+ reject(this.wrapError(err, token.isCancelled));
281
+ }
282
+ else {
283
+ resolve(zfile);
284
+ }
285
+ };
286
+ if (typeof zipFileOrBuffer === "string") {
287
+ yauzl.open(zipFileOrBuffer, options, callback);
238
288
  }
239
289
  else {
240
- c(zfile);
290
+ yauzl.fromBuffer(zipFileOrBuffer, options, callback);
241
291
  }
242
292
  });
243
- });
293
+ }
294
+ catch (error) {
295
+ throw this.wrapError(error, token.isCancelled);
296
+ }
244
297
  }
245
298
  async handleEntry(zfile, entry, entryContext, token) {
246
299
  if (/\/$/.test(entryContext.decodeEntryFileName)) {
247
300
  // Directory file names end with '/'.
248
- // Note that entires for directories themselves are optional.
301
+ // Note that entries for directories themselves are optional.
249
302
  // An entry's fileName implicitly requires its parent directories to exist.
250
303
  await exfs.ensureFolder(entryContext.getFilePath());
251
- zfile.readEntry();
304
+ this.readNextEntry(zfile, token);
252
305
  }
253
306
  else {
254
307
  // file entry
@@ -256,13 +309,13 @@ class Unzip extends cancelable_1.Cancelable {
256
309
  }
257
310
  }
258
311
  openZipFileStream(zfile, entry, token) {
259
- return new Promise((c, e) => {
312
+ return new Promise((resolve, reject) => {
260
313
  zfile.openReadStream(entry, (err, readStream) => {
261
314
  if (err) {
262
- e(this.wrapError(err, token.isCancelled));
315
+ reject(this.wrapError(err, token.isCancelled));
263
316
  }
264
317
  else {
265
- c(readStream);
318
+ resolve(readStream);
266
319
  }
267
320
  });
268
321
  });
@@ -270,7 +323,7 @@ class Unzip extends cancelable_1.Cancelable {
270
323
  async extractEntry(zfile, entry, entryContext, token) {
271
324
  const filePath = entryContext.getFilePath();
272
325
  const fileDir = path.dirname(filePath);
273
- await entryContext.ensureFolder(fileDir);
326
+ await entryContext.inspectFolder(fileDir);
274
327
  const outside = await entryContext.isOutsideTargetFolder(fileDir);
275
328
  if (outside) {
276
329
  const error = new Error(`Refuse to write file outside "${entryContext.targetFolder}", file: "${filePath}"`);
@@ -279,55 +332,69 @@ class Unzip extends cancelable_1.Cancelable {
279
332
  }
280
333
  const readStream = await this.openZipFileStream(zfile, entry, token);
281
334
  await this.writeEntryToFile(readStream, entry, entryContext, token);
282
- zfile.readEntry();
335
+ this.readNextEntry(zfile, token);
283
336
  }
284
337
  async writeEntryToFile(readStream, entry, entryContext, token) {
285
- let fileStream;
286
- token.onCancelled(() => {
287
- if (fileStream) {
288
- readStream.unpipe(fileStream);
289
- fileStream.destroy(this.canceledError());
338
+ var _a;
339
+ try {
340
+ const filePath = entryContext.getFilePath();
341
+ const mode = this.modeFromEntry(entry);
342
+ // see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink
343
+ const isSymlink = (mode & 0o170000) === 0o120000;
344
+ if (isSymlink) {
345
+ entryContext.symlinkFileNames.push(path.resolve(path.join(entryContext.targetFolder, entryContext.decodeEntryFileName)));
290
346
  }
291
- });
292
- return new Promise((c, e) => {
293
- try {
294
- const filePath = entryContext.getFilePath();
295
- readStream.once("error", (err) => {
296
- e(this.wrapError(err, token.isCancelled));
297
- });
298
- const mode = this.modeFromEntry(entry);
299
- // see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink
300
- const isSymlink = (mode & 0o170000) === 0o120000;
301
- if (isSymlink) {
302
- entryContext.symlinkFileNames.push(path.resolve(path.join(entryContext.targetFolder, entryContext.decodeEntryFileName)));
347
+ if (isSymlink && !this.symlinkToFile()) {
348
+ const linkContent = await this.readStreamContent(readStream, token);
349
+ if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.safeSymlinksOnly) &&
350
+ entryContext.isSymlinkTargetOutsideTargetFolder(linkContent, filePath)) {
351
+ const error = new Error(`Dangerous link path was refused : "${entryContext.targetFolder}", file: "${filePath}", target: "${linkContent}". Set safeSymlinksOnly to false to allow writing through this symlink.`);
352
+ error.name = "AF_ILLEGAL_TARGET";
353
+ throw error;
303
354
  }
304
- if (isSymlink && !this.symlinkToFile()) {
305
- let linkContent = "";
306
- readStream.on("data", (chunk) => {
307
- if (chunk instanceof String) {
308
- linkContent += chunk;
309
- }
310
- else {
311
- linkContent += chunk.toString();
312
- }
313
- });
314
- readStream.once("end", () => {
315
- this.createSymlink(linkContent, filePath).then(c, e);
316
- });
355
+ await entryContext.ensureFolder(path.dirname(filePath));
356
+ await this.createSymlink(linkContent, filePath);
357
+ }
358
+ else {
359
+ await entryContext.ensureFolder(path.dirname(filePath));
360
+ const fileStream = (0, node_fs_1.createWriteStream)(filePath, { mode });
361
+ const pipelinePromise = (0, promises_1.pipeline)(readStream, fileStream);
362
+ const disposeCancel = token.onCancelled(() => {
363
+ fileStream.destroy(this.canceledError());
364
+ });
365
+ try {
366
+ await pipelinePromise;
317
367
  }
318
- else {
319
- fileStream = (0, node_fs_1.createWriteStream)(filePath, { mode });
320
- fileStream.once("close", () => c());
321
- fileStream.once("error", (err) => {
322
- e(this.wrapError(err, token.isCancelled));
323
- });
324
- readStream.pipe(fileStream);
368
+ finally {
369
+ disposeCancel();
325
370
  }
326
371
  }
327
- catch (error) {
328
- e(this.wrapError(error, token.isCancelled));
329
- }
372
+ }
373
+ catch (error) {
374
+ throw this.wrapError(error, token.isCancelled);
375
+ }
376
+ }
377
+ async readStreamContent(readStream, token) {
378
+ let linkContent = "";
379
+ const readPromise = new Promise((resolve, reject) => {
380
+ readStream.on("data", (chunk) => {
381
+ linkContent += typeof chunk === "string" ? chunk : chunk.toString();
382
+ });
383
+ readStream.once("end", resolve);
384
+ readStream.once("error", (err) => {
385
+ reject(this.wrapError(err, token.isCancelled));
386
+ });
387
+ });
388
+ const disposeCancel = token.onCancelled(() => {
389
+ readStream.destroy(this.canceledError());
330
390
  });
391
+ try {
392
+ await readPromise;
393
+ return linkContent;
394
+ }
395
+ finally {
396
+ disposeCancel();
397
+ }
331
398
  }
332
399
  modeFromEntry(entry) {
333
400
  const attr = entry.externalFileAttributes >> 16 || 33188;
@@ -335,7 +402,7 @@ class Unzip extends cancelable_1.Cancelable {
335
402
  .map((mask) => attr & mask)
336
403
  .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
337
404
  }
338
- async createSymlink(linkContent, des) {
405
+ async createSymlink(linkContent, filePath) {
339
406
  let linkType = "file";
340
407
  if (process.platform === "win32") {
341
408
  if (/\/$/.test(linkContent)) {
@@ -344,7 +411,7 @@ class Unzip extends cancelable_1.Cancelable {
344
411
  else {
345
412
  let targetPath = linkContent;
346
413
  if (!path.isAbsolute(linkContent)) {
347
- targetPath = path.join(path.dirname(des), linkContent);
414
+ targetPath = path.join(path.dirname(filePath), linkContent);
348
415
  }
349
416
  try {
350
417
  const stat = await fs.stat(targetPath);
@@ -357,7 +424,7 @@ class Unzip extends cancelable_1.Cancelable {
357
424
  }
358
425
  }
359
426
  }
360
- await fs.symlink(linkContent, des, linkType);
427
+ await fs.symlink(linkContent, filePath, linkType);
361
428
  }
362
429
  isOverwrite() {
363
430
  var _a;
package/lib/zip.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Cancelable } from "./cancelable";
2
2
  export interface IZipOptions {
3
3
  /**
4
- * Indicates how to handle when the given path is a symbolic link.
4
+ * Indicates how to handle the given path when it is a symbolic link.
5
5
  *
6
6
  * `true`: add the target of the symbolic link to the zip.
7
7
  *
8
- * `false`: add symbolic link itself to the zip.
8
+ * `false`: add the symbolic link itself to the zip.
9
9
  *
10
10
  * The default value is `false`.
11
11
  */
@@ -13,7 +13,7 @@ export interface IZipOptions {
13
13
  /**
14
14
  * Sets the compression level.
15
15
  *
16
- * 0: the file data will be stored, otherwise, the file data will be deflated.
16
+ * `0`: the file data will be stored; otherwise, the file data will be deflated.
17
17
  *
18
18
  * The default value is `6`.
19
19
  */
@@ -30,29 +30,44 @@ export declare class Zip extends Cancelable {
30
30
  constructor(options?: IZipOptions | undefined);
31
31
  private yazlFile;
32
32
  private isPipe;
33
+ private isChunk;
33
34
  private zipStream;
34
35
  private zipFiles;
35
36
  private zipFolders;
36
37
  private token;
37
38
  /**
38
- * Adds a file from the file system at realPath into the zipfile as metadataPath.
39
+ * Adds a file from the file system at `realPath` to the zip file as `metadataPath`.
39
40
  * @param file
40
- * @param metadataPath Typically metadataPath would be calculated as path.relative(root, realPath).
41
+ * @param metadataPath Typically, `metadataPath` would be calculated as `path.relative(root, realPath)`.
41
42
  * A valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain "..".
42
43
  */
43
44
  addFile(file: string, metadataPath?: string): void;
44
45
  /**
45
- * Adds a folder from the file system at realPath into the zipfile as metadataPath.
46
+ * Adds a folder from the file system at `realPath` to the zip file as `metadataPath`.
46
47
  * @param folder
47
- * @param metadataPath Typically metadataPath would be calculated as path.relative(root, realPath).
48
+ * @param metadataPath Typically, `metadataPath` would be calculated as `path.relative(root, realPath)`.
48
49
  * A valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain "..".
49
50
  */
50
51
  addFolder(folder: string, metadataPath?: string): void;
51
52
  /**
52
- * Generate zip file.
53
- * @param zipFile the zip file path.
53
+ * Zips the content and returns it as a single Buffer.
54
+ *
55
+ * @returns A promise that resolves to the zipped Buffer.
56
+ */
57
+ archive(): Promise<Buffer>;
58
+ /**
59
+ * Zips the content and saves it directly to the specified file path.
60
+ *
61
+ * @param zipFile The absolute or relative path where the .zip file will be created.
62
+ * @returns A promise that resolves when the file has been fully written.
54
63
  */
55
64
  archive(zipFile: string): Promise<void>;
65
+ private prepareArchive;
66
+ private bindArchiveOutput;
67
+ private archiveToFile;
68
+ private archiveToBuffer;
69
+ private createPromiseSettler;
70
+ private addQueuedEntries;
56
71
  /**
57
72
  * Cancel compression.
58
73
  * If the cancel method is called after the archive is complete, nothing will happen.
@@ -62,7 +77,7 @@ export declare class Zip extends Cancelable {
62
77
  private addFileStream;
63
78
  private addSymlink;
64
79
  private walkDir;
65
- private stopPipe;
80
+ private stop;
66
81
  private followSymlink;
67
82
  /**
68
83
  * Retrieves the yazl options based on the current settings.