zip-lib 1.2.3 → 1.3.1

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