appium-ios-remotexpc 2.2.4 → 2.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/src/index.d.ts +1 -0
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/index.js.map +1 -1
  5. package/build/src/services/ios/afc/codec.d.ts +5 -0
  6. package/build/src/services/ios/afc/codec.d.ts.map +1 -1
  7. package/build/src/services/ios/afc/codec.js +10 -0
  8. package/build/src/services/ios/afc/codec.js.map +1 -1
  9. package/build/src/services/ios/installation-proxy/index.d.ts.map +1 -1
  10. package/build/src/services/ios/installation-proxy/index.js +1 -24
  11. package/build/src/services/ios/installation-proxy/index.js.map +1 -1
  12. package/build/src/services/ios/zipconduit/constants.d.ts +20 -0
  13. package/build/src/services/ios/zipconduit/constants.d.ts.map +1 -0
  14. package/build/src/services/ios/zipconduit/constants.js +20 -0
  15. package/build/src/services/ios/zipconduit/constants.js.map +1 -0
  16. package/build/src/services/ios/zipconduit/index.d.ts +38 -0
  17. package/build/src/services/ios/zipconduit/index.d.ts.map +1 -0
  18. package/build/src/services/ios/zipconduit/index.js +148 -0
  19. package/build/src/services/ios/zipconduit/index.js.map +1 -0
  20. package/build/src/services/ios/zipconduit/plists.d.ts +46 -0
  21. package/build/src/services/ios/zipconduit/plists.d.ts.map +1 -0
  22. package/build/src/services/ios/zipconduit/plists.js +87 -0
  23. package/build/src/services/ios/zipconduit/plists.js.map +1 -0
  24. package/build/src/services/ios/zipconduit/stream-zip.d.ts +105 -0
  25. package/build/src/services/ios/zipconduit/stream-zip.d.ts.map +1 -0
  26. package/build/src/services/ios/zipconduit/stream-zip.js +883 -0
  27. package/build/src/services/ios/zipconduit/stream-zip.js.map +1 -0
  28. package/build/src/services/ios/zipconduit/zip-reader.d.ts +15 -0
  29. package/build/src/services/ios/zipconduit/zip-reader.d.ts.map +1 -0
  30. package/build/src/services/ios/zipconduit/zip-reader.js +33 -0
  31. package/build/src/services/ios/zipconduit/zip-reader.js.map +1 -0
  32. package/build/src/services/ios/zipconduit/zip-utils.d.ts +27 -0
  33. package/build/src/services/ios/zipconduit/zip-utils.d.ts.map +1 -0
  34. package/build/src/services/ios/zipconduit/zip-utils.js +116 -0
  35. package/build/src/services/ios/zipconduit/zip-utils.js.map +1 -0
  36. package/build/src/services.d.ts +6 -0
  37. package/build/src/services.d.ts.map +1 -1
  38. package/build/src/services.js +14 -0
  39. package/build/src/services.js.map +1 -1
  40. package/package.json +2 -1
  41. package/src/index.ts +6 -0
  42. package/src/services/ios/afc/codec.ts +15 -0
  43. package/src/services/ios/installation-proxy/index.ts +1 -42
  44. package/src/services/ios/zipconduit/constants.ts +30 -0
  45. package/src/services/ios/zipconduit/index.ts +242 -0
  46. package/src/services/ios/zipconduit/plists.ts +139 -0
  47. package/src/services/ios/zipconduit/stream-zip.ts +1082 -0
  48. package/src/services/ios/zipconduit/zip-reader.ts +48 -0
  49. package/src/services/ios/zipconduit/zip-utils.ts +185 -0
  50. package/src/services.ts +19 -0
@@ -0,0 +1,1082 @@
1
+ /**
2
+ * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE
3
+ * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE
4
+ *
5
+ * Vendored from node-stream-zip@1.15.0, trimmed to the archive-reading paths
6
+ * used by ZipConduit (central-directory listing + streaming entry reads).
7
+ * Removed unused features: extract-to-disk, synchronous entry data, setFs,
8
+ * and the async helpers entriesCount/comment/entry/entryData/extract.
9
+ */
10
+ import { EventEmitter } from 'node:events';
11
+ import fs from 'node:fs';
12
+ import stream from 'node:stream';
13
+ import zlib from 'node:zlib';
14
+
15
+ export interface StreamZipConfig {
16
+ file?: string;
17
+ fd?: number;
18
+ chunkSize?: number;
19
+ storeEntries?: boolean;
20
+ skipEntryNameValidation?: boolean;
21
+ nameEncoding?: string;
22
+ /**
23
+ * When true, skip local header reads for STORED entries (IPAs use STORE).
24
+ * Deflated entries always read the local header for a correct data offset.
25
+ */
26
+ skipLocalHeaderRead?: boolean;
27
+ /** When false, skip CRC32 verification while streaming (faster; default verifies). */
28
+ verifyEntryCrc?: boolean;
29
+ }
30
+
31
+ export type StreamZipEntry = ZipEntry;
32
+
33
+ interface SignatureSearchState {
34
+ win: FileWindowBuffer;
35
+ totalReadLength: number;
36
+ minPos: number;
37
+ lastPos: number;
38
+ chunkSize: number;
39
+ firstByte: number;
40
+ sig: number;
41
+ lastBufferPosition: number;
42
+ lastBytesRead: number;
43
+ complete: () => void;
44
+ }
45
+
46
+ interface EntriesReadState {
47
+ win: FileWindowBuffer;
48
+ pos: number;
49
+ chunkSize: number;
50
+ entriesLeft: number;
51
+ entry: ZipEntry | null;
52
+ move?: boolean;
53
+ }
54
+
55
+ type FsReadCallback = (
56
+ err: NodeJS.ErrnoException | null,
57
+ bytesRead?: number,
58
+ ) => void;
59
+
60
+ const ZIP = {
61
+ LOCHDR: 30,
62
+ LOCSIG: 0x04034b50,
63
+ LOCVER: 4,
64
+ LOCFLG: 6,
65
+ LOCHOW: 8,
66
+ LOCTIM: 10,
67
+ LOCCRC: 14,
68
+ LOCSIZ: 18,
69
+ LOCLEN: 22,
70
+ LOCNAM: 26,
71
+ LOCEXT: 28,
72
+ CENHDR: 46,
73
+ CENSIG: 0x02014b50,
74
+ CENVEM: 4,
75
+ CENVER: 6,
76
+ CENFLG: 8,
77
+ CENHOW: 10,
78
+ CENTIM: 12,
79
+ CENCRC: 16,
80
+ CENSIZ: 20,
81
+ CENLEN: 24,
82
+ CENNAM: 28,
83
+ CENEXT: 30,
84
+ CENCOM: 32,
85
+ CENDSK: 34,
86
+ CENATT: 36,
87
+ CENATX: 38,
88
+ CENOFF: 42,
89
+ ENDHDR: 22,
90
+ ENDSIG: 0x06054b50,
91
+ ENDSIGFIRST: 0x50,
92
+ ENDSUB: 8,
93
+ ENDTOT: 10,
94
+ ENDSIZ: 12,
95
+ ENDOFF: 16,
96
+ ENDCOM: 20,
97
+ MAXFILECOMMENT: 0xffff,
98
+ ENDL64HDR: 20,
99
+ ENDL64SIG: 0x07064b50,
100
+ ENDL64SIGFIRST: 0x50,
101
+ END64HDR: 56,
102
+ END64SIG: 0x06064b50,
103
+ END64SIGFIRST: 0x50,
104
+ END64SUB: 24,
105
+ END64TOT: 32,
106
+ END64SIZ: 40,
107
+ END64OFF: 48,
108
+ STORED: 0,
109
+ DEFLATED: 8,
110
+ FLG_ENTRY_ENC: 1,
111
+ ID_ZIP64: 0x0001,
112
+ EF_ZIP64_OR_32: 0xffffffff,
113
+ EF_ZIP64_OR_16: 0xffff,
114
+ } as const;
115
+
116
+ class CentralDirectoryHeader {
117
+ volumeEntries = 0;
118
+ totalEntries = 0;
119
+ size = 0;
120
+ offset = 0;
121
+ commentLength = 0;
122
+ headerOffset = 0;
123
+
124
+ read(data: Buffer): void {
125
+ if (data.length !== ZIP.ENDHDR || data.readUInt32LE(0) !== ZIP.ENDSIG) {
126
+ throw new Error('Invalid central directory');
127
+ }
128
+ this.volumeEntries = data.readUInt16LE(ZIP.ENDSUB);
129
+ this.totalEntries = data.readUInt16LE(ZIP.ENDTOT);
130
+ this.size = data.readUInt32LE(ZIP.ENDSIZ);
131
+ this.offset = data.readUInt32LE(ZIP.ENDOFF);
132
+ this.commentLength = data.readUInt16LE(ZIP.ENDCOM);
133
+ }
134
+ }
135
+
136
+ class CentralDirectoryLoc64Header {
137
+ headerOffset = 0;
138
+
139
+ read(data: Buffer): void {
140
+ if (
141
+ data.length !== ZIP.ENDL64HDR ||
142
+ data.readUInt32LE(0) !== ZIP.ENDL64SIG
143
+ ) {
144
+ throw new Error('Invalid zip64 central directory locator');
145
+ }
146
+ this.headerOffset = readUInt64LE(data, ZIP.ENDSUB);
147
+ }
148
+ }
149
+
150
+ class CentralDirectoryZip64Header {
151
+ volumeEntries = 0;
152
+ totalEntries = 0;
153
+ size = 0;
154
+ offset = 0;
155
+
156
+ read(data: Buffer): void {
157
+ if (data.length !== ZIP.END64HDR || data.readUInt32LE(0) !== ZIP.END64SIG) {
158
+ throw new Error('Invalid central directory');
159
+ }
160
+ this.volumeEntries = readUInt64LE(data, ZIP.END64SUB);
161
+ this.totalEntries = readUInt64LE(data, ZIP.END64TOT);
162
+ this.size = readUInt64LE(data, ZIP.END64SIZ);
163
+ this.offset = readUInt64LE(data, ZIP.END64OFF);
164
+ }
165
+ }
166
+
167
+ class FsRead {
168
+ bytesRead = 0;
169
+ waiting = false;
170
+
171
+ constructor(
172
+ private readonly fd: number,
173
+ private readonly buffer: Buffer,
174
+ private readonly offset: number,
175
+ private readonly length: number,
176
+ private readonly position: number,
177
+ private readonly callback: FsReadCallback,
178
+ ) {}
179
+
180
+ read(sync = false): this {
181
+ this.waiting = true;
182
+ if (sync) {
183
+ let err: NodeJS.ErrnoException | undefined;
184
+ let bytesRead = 0;
185
+ try {
186
+ bytesRead = fs.readSync(
187
+ this.fd,
188
+ this.buffer,
189
+ this.offset + this.bytesRead,
190
+ this.length - this.bytesRead,
191
+ this.position + this.bytesRead,
192
+ );
193
+ } catch (e) {
194
+ err = e as NodeJS.ErrnoException;
195
+ }
196
+ this.readCallback(sync, err ?? null, err ? bytesRead : null);
197
+ } else {
198
+ fs.read(
199
+ this.fd,
200
+ this.buffer,
201
+ this.offset + this.bytesRead,
202
+ this.length - this.bytesRead,
203
+ this.position + this.bytesRead,
204
+ (err, bytesRead) => this.readCallback(sync, err, bytesRead),
205
+ );
206
+ }
207
+ return this;
208
+ }
209
+
210
+ private readCallback(
211
+ sync: boolean,
212
+ err: NodeJS.ErrnoException | null,
213
+ bytesRead: number | null,
214
+ ): void {
215
+ if (typeof bytesRead === 'number') {
216
+ this.bytesRead += bytesRead;
217
+ }
218
+ if (err || !bytesRead || this.bytesRead === this.length) {
219
+ this.waiting = false;
220
+ this.callback(err, this.bytesRead);
221
+ return;
222
+ }
223
+ this.read(sync);
224
+ }
225
+ }
226
+
227
+ class FileWindowBuffer {
228
+ position = 0;
229
+ buffer = Buffer.alloc(0);
230
+ fsOp: FsRead | null = null;
231
+
232
+ constructor(private readonly fd: number) {}
233
+
234
+ read(pos: number, length: number, callback: FsReadCallback): void {
235
+ this.checkOp();
236
+ if (this.buffer.length < length) {
237
+ this.buffer = Buffer.alloc(length);
238
+ }
239
+ this.position = pos;
240
+ this.fsOp = new FsRead(
241
+ this.fd,
242
+ this.buffer,
243
+ 0,
244
+ length,
245
+ this.position,
246
+ callback,
247
+ );
248
+ this.fsOp.read();
249
+ }
250
+
251
+ expandLeft(length: number, callback: FsReadCallback): void {
252
+ this.checkOp();
253
+ this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]);
254
+ this.position -= length;
255
+ if (this.position < 0) {
256
+ this.position = 0;
257
+ }
258
+ this.fsOp = new FsRead(
259
+ this.fd,
260
+ this.buffer,
261
+ 0,
262
+ length,
263
+ this.position,
264
+ callback,
265
+ );
266
+ this.fsOp.read();
267
+ }
268
+
269
+ expandRight(length: number, callback: FsReadCallback): void {
270
+ this.checkOp();
271
+ const offset = this.buffer.length;
272
+ this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]);
273
+ this.fsOp = new FsRead(
274
+ this.fd,
275
+ this.buffer,
276
+ offset,
277
+ length,
278
+ this.position + offset,
279
+ callback,
280
+ );
281
+ this.fsOp.read();
282
+ }
283
+
284
+ moveRight(length: number, callback: FsReadCallback, shift = 0): void {
285
+ this.checkOp();
286
+ if (shift) {
287
+ this.buffer.copy(this.buffer, 0, shift);
288
+ }
289
+ this.position += shift;
290
+ this.fsOp = new FsRead(
291
+ this.fd,
292
+ this.buffer,
293
+ this.buffer.length - shift,
294
+ shift,
295
+ this.position + this.buffer.length - shift,
296
+ callback,
297
+ );
298
+ this.fsOp.read();
299
+ }
300
+
301
+ private checkOp(): void {
302
+ if (this.fsOp?.waiting) {
303
+ throw new Error('Operation in progress');
304
+ }
305
+ }
306
+ }
307
+
308
+ class EntryDataReaderStream extends stream.Readable {
309
+ private pos = 0;
310
+
311
+ constructor(
312
+ private readonly fd: number,
313
+ private readonly dataOffset: number,
314
+ private readonly length: number,
315
+ private readonly readChunkSize: number,
316
+ ) {
317
+ super({ highWaterMark: readChunkSize });
318
+ }
319
+
320
+ override _read(): void {
321
+ const toRead = Math.min(this.readChunkSize, this.length - this.pos);
322
+ const buffer = Buffer.allocUnsafe(toRead);
323
+ if (buffer.length) {
324
+ fs.read(
325
+ this.fd,
326
+ buffer,
327
+ 0,
328
+ buffer.length,
329
+ this.dataOffset + this.pos,
330
+ (err, bytesRead) => this.onRead(err, bytesRead, buffer),
331
+ );
332
+ } else {
333
+ this.push(null);
334
+ }
335
+ }
336
+
337
+ private onRead(
338
+ err: NodeJS.ErrnoException | null,
339
+ bytesRead: number,
340
+ buffer: Buffer,
341
+ ): void {
342
+ this.pos += bytesRead;
343
+ if (err) {
344
+ this.emit('error', err);
345
+ this.push(null);
346
+ return;
347
+ }
348
+ if (!bytesRead) {
349
+ this.push(null);
350
+ return;
351
+ }
352
+ this.push(
353
+ bytesRead === buffer.length ? buffer : buffer.subarray(0, bytesRead),
354
+ );
355
+ }
356
+ }
357
+
358
+ class CrcVerify {
359
+ private static crcTable: number[] | undefined;
360
+
361
+ private readonly state = { crc: ~0, size: 0 };
362
+
363
+ constructor(
364
+ private readonly expectedCrc: number,
365
+ private readonly expectedSize: number,
366
+ ) {}
367
+
368
+ data(data: Buffer): void {
369
+ const crcTable = CrcVerify.getCrcTable();
370
+ let crc = this.state.crc;
371
+ let off = 0;
372
+ let len = data.length;
373
+ while (--len >= 0) {
374
+ crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8);
375
+ }
376
+ this.state.crc = crc;
377
+ this.state.size += data.length;
378
+ if (this.state.size >= this.expectedSize) {
379
+ const buf = Buffer.alloc(4);
380
+ buf.writeInt32LE(~this.state.crc & 0xffffffff, 0);
381
+ crc = buf.readUInt32LE(0);
382
+ if (crc !== this.expectedCrc) {
383
+ throw new Error('Invalid CRC');
384
+ }
385
+ if (this.state.size !== this.expectedSize) {
386
+ throw new Error('Invalid size');
387
+ }
388
+ }
389
+ }
390
+
391
+ private static getCrcTable(): number[] {
392
+ if (CrcVerify.crcTable) {
393
+ return CrcVerify.crcTable;
394
+ }
395
+ const crcTable: number[] = [];
396
+ const b = Buffer.alloc(4);
397
+ for (let n = 0; n < 256; n++) {
398
+ let c = n;
399
+ for (let k = 7; k >= 0; k--) {
400
+ c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
401
+ }
402
+ if (c < 0) {
403
+ b.writeInt32LE(c, 0);
404
+ c = b.readUInt32LE(0);
405
+ }
406
+ crcTable[n] = c;
407
+ }
408
+ CrcVerify.crcTable = crcTable;
409
+ return crcTable;
410
+ }
411
+ }
412
+
413
+ class EntryVerifyStream extends stream.Transform {
414
+ private readonly verify: CrcVerify;
415
+
416
+ constructor(baseStm: stream.Readable, crc: number, size: number) {
417
+ super();
418
+ this.verify = new CrcVerify(crc, size);
419
+ baseStm.on('error', (e) => this.emit('error', e));
420
+ }
421
+
422
+ override _transform(
423
+ data: Buffer,
424
+ _encoding: BufferEncoding,
425
+ callback: stream.TransformCallback,
426
+ ): void {
427
+ let err: Error | undefined;
428
+ try {
429
+ this.verify.data(data);
430
+ } catch (e) {
431
+ err = e as Error;
432
+ }
433
+ callback(err, data);
434
+ }
435
+ }
436
+
437
+ /** ZIP central-directory entry (also returned from {@link StreamZip.entries}). */
438
+ export class ZipEntry {
439
+ verMade = 0;
440
+ version = 0;
441
+ flags = 0;
442
+ method = 0;
443
+ time = 0;
444
+ crc = 0;
445
+ compressedSize = 0;
446
+ size = 0;
447
+ fnameLen = 0;
448
+ extraLen = 0;
449
+ comLen = 0;
450
+ diskStart = 0;
451
+ inattr = 0;
452
+ attr = 0;
453
+ offset = 0;
454
+ headerOffset = 0;
455
+ name = '';
456
+ isDirectory = false;
457
+ comment: string | null = null;
458
+
459
+ readHeader(data: Buffer, offset: number): void {
460
+ if (
461
+ data.length < offset + ZIP.CENHDR ||
462
+ data.readUInt32LE(offset) !== ZIP.CENSIG
463
+ ) {
464
+ throw new Error('Invalid entry header');
465
+ }
466
+ this.verMade = data.readUInt16LE(offset + ZIP.CENVEM);
467
+ this.version = data.readUInt16LE(offset + ZIP.CENVER);
468
+ this.flags = data.readUInt16LE(offset + ZIP.CENFLG);
469
+ this.method = data.readUInt16LE(offset + ZIP.CENHOW);
470
+ const timebytes = data.readUInt16LE(offset + ZIP.CENTIM);
471
+ const datebytes = data.readUInt16LE(offset + ZIP.CENTIM + 2);
472
+ this.time = parseZipTime(timebytes, datebytes);
473
+ this.crc = data.readUInt32LE(offset + ZIP.CENCRC);
474
+ this.compressedSize = data.readUInt32LE(offset + ZIP.CENSIZ);
475
+ this.size = data.readUInt32LE(offset + ZIP.CENLEN);
476
+ this.fnameLen = data.readUInt16LE(offset + ZIP.CENNAM);
477
+ this.extraLen = data.readUInt16LE(offset + ZIP.CENEXT);
478
+ this.comLen = data.readUInt16LE(offset + ZIP.CENCOM);
479
+ this.diskStart = data.readUInt16LE(offset + ZIP.CENDSK);
480
+ this.inattr = data.readUInt16LE(offset + ZIP.CENATT);
481
+ this.attr = data.readUInt32LE(offset + ZIP.CENATX);
482
+ this.offset = data.readUInt32LE(offset + ZIP.CENOFF);
483
+ }
484
+
485
+ readDataHeader(data: Buffer): void {
486
+ if (data.readUInt32LE(0) !== ZIP.LOCSIG) {
487
+ throw new Error('Invalid local header');
488
+ }
489
+ this.version = data.readUInt16LE(ZIP.LOCVER);
490
+ this.flags = data.readUInt16LE(ZIP.LOCFLG);
491
+ this.method = data.readUInt16LE(ZIP.LOCHOW);
492
+ const timebytes = data.readUInt16LE(ZIP.LOCTIM);
493
+ const datebytes = data.readUInt16LE(ZIP.LOCTIM + 2);
494
+ this.time = parseZipTime(timebytes, datebytes);
495
+ this.crc = data.readUInt32LE(ZIP.LOCCRC) || this.crc;
496
+ const compressedSize = data.readUInt32LE(ZIP.LOCSIZ);
497
+ if (compressedSize && compressedSize !== ZIP.EF_ZIP64_OR_32) {
498
+ this.compressedSize = compressedSize;
499
+ }
500
+ const size = data.readUInt32LE(ZIP.LOCLEN);
501
+ if (size && size !== ZIP.EF_ZIP64_OR_32) {
502
+ this.size = size;
503
+ }
504
+ this.fnameLen = data.readUInt16LE(ZIP.LOCNAM);
505
+ this.extraLen = data.readUInt16LE(ZIP.LOCEXT);
506
+ }
507
+
508
+ read(data: Buffer, offset: number, textDecoder: TextDecoder | null): void {
509
+ const nameData = data.slice(offset, (offset += this.fnameLen));
510
+ this.name = textDecoder
511
+ ? textDecoder.decode(new Uint8Array(nameData))
512
+ : nameData.toString('utf8');
513
+ const lastChar = data[offset - 1];
514
+ this.isDirectory = lastChar === 47 || lastChar === 92;
515
+ if (this.extraLen) {
516
+ this.readExtra(data, offset);
517
+ offset += this.extraLen;
518
+ }
519
+ this.comment = this.comLen
520
+ ? data.slice(offset, offset + this.comLen).toString()
521
+ : null;
522
+ }
523
+
524
+ validateName(): void {
525
+ if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) {
526
+ throw new Error(`Malicious entry: ${this.name}`);
527
+ }
528
+ }
529
+
530
+ readExtra(data: Buffer, offset: number): void {
531
+ const maxPos = offset + this.extraLen;
532
+ while (offset < maxPos) {
533
+ const signature = data.readUInt16LE(offset);
534
+ offset += 2;
535
+ const size = data.readUInt16LE(offset);
536
+ offset += 2;
537
+ if (ZIP.ID_ZIP64 === signature) {
538
+ this.parseZip64Extra(data, offset, size);
539
+ }
540
+ offset += size;
541
+ }
542
+ }
543
+
544
+ parseZip64Extra(data: Buffer, offset: number, length: number): void {
545
+ if (length >= 8 && this.size === ZIP.EF_ZIP64_OR_32) {
546
+ this.size = readUInt64LE(data, offset);
547
+ offset += 8;
548
+ length -= 8;
549
+ }
550
+ if (length >= 8 && this.compressedSize === ZIP.EF_ZIP64_OR_32) {
551
+ this.compressedSize = readUInt64LE(data, offset);
552
+ offset += 8;
553
+ length -= 8;
554
+ }
555
+ if (length >= 8 && this.offset === ZIP.EF_ZIP64_OR_32) {
556
+ this.offset = readUInt64LE(data, offset);
557
+ offset += 8;
558
+ length -= 8;
559
+ }
560
+ if (length >= 4 && this.diskStart === ZIP.EF_ZIP64_OR_16) {
561
+ this.diskStart = data.readUInt32LE(offset);
562
+ }
563
+ }
564
+
565
+ get encrypted(): boolean {
566
+ return (this.flags & ZIP.FLG_ENTRY_ENC) === ZIP.FLG_ENTRY_ENC;
567
+ }
568
+
569
+ get isFile(): boolean {
570
+ return !this.isDirectory;
571
+ }
572
+ }
573
+
574
+ /** Streaming ZIP reader with promise-based entry access. */
575
+ export class StreamZip extends EventEmitter {
576
+ entriesCount = 0;
577
+ comment: string | null = null;
578
+ centralDirectory!: CentralDirectoryHeader;
579
+
580
+ private fd: number | null = null;
581
+ private fileSize = 0;
582
+ private chunkSize = 0;
583
+ private closed = false;
584
+ private signatureOp: SignatureSearchState | null = null;
585
+ private entriesOp: EntriesReadState | null = null;
586
+ private readonly entryMap: Record<string, ZipEntry> | null;
587
+ private readonly textDecoder: TextDecoder | null;
588
+ private readonly readyPromise: Promise<void>;
589
+
590
+ constructor(private readonly config: StreamZipConfig) {
591
+ super();
592
+ this.entryMap = config.storeEntries !== false ? {} : null;
593
+ this.textDecoder = config.nameEncoding
594
+ ? new TextDecoder(config.nameEncoding)
595
+ : null;
596
+ this.readyPromise = new Promise((resolve, reject) => {
597
+ this.once('ready', () => {
598
+ this.removeListener('error', reject);
599
+ resolve();
600
+ });
601
+ this.once('error', reject);
602
+ });
603
+ this.open();
604
+ }
605
+
606
+ /** Resolves when the central directory has been parsed. */
607
+ async waitUntilReady(): Promise<void> {
608
+ await this.readyPromise;
609
+ }
610
+
611
+ async entries(): Promise<Record<string, ZipEntry>> {
612
+ await this.readyPromise;
613
+ const entryMap = this.entryMap;
614
+ if (!entryMap) {
615
+ throw new Error('storeEntries disabled');
616
+ }
617
+ return entryMap;
618
+ }
619
+
620
+ async stream(entry: ZipEntry | string): Promise<stream.Readable> {
621
+ await this.readyPromise;
622
+ const resolvedEntry = await this.resolveFileEntry(entry);
623
+ const openedEntry =
624
+ this.config.skipLocalHeaderRead === true &&
625
+ resolvedEntry.method === ZIP.STORED
626
+ ? resolvedEntry
627
+ : await this.openEntry(resolvedEntry);
628
+ if (openedEntry.encrypted) {
629
+ throw new Error('Entry encrypted');
630
+ }
631
+ const offset = this.dataOffset(openedEntry);
632
+ if (this.fd === null) {
633
+ throw new Error('Archive closed');
634
+ }
635
+ let entryStream: stream.Readable = new EntryDataReaderStream(
636
+ this.fd,
637
+ offset,
638
+ openedEntry.compressedSize,
639
+ this.chunkSize || 1024 * 1024,
640
+ );
641
+ if (openedEntry.method === ZIP.STORED) {
642
+ // stored — pass through
643
+ } else if (openedEntry.method === ZIP.DEFLATED) {
644
+ entryStream = entryStream.pipe(zlib.createInflateRaw());
645
+ } else {
646
+ throw new Error(`Unknown compression method: ${openedEntry.method}`);
647
+ }
648
+ if (
649
+ this.config.verifyEntryCrc !== false &&
650
+ this.canVerifyCrc(openedEntry)
651
+ ) {
652
+ entryStream = entryStream.pipe(
653
+ new EntryVerifyStream(entryStream, openedEntry.crc, openedEntry.size),
654
+ );
655
+ }
656
+ return entryStream;
657
+ }
658
+
659
+ async openEntry(entry: ZipEntry | string): Promise<ZipEntry> {
660
+ await this.readyPromise;
661
+ let resolvedEntry: ZipEntry | undefined;
662
+ if (typeof entry === 'string') {
663
+ const entryMap = this.entryMap;
664
+ if (!entryMap) {
665
+ throw new Error('storeEntries disabled');
666
+ }
667
+ resolvedEntry = entryMap[entry];
668
+ if (!resolvedEntry) {
669
+ throw new Error('Entry not found');
670
+ }
671
+ } else {
672
+ resolvedEntry = entry;
673
+ }
674
+ if (!resolvedEntry.isFile) {
675
+ throw new Error('Entry is not file');
676
+ }
677
+ const fd = this.fd;
678
+ if (fd === null) {
679
+ throw new Error('Archive closed');
680
+ }
681
+ const entryOffset = resolvedEntry.offset;
682
+ const buffer = Buffer.alloc(ZIP.LOCHDR);
683
+ await new Promise<void>((resolve, reject) => {
684
+ new FsRead(fd, buffer, 0, buffer.length, entryOffset, (err) => {
685
+ if (err) {
686
+ reject(err);
687
+ return;
688
+ }
689
+ resolve();
690
+ }).read();
691
+ });
692
+ resolvedEntry.readDataHeader(buffer);
693
+ if (resolvedEntry.encrypted) {
694
+ throw new Error('Entry encrypted');
695
+ }
696
+ return resolvedEntry;
697
+ }
698
+
699
+ async close(): Promise<void> {
700
+ if (this.closed || this.fd === null) {
701
+ this.closed = true;
702
+ return;
703
+ }
704
+ this.closed = true;
705
+ const fd = this.fd;
706
+ this.fd = null;
707
+ await new Promise<void>((resolve, reject) => {
708
+ fs.close(fd, (err) => (err ? reject(err) : resolve()));
709
+ });
710
+ }
711
+
712
+ override emit(event: string | symbol, ...args: unknown[]): boolean {
713
+ if (this.closed) {
714
+ return false;
715
+ }
716
+ return super.emit(event, ...args);
717
+ }
718
+
719
+ private async resolveFileEntry(entry: ZipEntry | string): Promise<ZipEntry> {
720
+ if (typeof entry === 'string') {
721
+ const entryMap = this.entryMap;
722
+ if (!entryMap) {
723
+ throw new Error('storeEntries disabled');
724
+ }
725
+ const resolvedEntry = entryMap[entry];
726
+ if (!resolvedEntry) {
727
+ throw new Error('Entry not found');
728
+ }
729
+ if (!resolvedEntry.isFile) {
730
+ throw new Error('Entry is not file');
731
+ }
732
+ return resolvedEntry;
733
+ }
734
+ if (!entry.isFile) {
735
+ throw new Error('Entry is not file');
736
+ }
737
+ return entry;
738
+ }
739
+
740
+ private open(): void {
741
+ if (this.config.fd !== undefined) {
742
+ this.fd = this.config.fd;
743
+ this.readFile();
744
+ return;
745
+ }
746
+ const filePath = this.config.file;
747
+ if (!filePath) {
748
+ this.emit('error', new Error('ZIP file path is required'));
749
+ return;
750
+ }
751
+ fs.open(filePath, 'r', (err, f) => {
752
+ if (err) {
753
+ this.emit('error', err);
754
+ return;
755
+ }
756
+ this.fd = f;
757
+ this.readFile();
758
+ });
759
+ }
760
+
761
+ private readFile(): void {
762
+ if (this.fd === null) {
763
+ return;
764
+ }
765
+ fs.fstat(this.fd, (err, stat) => {
766
+ if (err) {
767
+ this.emit('error', err);
768
+ return;
769
+ }
770
+ this.fileSize = stat.size;
771
+ let chunk = this.config.chunkSize ?? Math.round(this.fileSize / 1000);
772
+ chunk = Math.max(
773
+ Math.min(chunk, Math.min(128 * 1024, this.fileSize)),
774
+ Math.min(1024, this.fileSize),
775
+ );
776
+ this.chunkSize = chunk;
777
+ this.readCentralDirectory();
778
+ });
779
+ }
780
+
781
+ private readUntilFoundCallback: FsReadCallback = (err, bytesRead) => {
782
+ const op = this.signatureOp;
783
+ if (!op) {
784
+ return;
785
+ }
786
+ if (err || !bytesRead) {
787
+ this.emit('error', err ?? new Error('Archive read error'));
788
+ return;
789
+ }
790
+ let pos = op.lastPos;
791
+ let bufferPosition = pos - op.win.position;
792
+ const buffer = op.win.buffer;
793
+ const minPos = op.minPos;
794
+ while (--pos >= minPos && --bufferPosition >= 0) {
795
+ if (
796
+ buffer.length - bufferPosition >= 4 &&
797
+ buffer[bufferPosition] === op.firstByte
798
+ ) {
799
+ if (buffer.readUInt32LE(bufferPosition) === op.sig) {
800
+ op.lastBufferPosition = bufferPosition;
801
+ op.lastBytesRead = bytesRead;
802
+ op.complete();
803
+ return;
804
+ }
805
+ }
806
+ }
807
+ if (pos === minPos) {
808
+ this.emit('error', new Error('Bad archive'));
809
+ return;
810
+ }
811
+ op.lastPos = pos + 1;
812
+ op.chunkSize *= 2;
813
+ if (pos <= minPos) {
814
+ this.emit('error', new Error('Bad archive'));
815
+ return;
816
+ }
817
+ const expandLength = Math.min(op.chunkSize, pos - minPos);
818
+ op.win.expandLeft(expandLength, this.readUntilFoundCallback);
819
+ };
820
+
821
+ private readCentralDirectory(): void {
822
+ if (this.fd === null) {
823
+ return;
824
+ }
825
+ const totalReadLength = Math.min(
826
+ ZIP.ENDHDR + ZIP.MAXFILECOMMENT,
827
+ this.fileSize,
828
+ );
829
+ this.signatureOp = {
830
+ win: new FileWindowBuffer(this.fd),
831
+ totalReadLength,
832
+ minPos: this.fileSize - totalReadLength,
833
+ lastPos: this.fileSize,
834
+ chunkSize: Math.min(1024, this.chunkSize),
835
+ firstByte: ZIP.ENDSIGFIRST,
836
+ sig: ZIP.ENDSIG,
837
+ lastBufferPosition: 0,
838
+ lastBytesRead: 0,
839
+ complete: () => this.readCentralDirectoryComplete(),
840
+ };
841
+ const op = this.signatureOp;
842
+ op.win.read(
843
+ this.fileSize - op.chunkSize,
844
+ op.chunkSize,
845
+ this.readUntilFoundCallback,
846
+ );
847
+ }
848
+
849
+ private readCentralDirectoryComplete(): void {
850
+ const op = this.signatureOp;
851
+ if (!op) {
852
+ return;
853
+ }
854
+ const buffer = op.win.buffer;
855
+ const pos = op.lastBufferPosition;
856
+ try {
857
+ const centralDirectory = new CentralDirectoryHeader();
858
+ centralDirectory.read(buffer.subarray(pos, pos + ZIP.ENDHDR));
859
+ centralDirectory.headerOffset = op.win.position + pos;
860
+ if (centralDirectory.commentLength) {
861
+ this.comment = buffer
862
+ .subarray(
863
+ pos + ZIP.ENDHDR,
864
+ pos + ZIP.ENDHDR + centralDirectory.commentLength,
865
+ )
866
+ .toString();
867
+ } else {
868
+ this.comment = null;
869
+ }
870
+ this.entriesCount = centralDirectory.volumeEntries;
871
+ this.centralDirectory = centralDirectory;
872
+ if (
873
+ (centralDirectory.volumeEntries === ZIP.EF_ZIP64_OR_16 &&
874
+ centralDirectory.totalEntries === ZIP.EF_ZIP64_OR_16) ||
875
+ centralDirectory.size === ZIP.EF_ZIP64_OR_32 ||
876
+ centralDirectory.offset === ZIP.EF_ZIP64_OR_32
877
+ ) {
878
+ this.readZip64CentralDirectoryLocator();
879
+ } else {
880
+ this.signatureOp = null;
881
+ this.readEntries();
882
+ }
883
+ } catch (err) {
884
+ this.emit('error', err);
885
+ }
886
+ }
887
+
888
+ private readZip64CentralDirectoryLocator(): void {
889
+ const op = this.signatureOp;
890
+ if (!op) {
891
+ return;
892
+ }
893
+ const length = ZIP.ENDL64HDR;
894
+ if (op.lastBufferPosition > length) {
895
+ op.lastBufferPosition -= length;
896
+ this.readZip64CentralDirectoryLocatorComplete();
897
+ return;
898
+ }
899
+ this.signatureOp = {
900
+ win: op.win,
901
+ totalReadLength: length,
902
+ minPos: op.win.position - length,
903
+ lastPos: op.win.position,
904
+ chunkSize: op.chunkSize,
905
+ firstByte: ZIP.ENDL64SIGFIRST,
906
+ sig: ZIP.ENDL64SIG,
907
+ lastBufferPosition: op.lastBufferPosition,
908
+ lastBytesRead: op.lastBytesRead,
909
+ complete: () => this.readZip64CentralDirectoryLocatorComplete(),
910
+ };
911
+ const next = this.signatureOp;
912
+ next.win.read(
913
+ next.lastPos - next.chunkSize,
914
+ next.chunkSize,
915
+ this.readUntilFoundCallback,
916
+ );
917
+ }
918
+
919
+ private readZip64CentralDirectoryLocatorComplete(): void {
920
+ const op = this.signatureOp;
921
+ if (!op) {
922
+ return;
923
+ }
924
+ const buffer = op.win.buffer;
925
+ const locHeader = new CentralDirectoryLoc64Header();
926
+ locHeader.read(
927
+ buffer.subarray(
928
+ op.lastBufferPosition,
929
+ op.lastBufferPosition + ZIP.ENDL64HDR,
930
+ ),
931
+ );
932
+ const readLength = this.fileSize - locHeader.headerOffset;
933
+ this.signatureOp = {
934
+ win: op.win,
935
+ totalReadLength: readLength,
936
+ minPos: locHeader.headerOffset,
937
+ lastPos: op.lastPos,
938
+ chunkSize: op.chunkSize,
939
+ firstByte: ZIP.END64SIGFIRST,
940
+ sig: ZIP.END64SIG,
941
+ lastBufferPosition: op.lastBufferPosition,
942
+ lastBytesRead: op.lastBytesRead,
943
+ complete: () => this.readZip64CentralDirectoryComplete(),
944
+ };
945
+ const next = this.signatureOp;
946
+ next.win.read(
947
+ this.fileSize - next.chunkSize,
948
+ next.chunkSize,
949
+ this.readUntilFoundCallback,
950
+ );
951
+ }
952
+
953
+ private readZip64CentralDirectoryComplete(): void {
954
+ const op = this.signatureOp;
955
+ if (!op) {
956
+ return;
957
+ }
958
+ const buffer = op.win.buffer;
959
+ const zip64cd = new CentralDirectoryZip64Header();
960
+ zip64cd.read(
961
+ buffer.subarray(
962
+ op.lastBufferPosition,
963
+ op.lastBufferPosition + ZIP.END64HDR,
964
+ ),
965
+ );
966
+ this.centralDirectory.volumeEntries = zip64cd.volumeEntries;
967
+ this.centralDirectory.totalEntries = zip64cd.totalEntries;
968
+ this.centralDirectory.size = zip64cd.size;
969
+ this.centralDirectory.offset = zip64cd.offset;
970
+ this.entriesCount = zip64cd.volumeEntries;
971
+ this.signatureOp = null;
972
+ this.readEntries();
973
+ }
974
+
975
+ private readEntries(): void {
976
+ if (this.fd === null) {
977
+ return;
978
+ }
979
+ this.entriesOp = {
980
+ win: new FileWindowBuffer(this.fd),
981
+ pos: this.centralDirectory.offset,
982
+ chunkSize: this.chunkSize,
983
+ entriesLeft: this.centralDirectory.volumeEntries,
984
+ entry: null,
985
+ };
986
+ const op = this.entriesOp;
987
+ op.win.read(
988
+ op.pos,
989
+ Math.min(this.chunkSize, this.fileSize - op.pos),
990
+ this.readEntriesCallback,
991
+ );
992
+ }
993
+
994
+ private readEntriesCallback: FsReadCallback = (err, bytesRead) => {
995
+ const op = this.entriesOp;
996
+ if (!op) {
997
+ return;
998
+ }
999
+ if (err || !bytesRead) {
1000
+ this.emit('error', err ?? new Error('Entries read error'));
1001
+ return;
1002
+ }
1003
+ let bufferPos = op.pos - op.win.position;
1004
+ let entry = op.entry;
1005
+ const buffer = op.win.buffer;
1006
+ const bufferLength = buffer.length;
1007
+ try {
1008
+ while (op.entriesLeft > 0) {
1009
+ if (!entry) {
1010
+ entry = new ZipEntry();
1011
+ entry.readHeader(buffer, bufferPos);
1012
+ entry.headerOffset = op.win.position + bufferPos;
1013
+ op.entry = entry;
1014
+ op.pos += ZIP.CENHDR;
1015
+ bufferPos += ZIP.CENHDR;
1016
+ }
1017
+ const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen;
1018
+ const advanceBytes =
1019
+ entryHeaderSize + (op.entriesLeft > 1 ? ZIP.CENHDR : 0);
1020
+ if (bufferLength - bufferPos < advanceBytes) {
1021
+ op.win.moveRight(this.chunkSize, this.readEntriesCallback, bufferPos);
1022
+ op.move = true;
1023
+ return;
1024
+ }
1025
+ entry.read(buffer, bufferPos, this.textDecoder);
1026
+ if (!this.config.skipEntryNameValidation) {
1027
+ entry.validateName();
1028
+ }
1029
+ if (this.entryMap) {
1030
+ this.entryMap[entry.name] = entry;
1031
+ }
1032
+ this.emit('entry', entry);
1033
+ op.entry = null;
1034
+ entry = null;
1035
+ op.entriesLeft--;
1036
+ op.pos += entryHeaderSize;
1037
+ bufferPos += entryHeaderSize;
1038
+ }
1039
+ this.emit('ready');
1040
+ } catch (readErr) {
1041
+ this.emit('error', readErr);
1042
+ }
1043
+ };
1044
+
1045
+ private dataOffset(entry: ZipEntry): number {
1046
+ return entry.offset + ZIP.LOCHDR + entry.fnameLen + entry.extraLen;
1047
+ }
1048
+
1049
+ private canVerifyCrc(entry: ZipEntry): boolean {
1050
+ return (entry.flags & 0x8) !== 0x8;
1051
+ }
1052
+ }
1053
+
1054
+ function parseZipTime(timebytes: number, datebytes: number): number {
1055
+ const timebits = toBits(timebytes, 16);
1056
+ const datebits = toBits(datebytes, 16);
1057
+ const mt = {
1058
+ h: Number.parseInt(timebits.slice(0, 5).join(''), 2),
1059
+ m: Number.parseInt(timebits.slice(5, 11).join(''), 2),
1060
+ s: Number.parseInt(timebits.slice(11, 16).join(''), 2) * 2,
1061
+ Y: Number.parseInt(datebits.slice(0, 7).join(''), 2) + 1980,
1062
+ M: Number.parseInt(datebits.slice(7, 11).join(''), 2),
1063
+ D: Number.parseInt(datebits.slice(11, 16).join(''), 2),
1064
+ };
1065
+ const dtStr = `${[mt.Y, mt.M, mt.D].join('-')} ${[mt.h, mt.m, mt.s].join(':')} GMT+0`;
1066
+ return new Date(dtStr).getTime();
1067
+ }
1068
+
1069
+ function toBits(dec: number, size: number): string[] {
1070
+ let b = (dec >>> 0).toString(2);
1071
+ while (b.length < size) {
1072
+ b = `0${b}`;
1073
+ }
1074
+ return b.split('');
1075
+ }
1076
+
1077
+ function readUInt64LE(buffer: Buffer, offset: number): number {
1078
+ return (
1079
+ buffer.readUInt32LE(offset + 4) * 0x0000000100000000 +
1080
+ buffer.readUInt32LE(offset)
1081
+ );
1082
+ }