zip-lib 1.2.3 → 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() {
@@ -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,124 @@ 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
+ 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) {
130
145
  if (this.isOverwrite()) {
131
146
  await exfs.rimraf(targetFolder);
132
147
  }
133
148
  if (token.isCancelled) {
134
- return Promise.reject(this.canceledError());
149
+ throw this.canceledError();
135
150
  }
136
151
  await exfs.ensureFolder(targetFolder);
137
152
  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) => {
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;
142
159
  let anyError = null;
143
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
+ });
144
168
  zfile.once("error", (err) => {
169
+ disposeCancel();
170
+ anyError = this.wrapError(err, token.isCancelled);
145
171
  this.closeZip();
146
- e(this.wrapError(err, token.isCancelled));
172
+ settle.reject(anyError);
147
173
  });
148
174
  zfile.once("close", () => {
175
+ disposeCancel();
149
176
  this.zipFile = null;
150
177
  if (anyError) {
151
- e(this.wrapError(anyError, token.isCancelled));
178
+ settle.reject(this.wrapError(anyError, token.isCancelled));
152
179
  }
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
- }
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();
161
186
  }
162
187
  });
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
188
  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
189
  try {
190
- if (entryEvent.isPrevented) {
191
- entryEvent.reset();
192
- zfile.readEntry();
193
- }
194
- else {
195
- await this.handleEntry(zfile, entry, entryContext, token);
196
- }
190
+ await this.handleZipEntry(zfile, entry, entryContext, entryEvent, token);
197
191
  extractedEntriesCount++;
198
- if (extractedEntriesCount === total) {
199
- c();
192
+ if (extractedEntriesCount >= total) {
193
+ settle.resolve();
200
194
  }
201
195
  }
202
196
  catch (error) {
203
197
  anyError = this.wrapError(error, token.isCancelled);
204
198
  this.closeZip();
205
- e(anyError);
199
+ settle.reject(anyError);
206
200
  }
207
201
  });
202
+ this.readNextEntry(zfile, token);
208
203
  });
209
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
+ }
210
253
  /**
211
- * Cancel decompression.
254
+ * Cancel extraction.
212
255
  * If the cancel method is called after the extract is complete, nothing will happen.
213
256
  */
214
257
  cancel() {
@@ -224,29 +267,41 @@ class Unzip extends cancelable_1.Cancelable {
224
267
  this.zipFile = null;
225
268
  }
226
269
  }
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));
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);
236
288
  }
237
289
  else {
238
- c(zfile);
290
+ yauzl.fromBuffer(zipFileOrBuffer, options, callback);
239
291
  }
240
292
  });
241
- });
293
+ }
294
+ catch (error) {
295
+ throw this.wrapError(error, token.isCancelled);
296
+ }
242
297
  }
243
298
  async handleEntry(zfile, entry, entryContext, token) {
244
299
  if (/\/$/.test(entryContext.decodeEntryFileName)) {
245
300
  // Directory file names end with '/'.
246
- // Note that entires for directories themselves are optional.
301
+ // Note that entries for directories themselves are optional.
247
302
  // An entry's fileName implicitly requires its parent directories to exist.
248
303
  await exfs.ensureFolder(entryContext.getFilePath());
249
- zfile.readEntry();
304
+ this.readNextEntry(zfile, token);
250
305
  }
251
306
  else {
252
307
  // file entry
@@ -254,13 +309,13 @@ class Unzip extends cancelable_1.Cancelable {
254
309
  }
255
310
  }
256
311
  openZipFileStream(zfile, entry, token) {
257
- return new Promise((c, e) => {
312
+ return new Promise((resolve, reject) => {
258
313
  zfile.openReadStream(entry, (err, readStream) => {
259
314
  if (err) {
260
- e(this.wrapError(err, token.isCancelled));
315
+ reject(this.wrapError(err, token.isCancelled));
261
316
  }
262
317
  else {
263
- c(readStream);
318
+ resolve(readStream);
264
319
  }
265
320
  });
266
321
  });
@@ -268,7 +323,7 @@ class Unzip extends cancelable_1.Cancelable {
268
323
  async extractEntry(zfile, entry, entryContext, token) {
269
324
  const filePath = entryContext.getFilePath();
270
325
  const fileDir = path.dirname(filePath);
271
- await entryContext.ensureFolder(fileDir);
326
+ await entryContext.inspectFolder(fileDir);
272
327
  const outside = await entryContext.isOutsideTargetFolder(fileDir);
273
328
  if (outside) {
274
329
  const error = new Error(`Refuse to write file outside "${entryContext.targetFolder}", file: "${filePath}"`);
@@ -277,64 +332,69 @@ class Unzip extends cancelable_1.Cancelable {
277
332
  }
278
333
  const readStream = await this.openZipFileStream(zfile, entry, token);
279
334
  await this.writeEntryToFile(readStream, entry, entryContext, token);
280
- zfile.readEntry();
335
+ this.readNextEntry(zfile, token);
281
336
  }
282
337
  async writeEntryToFile(readStream, entry, entryContext, token) {
283
- let fileStream;
284
- token.onCancelled(() => {
285
- if (fileStream) {
286
- readStream.unpipe(fileStream);
287
- 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)));
288
346
  }
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)));
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;
301
354
  }
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
- });
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;
324
367
  }
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);
368
+ finally {
369
+ disposeCancel();
332
370
  }
333
371
  }
334
- catch (error) {
335
- e(this.wrapError(error, token.isCancelled));
336
- }
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());
337
390
  });
391
+ try {
392
+ await readPromise;
393
+ return linkContent;
394
+ }
395
+ finally {
396
+ disposeCancel();
397
+ }
338
398
  }
339
399
  modeFromEntry(entry) {
340
400
  const attr = entry.externalFileAttributes >> 16 || 33188;
@@ -342,7 +402,7 @@ class Unzip extends cancelable_1.Cancelable {
342
402
  .map((mask) => attr & mask)
343
403
  .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
344
404
  }
345
- async createSymlink(linkContent, des) {
405
+ async createSymlink(linkContent, filePath) {
346
406
  let linkType = "file";
347
407
  if (process.platform === "win32") {
348
408
  if (/\/$/.test(linkContent)) {
@@ -351,7 +411,7 @@ class Unzip extends cancelable_1.Cancelable {
351
411
  else {
352
412
  let targetPath = linkContent;
353
413
  if (!path.isAbsolute(linkContent)) {
354
- targetPath = path.join(path.dirname(des), linkContent);
414
+ targetPath = path.join(path.dirname(filePath), linkContent);
355
415
  }
356
416
  try {
357
417
  const stat = await fs.stat(targetPath);
@@ -364,7 +424,7 @@ class Unzip extends cancelable_1.Cancelable {
364
424
  }
365
425
  }
366
426
  }
367
- await fs.symlink(linkContent, des, linkType);
427
+ await fs.symlink(linkContent, filePath, linkType);
368
428
  }
369
429
  isOverwrite() {
370
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.