@appium/support 7.0.4 → 7.0.6

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.
Files changed (113) hide show
  1. package/build/lib/console.d.ts +42 -88
  2. package/build/lib/console.d.ts.map +1 -1
  3. package/build/lib/console.js +25 -85
  4. package/build/lib/console.js.map +1 -1
  5. package/build/lib/doctor.d.ts +6 -18
  6. package/build/lib/doctor.d.ts.map +1 -1
  7. package/build/lib/doctor.js +0 -15
  8. package/build/lib/doctor.js.map +1 -1
  9. package/build/lib/env.d.ts +14 -20
  10. package/build/lib/env.d.ts.map +1 -1
  11. package/build/lib/env.js +24 -61
  12. package/build/lib/env.js.map +1 -1
  13. package/build/lib/fs.d.ts +109 -148
  14. package/build/lib/fs.d.ts.map +1 -1
  15. package/build/lib/fs.js +130 -230
  16. package/build/lib/fs.js.map +1 -1
  17. package/build/lib/image-util.d.ts +7 -6
  18. package/build/lib/image-util.d.ts.map +1 -1
  19. package/build/lib/image-util.js +9 -6
  20. package/build/lib/image-util.js.map +1 -1
  21. package/build/lib/index.d.ts +19 -17
  22. package/build/lib/index.d.ts.map +1 -1
  23. package/build/lib/logger.d.ts +1 -1
  24. package/build/lib/logger.d.ts.map +1 -1
  25. package/build/lib/logger.js +1 -1
  26. package/build/lib/logger.js.map +1 -1
  27. package/build/lib/logging.d.ts +7 -15
  28. package/build/lib/logging.d.ts.map +1 -1
  29. package/build/lib/logging.js +36 -62
  30. package/build/lib/logging.js.map +1 -1
  31. package/build/lib/mjpeg.d.ts +19 -56
  32. package/build/lib/mjpeg.d.ts.map +1 -1
  33. package/build/lib/mjpeg.js +55 -78
  34. package/build/lib/mjpeg.js.map +1 -1
  35. package/build/lib/mkdirp.d.ts +4 -1
  36. package/build/lib/mkdirp.d.ts.map +1 -1
  37. package/build/lib/mkdirp.js +1 -2
  38. package/build/lib/mkdirp.js.map +1 -1
  39. package/build/lib/net.d.ts +52 -90
  40. package/build/lib/net.d.ts.map +1 -1
  41. package/build/lib/net.js +104 -193
  42. package/build/lib/net.js.map +1 -1
  43. package/build/lib/node.d.ts +16 -17
  44. package/build/lib/node.d.ts.map +1 -1
  45. package/build/lib/node.js +115 -120
  46. package/build/lib/node.js.map +1 -1
  47. package/build/lib/npm.d.ts +65 -86
  48. package/build/lib/npm.d.ts.map +1 -1
  49. package/build/lib/npm.js +64 -122
  50. package/build/lib/npm.js.map +1 -1
  51. package/build/lib/plist.d.ts +36 -29
  52. package/build/lib/plist.d.ts.map +1 -1
  53. package/build/lib/plist.js +62 -59
  54. package/build/lib/plist.js.map +1 -1
  55. package/build/lib/process.d.ts +19 -2
  56. package/build/lib/process.d.ts.map +1 -1
  57. package/build/lib/process.js +24 -7
  58. package/build/lib/process.js.map +1 -1
  59. package/build/lib/system.d.ts +41 -6
  60. package/build/lib/system.d.ts.map +1 -1
  61. package/build/lib/system.js +49 -14
  62. package/build/lib/system.js.map +1 -1
  63. package/build/lib/tempdir.d.ts +26 -49
  64. package/build/lib/tempdir.d.ts.map +1 -1
  65. package/build/lib/tempdir.js +46 -78
  66. package/build/lib/tempdir.js.map +1 -1
  67. package/build/lib/timing.d.ts +28 -22
  68. package/build/lib/timing.d.ts.map +1 -1
  69. package/build/lib/timing.js +16 -17
  70. package/build/lib/timing.js.map +1 -1
  71. package/build/lib/util.d.ts +164 -181
  72. package/build/lib/util.d.ts.map +1 -1
  73. package/build/lib/util.js +198 -253
  74. package/build/lib/util.js.map +1 -1
  75. package/build/lib/zip.d.ts +81 -139
  76. package/build/lib/zip.d.ts.map +1 -1
  77. package/build/lib/zip.js +235 -283
  78. package/build/lib/zip.js.map +1 -1
  79. package/lib/console.ts +139 -0
  80. package/lib/{doctor.js → doctor.ts} +6 -20
  81. package/lib/{env.js → env.ts} +34 -62
  82. package/lib/fs.ts +453 -0
  83. package/lib/image-util.ts +40 -0
  84. package/lib/index.ts +1 -0
  85. package/lib/{logger.js → logger.ts} +1 -1
  86. package/lib/logging.ts +157 -0
  87. package/lib/mjpeg.ts +186 -0
  88. package/lib/{mkdirp.js → mkdirp.ts} +2 -2
  89. package/lib/net.ts +305 -0
  90. package/lib/{node.js → node.ts} +136 -135
  91. package/lib/npm.ts +291 -0
  92. package/lib/plist.ts +187 -0
  93. package/lib/process.ts +62 -0
  94. package/lib/system.ts +95 -0
  95. package/lib/tempdir.ts +115 -0
  96. package/lib/{timing.js → timing.ts} +28 -33
  97. package/lib/util.ts +561 -0
  98. package/lib/{zip.js → zip.ts} +344 -299
  99. package/package.json +24 -26
  100. package/tsconfig.json +3 -5
  101. package/index.js +0 -1
  102. package/lib/console.js +0 -173
  103. package/lib/fs.js +0 -496
  104. package/lib/image-util.js +0 -32
  105. package/lib/logging.js +0 -145
  106. package/lib/mjpeg.js +0 -207
  107. package/lib/net.js +0 -336
  108. package/lib/npm.js +0 -310
  109. package/lib/plist.js +0 -182
  110. package/lib/process.js +0 -46
  111. package/lib/system.js +0 -48
  112. package/lib/tempdir.js +0 -131
  113. package/lib/util.js +0 -585
@@ -1,199 +1,60 @@
1
1
  import _ from 'lodash';
2
- import B from 'bluebird';
2
+ import {promisify} from 'node:util';
3
3
  import * as yauzl from 'yauzl';
4
4
  import archiver from 'archiver';
5
- import {createWriteStream} from 'fs';
6
- import path from 'path';
7
- import stream from 'stream';
5
+ import {createWriteStream} from 'node:fs';
6
+ import path from 'node:path';
7
+ import stream from 'node:stream';
8
+ import {pipeline} from 'node:stream/promises';
8
9
  import {fs} from './fs';
9
10
  import {isWindows} from './system';
10
11
  import {Base64Encode} from 'base64-stream';
11
- import {toReadableSizeString, GiB} from './util';
12
+ import {isSubPath, toReadableSizeString, GiB} from './util';
12
13
  import {Timer} from './timing';
13
14
  import log from './logger';
14
15
  import getStream from 'get-stream';
15
16
  import {exec} from 'teen_process';
16
17
 
17
- /**
18
- * @type {(path: string, options?: yauzl.Options) => Promise<yauzl.ZipFile>}
19
- */
20
- const openZip = B.promisify(yauzl.open);
21
- /**
22
- * @type {(source: NodeJS.ReadableStream, destination: NodeJS.WritableStream) => Promise<NodeJS.WritableStream>}
23
- */
24
- const pipeline = B.promisify(stream.pipeline);
25
- const ZIP_MAGIC = 'PK';
26
- const IFMT = 61440;
27
- const IFDIR = 16384;
28
- const IFLNK = 40960;
29
-
30
- // This class is mostly copied from https://github.com/maxogden/extract-zip/blob/master/index.js
31
- class ZipExtractor {
32
- /** @type {yauzl.ZipFile} */
33
- zipfile;
34
-
35
- constructor(sourcePath, opts = {}) {
36
- this.zipPath = sourcePath;
37
- this.opts = opts;
38
- this.canceled = false;
39
- }
40
-
41
- extractFileName(entry) {
42
- return _.isBuffer(entry.fileName)
43
- ? entry.fileName.toString(this.opts.fileNamesEncoding)
44
- : entry.fileName;
45
- }
46
-
47
- async extract() {
48
- const {dir, fileNamesEncoding} = this.opts;
49
- this.zipfile = await openZip(this.zipPath, {
50
- lazyEntries: true,
51
- // https://github.com/thejoshwolfe/yauzl/commit/cc7455ac789ba84973184e5ebde0581cdc4c3b39#diff-04c6e90faac2675aa89e2176d2eec7d8R95
52
- decodeStrings: !fileNamesEncoding,
53
- });
54
- this.canceled = false;
18
+ const openZip = promisify(yauzl.open) as (
19
+ zipPath: string,
20
+ options?: yauzl.Options
21
+ ) => Promise<yauzl.ZipFile>;
55
22
 
56
- return new B((resolve, reject) => {
57
- this.zipfile.on('error', (err) => {
58
- this.canceled = true;
59
- reject(err);
60
- });
61
- this.zipfile.readEntry();
62
-
63
- this.zipfile.on('close', () => {
64
- if (!this.canceled) {
65
- resolve();
66
- }
67
- });
68
-
69
- this.zipfile.on('entry', async (entry) => {
70
- if (this.canceled) {
71
- return;
72
- }
73
-
74
- const fileName = this.extractFileName(entry);
75
- if (fileName.startsWith('__MACOSX/')) {
76
- this.zipfile.readEntry();
77
- return;
78
- }
79
-
80
- const destDir = path.dirname(path.join(dir, fileName));
81
- try {
82
- await fs.mkdir(destDir, {recursive: true});
83
-
84
- const canonicalDestDir = await fs.realpath(destDir);
85
- const relativeDestDir = path.relative(dir, canonicalDestDir);
86
-
87
- if (relativeDestDir.split(path.sep).includes('..')) {
88
- new Error(
89
- `Out of bound path "${canonicalDestDir}" found while processing file ${fileName}`
90
- );
91
- }
92
-
93
- await this.extractEntry(entry);
94
- this.zipfile.readEntry();
95
- } catch (err) {
96
- this.canceled = true;
97
- this.zipfile.close();
98
- reject(err);
99
- }
100
- });
101
- });
102
- }
103
-
104
- async extractEntry(entry) {
105
- if (this.canceled) {
106
- return;
107
- }
108
-
109
- const {dir} = this.opts;
110
-
111
- const fileName = this.extractFileName(entry);
112
- const dest = path.join(dir, fileName);
113
-
114
- // convert external file attr int into a fs stat mode int
115
- const mode = (entry.externalFileAttributes >> 16) & 0xffff;
116
- // check if it's a symlink or dir (using stat mode constants)
117
- const isSymlink = (mode & IFMT) === IFLNK;
118
- const isDir =
119
- (mode & IFMT) === IFDIR ||
120
- // Failsafe, borrowed from jsZip
121
- fileName.endsWith('/') ||
122
- // check for windows weird way of specifying a directory
123
- // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
124
- (entry.versionMadeBy >> 8 === 0 && entry.externalFileAttributes === 16);
125
- const procMode = this.getExtractedMode(mode, isDir) & 0o777;
126
- // always ensure folders are created
127
- const destDir = isDir ? dest : path.dirname(dest);
128
- const mkdirOptions = {recursive: true};
129
- if (isDir) {
130
- mkdirOptions.mode = procMode;
131
- }
132
- await fs.mkdir(destDir, mkdirOptions);
133
- if (isDir) {
134
- return;
135
- }
136
-
137
- /** @type {(entry: yauzl.Entry) => Promise<NodeJS.ReadableStream>} */
138
- const openReadStream = B.promisify(this.zipfile.openReadStream.bind(this.zipfile));
139
- const readStream = await openReadStream(entry);
140
- if (isSymlink) {
141
- // @ts-ignore This typecast is ok
142
- const link = await getStream(readStream);
143
- await fs.symlink(link, dest);
144
- } else {
145
- await pipeline(readStream, fs.createWriteStream(dest, {mode: procMode}));
146
- }
147
- }
148
-
149
- getExtractedMode(entryMode, isDir) {
150
- const {defaultDirMode, defaultFileMode} = this.opts;
151
-
152
- let mode = entryMode;
153
- // Set defaults, if necessary
154
- if (mode === 0) {
155
- if (isDir) {
156
- if (defaultDirMode) {
157
- mode = parseInt(defaultDirMode, 10);
158
- }
159
-
160
- if (!mode) {
161
- mode = 0o755;
162
- }
163
- } else {
164
- if (defaultFileMode) {
165
- mode = parseInt(defaultFileMode, 10);
166
- }
23
+ const ZIP_MAGIC = 'PK';
24
+ const IFMT = 0b1111000000000000;
25
+ const IFDIR = 0b0100000000000000;
26
+ const IFLNK = 0b1010000000000000;
167
27
 
168
- if (!mode) {
169
- mode = 0o644;
170
- }
171
- }
172
- }
28
+ // Internal extraction helpers are defined near the end of the file.
173
29
 
174
- return mode;
175
- }
30
+ export interface ExtractAllOptions {
31
+ /**
32
+ * The encoding to use for extracted file names.
33
+ * For ZIP archives created on MacOS it is usually expected to be `utf8`.
34
+ * By default it is autodetected based on the entry metadata and is only needed to be set explicitly
35
+ * if the particular archive does not comply to the standards, which leads to corrupted file names
36
+ * after extraction. Only applicable if system unzip binary is NOT being used.
37
+ */
38
+ fileNamesEncoding?: BufferEncoding;
39
+ /**
40
+ * If true, attempt to use system unzip; if this fails,
41
+ * fallback to the JS unzip implementation.
42
+ */
43
+ useSystemUnzip?: boolean;
176
44
  }
177
45
 
178
- /**
179
- * @typedef ExtractAllOptions
180
- * @property {string} [fileNamesEncoding] The encoding to use for extracted file names.
181
- * For ZIP archives created on MacOS it is usually expected to be `utf8`.
182
- * By default it is autodetected based on the entry metadata and is only needed to be set explicitly
183
- * if the particular archive does not comply to the standards, which leads to corrupted file names
184
- * after extraction. Only applicable if system unzip binary is NOT being used.
185
- * @property {boolean} [useSystemUnzip] If true, attempt to use system unzip; if this fails,
186
- * fallback to the JS unzip implementation.
187
- */
188
-
189
46
  /**
190
47
  * Extract zipfile to a directory
191
48
  *
192
- * @param {string} zipFilePath The full path to the source ZIP file
193
- * @param {string} destDir The full path to the destination folder
194
- * @param {ExtractAllOptions} [opts]
49
+ * @param zipFilePath The full path to the source ZIP file
50
+ * @param destDir The full path to the destination folder
51
+ * @param opts Extraction options
195
52
  */
196
- async function extractAllTo(zipFilePath, destDir, opts = /** @type {ExtractAllOptions} */ ({})) {
53
+ export async function extractAllTo(
54
+ zipFilePath: string,
55
+ destDir: string,
56
+ opts: ExtractAllOptions = {}
57
+ ): Promise<void> {
197
58
  if (!path.isAbsolute(destDir)) {
198
59
  throw new Error(`Target path '${destDir}' is expected to be absolute`);
199
60
  }
@@ -204,63 +65,36 @@ async function extractAllTo(zipFilePath, destDir, opts = /** @type {ExtractAllOp
204
65
  try {
205
66
  await extractWithSystemUnzip(zipFilePath, dir);
206
67
  return;
207
- } catch (err) {
68
+ } catch (err: any) {
208
69
  log.warn('unzip failed; falling back to JS: %s', err.stderr || err.message);
209
70
  }
210
71
  }
211
72
  const extractor = new ZipExtractor(zipFilePath, {
212
- ...opts,
73
+ ...(opts as ExtractAllOptions & Partial<ZipExtractorOptions>),
213
74
  dir,
214
75
  });
215
76
  await extractor.extract();
216
77
  }
217
78
 
218
- /**
219
- * Executes system unzip (e.g., `/usr/bin/unzip`). If available, it is
220
- * significantly faster than the JS implementation.
221
- * By default all files in the destDir get overridden if already exist.
222
- *
223
- * @param {string} zipFilePath The full path to the source ZIP file
224
- * @param {string} destDir The full path to the destination folder.
225
- * This folder is expected to already exist before extracting the archive.
226
- */
227
- async function extractWithSystemUnzip(zipFilePath, destDir) {
228
- const isWindowsHost = isWindows();
229
- let executablePath;
230
- try {
231
- executablePath = await getExecutablePath(isWindowsHost ? 'powershell.exe' : 'unzip');
232
- } catch {
233
- throw new Error('Could not find system unzip');
234
- }
235
-
236
- if (isWindowsHost) {
237
- // on Windows we use PowerShell to unzip files
238
- await exec(executablePath, [
239
- '-command',
240
- 'Expand-Archive',
241
- '-LiteralPath',
242
- zipFilePath,
243
- '-DestinationPath',
244
- destDir,
245
- '-Force',
246
- ]);
247
- } else {
248
- // -q means quiet (no stdout)
249
- // -o means overwrite
250
- // -d is the dest dir
251
- await exec(executablePath, ['-q', '-o', zipFilePath, '-d', destDir]);
252
- }
253
- }
254
-
255
79
  /**
256
80
  * Extract a single zip entry to a directory
257
81
  *
258
- * @param {yauzl.ZipFile} zipFile The source ZIP stream
259
- * @param {yauzl.Entry} entry The entry instance
260
- * @param {string} destDir The full path to the destination folder
82
+ * @private
83
+ * @param zipFile The source ZIP stream
84
+ * @param entry The entry instance
85
+ * @param destDir The full path to the destination folder
261
86
  */
262
- async function _extractEntryTo(zipFile, entry, destDir) {
87
+ export async function _extractEntryTo(
88
+ zipFile: yauzl.ZipFile,
89
+ entry: yauzl.Entry,
90
+ destDir: string
91
+ ): Promise<void> {
263
92
  const dstPath = path.resolve(destDir, entry.fileName);
93
+ if (!isSubPath(dstPath, destDir)) {
94
+ throw new Error(
95
+ `Out of bound path "${dstPath}" found while processing file ${entry.fileName}`
96
+ );
97
+ }
264
98
 
265
99
  // Create dest directory if doesn't exist already
266
100
  if (entry.fileName.endsWith('/')) {
@@ -274,54 +108,59 @@ async function _extractEntryTo(zipFile, entry, destDir) {
274
108
 
275
109
  // Create a write stream
276
110
  const writeStream = createWriteStream(dstPath, {flags: 'w'});
277
- const writeStreamPromise = new B((resolve, reject) => {
111
+ const writeStreamPromise = new Promise<void>((resolve, reject) => {
278
112
  writeStream.once('finish', resolve);
279
113
  writeStream.once('error', reject);
280
114
  });
281
115
 
282
- // Create zipReadStream and pipe data to the write stream
283
- // (for some odd reason B.promisify doesn't work on zipfile.openReadStream, it causes an error 'closed')
284
- const zipReadStream = await new B((resolve, reject) => {
285
- zipFile.openReadStream(entry, (err, readStream) => (err ? reject(err) : resolve(readStream)));
286
- });
287
- const zipReadStreamPromise = new B((resolve, reject) => {
116
+ const openReadStream = promisify(zipFile.openReadStream.bind(zipFile)) as (
117
+ entry: yauzl.Entry
118
+ ) => Promise<NodeJS.ReadableStream>;
119
+ const zipReadStream = await openReadStream(entry);
120
+ const zipReadStreamPromise = new Promise<void>((resolve, reject) => {
288
121
  zipReadStream.once('end', resolve);
289
122
  zipReadStream.once('error', reject);
290
123
  });
291
124
  zipReadStream.pipe(writeStream);
292
125
 
293
126
  // Wait for the zipReadStream and writeStream to end before returning
294
- return await B.all([zipReadStreamPromise, writeStreamPromise]);
127
+ await Promise.all([zipReadStreamPromise, writeStreamPromise]);
295
128
  }
296
129
 
297
- /**
298
- * @typedef ZipEntry
299
- * @property {yauzl.Entry} entry The actual entry instance
300
- * @property {function} extractEntryTo An async function, which accepts one parameter.
301
- * This parameter contains the destination folder path to which this function is going to extract the entry.
302
- */
130
+ export interface ZipEntry {
131
+ /** The actual entry instance */
132
+ entry: yauzl.Entry;
133
+ /**
134
+ * Async function which accepts the destination folder path
135
+ * and extracts this entry into it.
136
+ */
137
+ extractEntryTo: (destDir: string) => Promise<void>;
138
+ }
303
139
 
304
140
  /**
305
141
  * Get entries for a zip folder
306
142
  *
307
- * @param {string} zipFilePath The full path to the source ZIP file
308
- * @param {function} onEntry Callback when entry is read.
143
+ * @param zipFilePath The full path to the source ZIP file
144
+ * @param onEntry Callback when entry is read.
309
145
  * The callback is expected to accept one argument of ZipEntry type.
310
- * The iteration through the source zip file will bi terminated as soon as
146
+ * The iteration through the source zip file will be terminated as soon as
311
147
  * the result of this function equals to `false`.
312
148
  */
313
- async function readEntries(zipFilePath, onEntry) {
149
+ export async function readEntries(
150
+ zipFilePath: string,
151
+ onEntry: (entry: ZipEntry) => boolean | void | Promise<boolean | void>
152
+ ): Promise<void> {
314
153
  // Open a zip file and start reading entries
315
154
  const zipfile = await openZip(zipFilePath, {lazyEntries: true});
316
- const zipReadStreamPromise = new B((resolve, reject) => {
155
+ const zipReadStreamPromise = new Promise<void>((resolve, reject) => {
317
156
  zipfile.once('end', resolve);
318
157
  zipfile.once('error', reject);
319
158
 
320
159
  // On each entry, call 'onEntry' and then read the next entry
321
- zipfile.on('entry', async (entry) => {
160
+ zipfile.on('entry', async (entry: yauzl.Entry) => {
322
161
  const res = await onEntry({
323
162
  entry,
324
- extractEntryTo: async (destDir) => await _extractEntryTo(zipfile, entry, destDir),
163
+ extractEntryTo: async (destDir: string) => await _extractEntryTo(zipfile, entry, destDir),
325
164
  });
326
165
  if (res === false) {
327
166
  return zipfile.emit('end');
@@ -332,46 +171,53 @@ async function readEntries(zipFilePath, onEntry) {
332
171
  zipfile.readEntry();
333
172
 
334
173
  // Wait for the entries to finish being iterated through
335
- return await zipReadStreamPromise;
174
+ await zipReadStreamPromise;
336
175
  }
337
176
 
338
- /**
339
- * @typedef ZipOptions
340
- * @property {boolean} [encodeToBase64=false] Whether to encode
341
- * the resulting archive to a base64-encoded string
342
- * @property {boolean} [isMetered=true] Whether to log the actual
343
- * archiver performance
344
- * @property {number} [maxSize=1073741824] The maximum size of
345
- * the resulting archive in bytes. This is set to 1GB by default, because
346
- * Appium limits the maximum HTTP body size to 1GB. Also, the NodeJS heap
347
- * size must be enough to keep the resulting object (usually this size is
348
- * limited to 1.4 GB)
349
- * @property {number} [level=9] The compression level. The maximum
350
- * level is 9 (the best compression, worst performance). The minimum
351
- * compression level is 0 (no compression).
352
- */
177
+ export interface ZipOptions {
178
+ /** Whether to encode the resulting archive to a base64-encoded string */
179
+ encodeToBase64?: boolean;
180
+ /** Whether to log the actual archiver performance */
181
+ isMetered?: boolean;
182
+ /**
183
+ * The maximum size of the resulting archive in bytes.
184
+ * This is set to 1GB by default, because Appium limits the maximum HTTP body size to 1GB.
185
+ * Also, the NodeJS heap size must be enough to keep the resulting object
186
+ * (usually this size is limited to 1.4 GB)
187
+ */
188
+ maxSize?: number;
189
+ /**
190
+ * The compression level.
191
+ * The maximum level is 9 (the best compression, worst performance).
192
+ * The minimum compression level is 0 (no compression).
193
+ */
194
+ level?: number;
195
+ }
353
196
 
354
197
  /**
355
198
  * Converts contents of local directory to an in-memory .zip buffer
356
199
  *
357
- * @param {string} srcPath The full path to the folder or file being zipped
358
- * @param {ZipOptions} opts Zipping options
359
- * @returns {Promise<Buffer>} Zipped (and encoded if `encodeToBase64` is truthy)
200
+ * @param srcPath The full path to the folder or file being zipped
201
+ * @param opts Zipping options
202
+ * @returns Zipped (and encoded if `encodeToBase64` is truthy)
360
203
  * content of the source path as memory buffer
361
204
  * @throws {Error} if there was an error while reading the source
362
205
  * or the source is too big
363
206
  */
364
- async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
207
+ export async function toInMemoryZip(
208
+ srcPath: string,
209
+ opts: ZipOptions = {}
210
+ ): Promise<Buffer> {
365
211
  if (!(await fs.exists(srcPath))) {
366
212
  throw new Error(`No such file or folder: ${srcPath}`);
367
213
  }
368
214
 
369
215
  const {isMetered = true, encodeToBase64 = false, maxSize = 1 * GiB, level = 9} = opts;
370
- const resultBuffers = [];
216
+ const resultBuffers: Buffer[] = [];
371
217
  let resultBuffersSize = 0;
372
218
  // Create a writable stream that zip buffers will be streamed to
373
219
  const resultWriteStream = new stream.Writable({
374
- write: (buffer, encoding, next) => {
220
+ write(buffer: Buffer, _encoding: string, next: (err?: Error) => void) {
375
221
  resultBuffers.push(buffer);
376
222
  resultBuffersSize += buffer.length;
377
223
  if (maxSize > 0 && resultBuffersSize > maxSize) {
@@ -391,10 +237,10 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
391
237
  const archive = archiver('zip', {
392
238
  zlib: {level},
393
239
  });
394
- let srcSize = null;
240
+ let srcSize: number | null = null;
395
241
  const base64EncoderStream = encodeToBase64 ? new Base64Encode() : null;
396
- const resultWriteStreamPromise = new B((resolve, reject) => {
397
- resultWriteStream.once('error', (e) => {
242
+ const resultWriteStreamPromise = new Promise<void>((resolve, reject) => {
243
+ resultWriteStream.once('error', (e: Error) => {
398
244
  if (base64EncoderStream) {
399
245
  archive.unpipe(base64EncoderStream);
400
246
  base64EncoderStream.unpipe(resultWriteStream);
@@ -410,9 +256,11 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
410
256
  resolve();
411
257
  });
412
258
  });
413
- const archiveStreamPromise = new B((resolve, reject) => {
259
+ const archiveStreamPromise = new Promise<void>((resolve, reject) => {
414
260
  archive.once('finish', resolve);
415
- archive.once('error', (e) => reject(new Error(`Failed to archive '${srcPath}': ${e.message}`)));
261
+ archive.once('error', (e: Error) =>
262
+ reject(new Error(`Failed to archive '${srcPath}': ${e.message}`))
263
+ );
416
264
  });
417
265
  const timer = isMetered ? new Timer().start() : null;
418
266
  if ((await fs.stat(srcPath)).isDirectory()) {
@@ -431,7 +279,7 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
431
279
  archive.finalize();
432
280
 
433
281
  // Wait for the streams to finish
434
- await B.all([archiveStreamPromise, resultWriteStreamPromise]);
282
+ await Promise.all([archiveStreamPromise, resultWriteStreamPromise]);
435
283
 
436
284
  if (timer) {
437
285
  log.debug(
@@ -449,10 +297,10 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
449
297
  /**
450
298
  * Verifies whether the given file is a valid ZIP archive
451
299
  *
452
- * @param {string} filePath - Full path to the file
300
+ * @param filePath - Full path to the file
453
301
  * @throws {Error} If the file does not exist or is not a valid ZIP archive
454
302
  */
455
- async function assertValidZip(filePath) {
303
+ export async function assertValidZip(filePath: string): Promise<boolean> {
456
304
  if (!(await fs.exists(filePath))) {
457
305
  throw new Error(`The file at '${filePath}' does not exist`);
458
306
  }
@@ -478,48 +326,51 @@ async function assertValidZip(filePath) {
478
326
  }
479
327
  }
480
328
 
481
- /**
482
- * @typedef ZipCompressionOptions
483
- * @property {number} level [9] - Compression level in range 0..9
484
- * (greater numbers mean better compression, but longer processing time)
485
- */
329
+ export interface ZipCompressionOptions {
330
+ /**
331
+ * Compression level in range 0..9
332
+ * (greater numbers mean better compression, but longer processing time)
333
+ */
334
+ level?: number;
335
+ }
486
336
 
487
- /**
488
- * @typedef ZipSourceOptions
489
- * @property {string} [pattern='**\/*'] - GLOB pattern for compression
490
- * @property {string} [cwd] - The source root folder (the parent folder of
491
- * the destination file by default)
492
- * @property {string[]} [ignore] - The list of ignored patterns
493
- */
337
+ export interface ZipSourceOptions {
338
+ /** GLOB pattern for compression */
339
+ pattern?: string;
340
+ /** The source root folder (the parent folder of the destination file by default) */
341
+ cwd?: string;
342
+ /** The list of ignored patterns */
343
+ ignore?: string[];
344
+ }
494
345
 
495
346
  /**
496
347
  * Creates an archive based on the given glob pattern
497
348
  *
498
- * @param {string} dstPath - The resulting archive path
499
- * @param {ZipSourceOptions} src - Source options
500
- * @param {ZipCompressionOptions} opts - Compression options
349
+ * @param dstPath - The resulting archive path
350
+ * @param src - Source options
351
+ * @param opts - Compression options
501
352
  * @throws {Error} If there was an error while creating the archive
502
353
  */
503
- async function toArchive(
504
- dstPath,
505
- src = /** @type {ZipSourceOptions} */ ({}),
506
- opts = /** @type {ZipCompressionOptions} */ ({})
507
- ) {
354
+ export async function toArchive(
355
+ dstPath: string,
356
+ src: ZipSourceOptions = {},
357
+ opts: ZipCompressionOptions = {}
358
+ ): Promise<void> {
508
359
  const {level = 9} = opts;
509
360
  const {pattern = '**/*', cwd = path.dirname(dstPath), ignore = []} = src;
510
361
  const archive = archiver('zip', {zlib: {level}});
511
- const stream = fs.createWriteStream(dstPath);
512
- return await new B((resolve, reject) => {
362
+ const outStream = fs.createWriteStream(dstPath);
363
+ await new Promise<void>((resolve, reject) => {
513
364
  archive
514
365
  .glob(pattern, {
515
366
  cwd,
516
367
  ignore,
517
368
  })
518
369
  .on('error', reject)
519
- .pipe(stream);
520
- stream
521
- .on('error', (e) => {
522
- archive.unpipe(stream);
370
+ .pipe(outStream);
371
+ outStream
372
+ .on('error', (e: Error) => {
373
+ archive.unpipe(outStream);
523
374
  archive.abort();
524
375
  archive.destroy();
525
376
  reject(e);
@@ -529,22 +380,215 @@ async function toArchive(
529
380
  });
530
381
  }
531
382
 
383
+ interface ZipExtractorOptions {
384
+ dir: string;
385
+ fileNamesEncoding?: BufferEncoding;
386
+ defaultDirMode?: string;
387
+ defaultFileMode?: string;
388
+ }
389
+
390
+ // This class is mostly copied from https://github.com/maxogden/extract-zip/blob/master/index.js
391
+ class ZipExtractor {
392
+ zipfile!: yauzl.ZipFile;
393
+ private readonly zipPath: string;
394
+ private readonly opts: ZipExtractorOptions;
395
+ private canceled = false;
396
+
397
+ constructor(sourcePath: string, opts: ZipExtractorOptions) {
398
+ this.zipPath = sourcePath;
399
+ this.opts = opts;
400
+ }
401
+
402
+ private extractFileName(entry: yauzl.Entry): string {
403
+ if (Buffer.isBuffer(entry.fileName)) {
404
+ return entry.fileName.toString(this.opts.fileNamesEncoding);
405
+ }
406
+ return entry.fileName;
407
+ }
408
+
409
+ async extract(): Promise<void> {
410
+ const {dir, fileNamesEncoding} = this.opts;
411
+ this.zipfile = await openZip(this.zipPath, {
412
+ lazyEntries: true,
413
+ // https://github.com/thejoshwolfe/yauzl/commit/cc7455ac789ba84973184e5ebde0581cdc4c3b39#diff-04c6e90faac2675aa89e2176d2eec7d8R95
414
+ decodeStrings: !fileNamesEncoding,
415
+ });
416
+ this.canceled = false;
417
+
418
+ return new Promise<void>((resolve, reject) => {
419
+ this.zipfile.on('error', (err: Error) => {
420
+ this.canceled = true;
421
+ reject(err);
422
+ });
423
+ this.zipfile.readEntry();
424
+
425
+ this.zipfile.on('close', () => {
426
+ if (!this.canceled) {
427
+ resolve();
428
+ }
429
+ });
430
+
431
+ this.zipfile.on('entry', async (entry: yauzl.Entry) => {
432
+ if (this.canceled) {
433
+ return;
434
+ }
435
+
436
+ const fileName = this.extractFileName(entry);
437
+ if (fileName.startsWith('__MACOSX/')) {
438
+ this.zipfile.readEntry();
439
+ return;
440
+ }
441
+
442
+ const destDir = path.dirname(path.join(dir, fileName));
443
+ try {
444
+ await fs.mkdir(destDir, {recursive: true});
445
+
446
+ const canonicalDestDir = await fs.realpath(destDir);
447
+ const relativeDestDir = path.relative(dir, canonicalDestDir);
448
+
449
+ if (relativeDestDir.split(path.sep).includes('..')) {
450
+ throw new Error(
451
+ `Out of bound path "${canonicalDestDir}" found while processing file ${fileName}`
452
+ );
453
+ }
454
+
455
+ await this.extractEntry(entry);
456
+ this.zipfile.readEntry();
457
+ } catch (err) {
458
+ this.canceled = true;
459
+ this.zipfile.close();
460
+ reject(err);
461
+ }
462
+ });
463
+ });
464
+ }
465
+
466
+ private async extractEntry(entry: yauzl.Entry): Promise<void> {
467
+ if (this.canceled) {
468
+ return;
469
+ }
470
+
471
+ const {dir} = this.opts;
472
+
473
+ const fileName = this.extractFileName(entry);
474
+ const dest = path.join(dir, fileName);
475
+
476
+ // convert external file attr int into a fs stat mode int
477
+ const mode = (entry.externalFileAttributes >> 16) & 0xffff;
478
+ // check if it's a symlink or dir (using stat mode constants)
479
+ const isSymlink = (mode & IFMT) === IFLNK;
480
+ const isDir =
481
+ (mode & IFMT) === IFDIR ||
482
+ // Failsafe, borrowed from jsZip
483
+ fileName.endsWith('/') ||
484
+ // check for windows weird way of specifying a directory
485
+ // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
486
+ (entry.versionMadeBy >> 8 === 0 && entry.externalFileAttributes === 16);
487
+ const procMode = this.getExtractedMode(mode, isDir) & 0o777;
488
+ // always ensure folders are created
489
+ const destDir = isDir ? dest : path.dirname(dest);
490
+ const mkdirOptions: Parameters<typeof fs.mkdir>[1] = {recursive: true};
491
+ if (isDir) {
492
+ mkdirOptions.mode = procMode;
493
+ }
494
+ await fs.mkdir(destDir, mkdirOptions);
495
+ if (isDir) {
496
+ return;
497
+ }
498
+
499
+ const openReadStream = promisify(
500
+ this.zipfile.openReadStream.bind(this.zipfile)
501
+ ) as (entry: yauzl.Entry) => Promise<NodeJS.ReadableStream>;
502
+ const readStream = await openReadStream(entry);
503
+ if (isSymlink) {
504
+ const link = await getStream(readStream);
505
+ await fs.symlink(link, dest);
506
+ } else {
507
+ await pipeline(readStream, fs.createWriteStream(dest, {mode: procMode}));
508
+ }
509
+ }
510
+
511
+ private getExtractedMode(entryMode: number, isDir: boolean): number {
512
+ const {defaultDirMode, defaultFileMode} = this.opts;
513
+
514
+ let mode = entryMode;
515
+ // Set defaults, if necessary
516
+ if (mode === 0) {
517
+ if (isDir) {
518
+ if (defaultDirMode) {
519
+ mode = parseInt(defaultDirMode, 10);
520
+ }
521
+
522
+ if (!mode) {
523
+ mode = 0o755;
524
+ }
525
+ } else {
526
+ if (defaultFileMode) {
527
+ mode = parseInt(defaultFileMode, 10);
528
+ }
529
+
530
+ if (!mode) {
531
+ mode = 0o644;
532
+ }
533
+ }
534
+ }
535
+
536
+ return mode;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Executes system unzip (e.g., `/usr/bin/unzip`). If available, it is
542
+ * significantly faster than the JS implementation.
543
+ * By default all files in the destDir get overridden if already exist.
544
+ *
545
+ * @param zipFilePath The full path to the source ZIP file
546
+ * @param destDir The full path to the destination folder.
547
+ * This folder is expected to already exist before extracting the archive.
548
+ */
549
+ async function extractWithSystemUnzip(zipFilePath: string, destDir: string): Promise<void> {
550
+ const isWindowsHost = isWindows();
551
+ let executablePath: string;
552
+ try {
553
+ executablePath = await getExecutablePath(isWindowsHost ? 'powershell.exe' : 'unzip');
554
+ } catch {
555
+ throw new Error('Could not find system unzip');
556
+ }
557
+
558
+ if (isWindowsHost) {
559
+ // on Windows we use PowerShell to unzip files
560
+ await exec(executablePath, [
561
+ '-command',
562
+ 'Expand-Archive',
563
+ '-LiteralPath',
564
+ zipFilePath,
565
+ '-DestinationPath',
566
+ destDir,
567
+ '-Force',
568
+ ]);
569
+ } else {
570
+ // -q means quiet (no stdout)
571
+ // -o means overwrite
572
+ // -d is the dest dir
573
+ await exec(executablePath, ['-q', '-o', zipFilePath, '-d', destDir]);
574
+ }
575
+ }
576
+
532
577
  /**
533
578
  * Finds and memoizes the full path to the given executable.
534
579
  * Rejects if it is not found.
535
580
  */
536
581
  const getExecutablePath = _.memoize(
537
582
  /**
538
- * @returns {Promise<string>} Full Path to the executable
583
+ * @returns Full Path to the executable
539
584
  */
540
- async function getExecutablePath(binaryName) {
585
+ async function getExecutablePath(binaryName: string): Promise<string> {
541
586
  const fullPath = await fs.which(binaryName);
542
587
  log.debug(`Found '${binaryName}' at '${fullPath}'`);
543
588
  return fullPath;
544
589
  }
545
590
  );
546
591
 
547
- export {extractAllTo, readEntries, toInMemoryZip, _extractEntryTo, assertValidZip, toArchive};
548
592
  export default {
549
593
  extractAllTo,
550
594
  readEntries,
@@ -552,3 +596,4 @@ export default {
552
596
  assertValidZip,
553
597
  toArchive,
554
598
  };
599
+