Arjun Barrett преди 4 години
родител
ревизия
de70e83f06
променени са 12 файла, в които са добавени 221 реда и са изтрити 159 реда
  1. 5 1
      CHANGELOG.md
  2. 16 15
      README.md
  3. 1 5
      docs/README.md
  4. 8 1
      docs/classes/asyncunzipinflate.md
  5. 9 1
      docs/interfaces/unzipdecoderconstructor.md
  6. 39 0
      docs/interfaces/unzipfile.md
  7. 6 5
      docs/interfaces/zipinputfile.md
  8. 1 1
      package.json
  9. 109 72
      src/index.ts
  10. 0 45
      test/4-multiperf.ts
  11. 0 0
      test/4-zip.ts
  12. 27 13
      test/util.ts

+ 5 - 1
CHANGELOG.md

@@ -1,5 +1,9 @@
+## 0.6.0
+- Revamped streaming unzip for compatibility and performance improvements
+## 0.5.2
+- General bugfixes
 ## 0.5.0
-- Add streaming ZIP, UNZIP
+- Add streaming zip, unzip
 - Fix import issues with certain environments
   - If you had problems with `worker_threads` being included in your bundle, try updating!
 ## 0.4.8

+ 16 - 15
README.md

@@ -76,8 +76,6 @@ import * as fflate from 'fflate/esm/browser.js';
 import * as fflate from 'fflate/esm/index.mjs';
 ```
 
-If you see `require('worker_threads')` in any code bundled for the browser, your bundler probably didn't resolve the `browser` field of `package.json`. You can enable it (e.g. [for Rollup](https://github.com/rollup/plugins/tree/master/packages/node-resolve#browser)) or you can manually import the ESM version at `fflate/esm/browser.js`. 
-
 And use:
 ```js
 // This is an ArrayBuffer of data
@@ -314,21 +312,24 @@ unzipper.register(fflate.UnzipInflate);
 const neededFiles = ['file1.txt', 'example.json'];
 
 // Can specify handler in constructor too
-unzipper.onfile = (err, filename, file) => {
-  if (err) {
-    // The filename will usually exist here too
-    console.log(`Error with file ${filename}: ${err}`);
-    return;
-  }
-  // filename is a string, file is a stream
-  if (neededFiles.includes(filename)) {
+unzipper.onfile = file => {
+  // file.name is a string, file is a stream
+  if (neededFiles.includes(file.name)) {
     file.ondata = (err, dat, final) => {
       // Stream output here
       console.log(dat, final);
     };
     
+    console.log('Reading:', file.name);
+
+    // File sizes are sometimes not set if the ZIP file did not encode
+    // them, so you may want to check that file.size != undefined
+    console.log('Compressed size', file.size);
+    console.log('Decompressed size', file.originalSize);
+
     // You should only start the stream if you plan to use it to improve
     // performance. Only after starting the stream will ondata be called.
+    // This method will throw if the compression method hasn't been registered
     file.start();
   }
 };
@@ -447,14 +448,14 @@ zip.end();
 
 // Streaming Unzip should register the asynchronous inflation algorithm
 // for parallel processing.
-const unzip = new Unzip((err, fn, stream) => {
-  if (fn.endsWith('.json')) {
+const unzip = new Unzip(stream => {
+  if (stream.name.endsWith('.json')) {
     stream.ondata = (err, chunk, final) => { ... };
     stream.start();
 
     if (needToCancel) {
-      // To cancel these streams, call file.terminate()
-      file.terminate();
+      // To cancel these streams, call .terminate()
+      stream.terminate();
     }
   }
 });
@@ -492,7 +493,7 @@ So what makes `fflate` different? It takes the brilliant innovations of `UZIP.js
 
 If you're willing to have 160 kB of extra weight and [much less browser support](https://caniuse.com/wasm), you could theoretically achieve more performance than `fflate` with a WASM build of Zlib like [`wasm-flate`](https://www.npmjs.com/package/wasm-flate). However, per some tests I conducted, the WASM interpreters of major browsers are not fast enough as of December 2020 for `wasm-flate` to be useful: `fflate` is around 2x faster.
 
-Before you decide that `fflate` is the end-all compression library, you should note that JavaScript simply cannot rival the performance of a native program. If you're only using Node.js, use the [native Zlib bindings](https://nodejs.org/api/zlib.html) that offer the best performance. Though note that even against Zlib, `fflate` is only around 30% slower in decompression and 10% slower in compression, and can still achieve better compression ratios!
+Before you decide that `fflate` is the end-all compression library, you should note that JavaScript simply cannot rival the performance of a native program. If you're only using Node.js, it's probably better to use the [native Zlib bindings](https://nodejs.org/api/zlib.html), which tend to offer the best performance. Though note that even against Zlib, `fflate` is only around 30% slower in decompression and 10% slower in compression, and can still achieve better compression ratios!
 
 ## Browser support
 `fflate` makes heavy use of typed arrays (`Uint8Array`, `Uint16Array`, etc.). Typed arrays can be polyfilled at the cost of performance, but the most recent browser that doesn't support them [is from 2011](https://caniuse.com/typedarrays), so I wouldn't bother.

+ 1 - 5
docs/README.md

@@ -161,14 +161,10 @@ ___
 
 ### UnzipFileHandler
 
-Ƭ  **UnzipFileHandler**: (err: Error \| string,name: string,file: [UnzipFile](interfaces/unzipfile.md)) => void
+Ƭ  **UnzipFileHandler**: (file: [UnzipFile](interfaces/unzipfile.md)) => void
 
 Handler for streaming ZIP decompression
 
-**`param`** Any errors that have occurred
-
-**`param`** The name of the file being processed
-
 **`param`** The file that was found in the archive
 
 ___

+ 8 - 1
docs/classes/asyncunzipinflate.md

@@ -30,10 +30,17 @@ Asynchronous streaming DEFLATE decompression for ZIP archives
 
 ### constructor
 
-\+ **new AsyncUnzipInflate**(): [AsyncUnzipInflate](asyncunzipinflate.md)
+\+ **new AsyncUnzipInflate**(`_`: string, `sz`: number): [AsyncUnzipInflate](asyncunzipinflate.md)
 
 Creates a DEFLATE decompression that can be used in ZIP archives
 
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`_` | string |
+`sz` | number |
+
 **Returns:** [AsyncUnzipInflate](asyncunzipinflate.md)
 
 ## Properties

+ 9 - 1
docs/interfaces/unzipdecoderconstructor.md

@@ -20,10 +20,18 @@ A constructor for a decoder for unzip streams
 
 ### constructor
 
-\+ **new UnzipDecoderConstructor**(): [UnzipDecoder](unzipdecoder.md)
+\+ **new UnzipDecoderConstructor**(`filename`: string, `size?`: number, `originalSize?`: number): [UnzipDecoder](unzipdecoder.md)
 
 Creates an instance of the decoder
 
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`filename` | string | The name of the file |
+`size?` | number | The compressed size of the file |
+`originalSize?` | number | The original size of the file  |
+
 **Returns:** [UnzipDecoder](unzipdecoder.md)
 
 ## Properties

+ 39 - 0
docs/interfaces/unzipfile.md

@@ -10,7 +10,11 @@ Streaming file extraction from ZIP archives
 
 ### Properties
 
+* [compression](unzipfile.md#compression)
+* [name](unzipfile.md#name)
 * [ondata](unzipfile.md#ondata)
+* [originalSize](unzipfile.md#originalsize)
+* [size](unzipfile.md#size)
 * [terminate](unzipfile.md#terminate)
 
 ### Methods
@@ -19,6 +23,25 @@ Streaming file extraction from ZIP archives
 
 ## Properties
 
+### compression
+
+•  **compression**: number
+
+The compression format for the data stream. This number is determined by
+the spec in PKZIP's APPNOTE.txt, section 4.4.5. For example, 0 = no
+compression, 8 = deflate, 14 = LZMA. If start() is called but there is no
+decompression stream available for this method, start() will throw.
+
+___
+
+### name
+
+•  **name**: string
+
+The name of the file
+
+___
+
 ### ondata
 
 •  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
@@ -27,6 +50,22 @@ The handler to call whenever data is available
 
 ___
 
+### originalSize
+
+• `Optional` **originalSize**: number
+
+The original size of the file
+
+___
+
+### size
+
+• `Optional` **size**: number
+
+The compressed size of the file
+
+___
+
 ### terminate
 
 •  **terminate**: [AsyncTerminable](asyncterminable.md)

+ 6 - 5
docs/interfaces/zipinputfile.md

@@ -77,7 +77,7 @@ stream completes.
 
 If you don't want to have to generate this yourself, consider extending the
 ZipPassThrough class and overriding its process() method, or using one of
-ZipDeflate or AsyncZipDeflate
+ZipDeflate or AsyncZipDeflate.
 
 ___
 
@@ -93,10 +93,11 @@ ___
 
 ### flag
 
-• `Optional` **flag**: 0 \| 1 \| 2 \| 3
+• `Optional` **flag**: number
 
 Bits 1 and 2 of the general purpose bit flag, specified in PKZIP's
-APPNOTE.txt, section 4.4.4. This is unlikely to be necessary.
+APPNOTE.txt, section 4.4.4. Should be between 0 and 3. This is unlikely
+to be necessary.
 
 ___
 
@@ -150,7 +151,7 @@ stream completes.
 
 If you don't want to have to compute this yourself, consider extending the
 ZipPassThrough class and overriding its process() method, or using one of
-ZipDeflate or AsyncZipDeflate
+ZipDeflate or AsyncZipDeflate.
 
 ___
 
@@ -160,5 +161,5 @@ ___
 
 A method called when the stream is no longer needed, for clean-up
 purposes. This will not always be called after the stream completes,
-so, you may wish to call this.terminate() after the final chunk is
+so you may wish to call this.terminate() after the final chunk is
 processed if you have clean-up logic.

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "fflate",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "description": "High performance (de)compression in an 8kB package",
   "main": "./lib/index.js",
   "module": "./esm/browser.js",

+ 109 - 72
src/index.ts

@@ -3,9 +3,9 @@
 // You may also wish to take a look at the guide I made about this program:
 // https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
 
-// Much of the following code is similar to that of UZIP.js:
+// Some of the following code is similar to that of UZIP.js:
 // https://github.com/photopea/UZIP.js
-// Many optimizations have been made, so the bundle size is ultimately smaller but performance is similar.
+// However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
 
 // Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
 // is better for memory in most engines (I *think*).
@@ -126,18 +126,18 @@ const max = (a: Uint8Array | number[]) => {
 
 // read d, starting at bit p and mask with m
 const bits = (d: Uint8Array, p: number, m: number) => {
-  const o = (p / 8) >> 0;
+  const o = (p / 8) | 0;
   return ((d[o] | (d[o + 1] << 8)) >>> (p & 7)) & m;
 }
 
 // read d, starting at bit p continuing for at least 16 bits
 const bits16 = (d: Uint8Array, p: number) => {
-  const o = (p / 8) >> 0;
+  const o = (p / 8) | 0;
   return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >>> (p & 7));
 }
 
 // get end of byte
-const shft = (p: number) => ((p / 8) >> 0) + (p & 7 && 1);
+const shft = (p: number) => ((p / 8) | 0) + (p & 7 && 1);
 
 // typed array slice - allows garbage collector to free original reference,
 // while being more compatible than .slice
@@ -322,7 +322,7 @@ const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
 // starting at p, write the minimum number of bits that can hold v to d
 const wbits = (d: Uint8Array, p: number, v: number) => {
   v <<= p & 7;
-  const o = (p / 8) >> 0;
+  const o = (p / 8) | 0;
   d[o] |= v;
   d[o + 1] |= v >>> 8;
 }
@@ -330,7 +330,7 @@ const wbits = (d: Uint8Array, p: number, v: number) => {
 // starting at p, write the minimum number of bits (>8) that can hold v to d
 const wbits16 = (d: Uint8Array, p: number, v: number) => {
   v <<= p & 7;
-  const o = (p / 8) >> 0;
+  const o = (p / 8) | 0;
   d[o] |= v;
   d[o + 1] |= v >>> 8;
   d[o + 2] |= v >>> 16;
@@ -356,7 +356,7 @@ const hTree = (d: Uint16Array, mb: number) => {
   }
   const s = t.length;
   const t2 = t.slice();
-  if (!s) return [new u8(0), 0] as const;
+  if (!s) return [et, 0] as const;
   if (s == 1) {
     const v = new u8(t[0].s + 1);
     v[t[0].s] = 1;
@@ -1198,7 +1198,7 @@ export class Inflate {
     const dt = inflt(this.p, this.o, this.s);
     this.ondata(slc(dt, bts, this.s.b), this.d);
     this.o = slc(dt, this.s.b - 32768), this.s.b = this.o.length;
-    this.p = slc(this.p, (this.s.p / 8) >> 0), this.s.p &= 7;
+    this.p = slc(this.p, (this.s.p / 8) | 0), this.s.p &= 7;
   }
 
   /**
@@ -1989,11 +1989,9 @@ export type UnzipCallback = (err: Error | string, data: Unzipped) => void;
 
 /**
  * Handler for streaming ZIP decompression
- * @param err Any errors that have occurred
- * @param name The name of the file being processed
  * @param file The file that was found in the archive
  */
-export type UnzipFileHandler = (err: Error | string, name: string, file: UnzipFile) => void;
+export type UnzipFileHandler = (file: UnzipFile) => void;
 
 // flattened Zippable
 type FlatZippable<A extends boolean> = Record<string, [Uint8Array, (A extends true ? AsyncZipOptions : ZipOptions)]>;
@@ -2024,7 +2022,7 @@ const dutf8 = (d: Uint8Array) => {
   for (let r = '', i = 0;;) {
     let c = d[i++];
     const eb = ((c > 127) as unknown as number) + ((c > 223) as unknown as number) + ((c > 239) as unknown as number);
-    if (i + eb > d.length) return [r, d.slice(i - 1)] as const;
+    if (i + eb > d.length) return [r, slc(d, i - 1)] as const;
     if (!eb) r += String.fromCharCode(c)
     else if (eb == 3) {
       c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | (d[i++] & 63)) - 65536,
@@ -2206,16 +2204,6 @@ const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c?: n
   return b + fl;
 }
 
-// create zip data descriptor
-const czdd = (f: Pick<ZipInputFile, 'size' | 'crc'>, c: number) => {
-  const d = new u8(16);
-  wbytes(d, 0, 0x8074B50)
-  wbytes(d, 4, f.crc);
-  wbytes(d, 8, c);
-  wbytes(d, 12, f.size);
-  return d;
-}
-
 // write zip footer (end of central directory)
 const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => {
   wbytes(o, b, 0x6054B50); // skip disk
@@ -2243,7 +2231,7 @@ export interface ZipInputFile extends ZipAttributes {
    * 
    * If you don't want to have to compute this yourself, consider extending the
    * ZipPassThrough class and overriding its process() method, or using one of
-   * ZipDeflate or AsyncZipDeflate
+   * ZipDeflate or AsyncZipDeflate.
    */
   size: number;
 
@@ -2254,7 +2242,7 @@ export interface ZipInputFile extends ZipAttributes {
    * 
    * If you don't want to have to generate this yourself, consider extending the
    * ZipPassThrough class and overriding its process() method, or using one of
-   * ZipDeflate or AsyncZipDeflate
+   * ZipDeflate or AsyncZipDeflate.
    */
   crc: number;
 
@@ -2267,9 +2255,10 @@ export interface ZipInputFile extends ZipAttributes {
 
   /**
    * Bits 1 and 2 of the general purpose bit flag, specified in PKZIP's
-   * APPNOTE.txt, section 4.4.4. This is unlikely to be necessary.
+   * APPNOTE.txt, section 4.4.4. Should be between 0 and 3. This is unlikely
+   * to be necessary.
    */
-  flag?: 0 | 1 | 2 | 3;
+  flag?: number;
 
   /**
    * The handler to be called when data is added. After passing this stream to
@@ -2289,7 +2278,7 @@ export interface ZipInputFile extends ZipAttributes {
   /**
    * A method called when the stream is no longer needed, for clean-up
    * purposes. This will not always be called after the stream completes,
-   * so, you may wish to call this.terminate() after the final chunk is
+   * so you may wish to call this.terminate() after the final chunk is
    * processed if you have clean-up logic.
    */
   terminate?: AsyncTerminable;
@@ -2353,7 +2342,7 @@ export class ZipPassThrough implements ZipInputFile {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final?: boolean) {
-    if (!(this as ZipInputFile).ondata) throw 'no callback - add to ZIP archive before pushing';
+    if (!this.ondata) throw 'no callback - add to ZIP archive before pushing';
     this.c.p(chunk);
     this.size += chunk.length;
     if (final) this.crc = this.c.d();
@@ -2535,7 +2524,12 @@ export class Zip {
         cl += dat.length;
         chks.push(dat);
         if (final) {
-          chks.push(czdd(file, cl));
+          const dd = new u8(16);
+          wbytes(dd, 0, 0x8074B50)
+          wbytes(dd, 4, file.crc);
+          wbytes(dd, 8, cl);
+          wbytes(dd, 12, file.size);
+          chks.push(dd);
           uf.c = cl, uf.b = hl + cl + 16, uf.crc = file.crc, uf.size = file.size;
           if (tr) uf.r();
           tr = 1;
@@ -2754,8 +2748,11 @@ export interface UnzipDecoder {
 export interface UnzipDecoderConstructor {
   /**
    * Creates an instance of the decoder
+   * @param filename The name of the file
+   * @param size The compressed size of the file
+   * @param originalSize The original size of the file
    */
-  new(): UnzipDecoder;
+  new(filename: string, size?: number, originalSize?: number): UnzipDecoder;
 
   /**
    * The compression format for the data stream. This number is determined by
@@ -2774,6 +2771,29 @@ export interface UnzipFile {
    */
   ondata: AsyncFlateStreamHandler;
 
+  /**
+   * The name of the file
+   */
+  name: string;
+
+  /**
+   * The compression format for the data stream. This number is determined by
+   * the spec in PKZIP's APPNOTE.txt, section 4.4.5. For example, 0 = no
+   * compression, 8 = deflate, 14 = LZMA. If start() is called but there is no
+   * decompression stream available for this method, start() will throw.
+   */
+  compression: number;
+
+  /**
+   * The compressed size of the file
+   */
+  size?: number;
+
+  /**
+   * The original size of the file
+   */
+  originalSize?: number;
+
   /**
    * Starts reading from the stream. Calling this function will always enable
    * this stream, but ocassionally the stream will be enabled even without
@@ -2831,22 +2851,29 @@ export class UnzipInflate implements UnzipDecoder {
  */
 export class AsyncUnzipInflate implements UnzipDecoder {
   static compression = 8;
-  private i: AsyncInflate;
+  private i: AsyncInflate | Inflate;
   ondata: AsyncFlateStreamHandler;
   terminate: AsyncTerminable;
 
   /**
    * Creates a DEFLATE decompression that can be used in ZIP archives
    */
-  constructor() {
-    this.i = new AsyncInflate((err, dat, final) => {
-      this.ondata(err, dat, final);
-    });
-    this.terminate = this.i.terminate;
+  constructor(_: string, sz: number) {
+    if (sz < 320000) {
+      this.i = new Inflate((dat, final) => {
+        this.ondata(null, dat, final);
+      });
+    } else {
+      this.i = new AsyncInflate((err, dat, final) => {
+        this.ondata(err, dat, final);
+      });
+      this.terminate = this.i.terminate;
+    }
   }
 
   push(data: Uint8Array, final: boolean) {
-    this.i.push(slc(data, 0), final);
+    if ((this.i as AsyncInflate).terminate) data = slc(data, 0);
+    this.i.push(data, final);
   }
 }
 
@@ -2857,7 +2884,7 @@ export class Unzip {
   private d: UnzipDecoder;
   private c: number;
   private p: Uint8Array;
-  private k: Array<[Uint8Array, boolean]>[];
+  private k: Uint8Array[][];
   private o: Record<number, UnzipDecoderConstructor>;
 
   /**
@@ -2879,75 +2906,85 @@ export class Unzip {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final: boolean) {
-    const add = this.c == -1 && this.d;
-    if (this.c && !add) {
+    if (!this.onfile) throw 'no callback';
+    if (this.c > 0) {
       const len = Math.min(this.c, chunk.length);
       const toAdd = chunk.subarray(0, len);
       this.c -= len;
       if (this.d) this.d.push(toAdd, !this.c);
-      else this.k[0].push([toAdd, !this.c]);
+      else this.k[0].push(toAdd);
       chunk = chunk.subarray(len);
-    }
-    if (add || !this.c) {
+      if (chunk.length) return this.push(chunk, final);
+    } else {
       let f = 0, i = 0, is: number, buf: Uint8Array;
-      const dl = chunk.length, pl = this.p.length, l = dl + pl;
-      if (!dl) {
-        if (!pl) return;
-        buf = this.p
-      } else if (!pl) buf = chunk;
+      if (!this.p.length) buf = chunk;
+      else if (!chunk.length) buf = this.p;
       else {
-        buf = new Uint8Array(l);
+        buf = new u8(this.p.length + chunk.length)
         buf.set(this.p), buf.set(chunk, this.p.length);
       }
-      this.p = et;
-      // not l - 4 because we need i to become l
-      for (; i < l; ++i) {
+      const l = buf.length, oc = this.c, add = oc && this.d;
+      for (; i < l - 4; ++i) {
         const sig = b4(buf, i);
         if (sig == 0x4034B50) {
-          f = 1;
-          if (add) add.push(et, true);
+          f = 1, is = i;
           this.d = null;
           this.c = 0;
           const bf = b2(buf, i + 6), cmp = b2(buf, i + 8), u = bf & 2048, dd = bf & 8, fnl = b2(buf, i + 26), es = b2(buf, i + 28);
           if (l > i + 30 + fnl + es) {
-            const chks = [];
+            const chks: Uint8Array[] = [];
             this.k.unshift(chks);
-            f = 2, is = i;
-            let sc = b4(buf, i + 18);
+            f = 2;
+            let sc = b4(buf, i + 18), su = b4(buf, i + 22);
             const fn = strFromU8(buf.subarray(i + 30, i += 30 + fnl), !u);
-            if (dd) sc = -1;
-            if (sc == 4294967295) sc = z64e(buf, i)[0];
-            if (!this.o[cmp]) {
-              this.onfile('unknown compression type ' + cmp, fn, null);
-              break;
-            }
+            if (sc == 4294967295) { [sc, su] = dd ? [-2] : z64e(buf, i); }
+            else if (dd) sc = -1;
+            i += es;
             this.c = sc;
             const file = {
+              name: fn,
+              compression: cmp,
               start: () => {
                 if (!file.ondata) throw 'no callback';
-                if (!sc) file.ondata(null, new u8(0), true);
+                if (!sc) file.ondata(null, et, true);
                 else {
-                  const d = new this.o[cmp]();
+                  const ctr = this.o[cmp];
+                  if (!ctr) throw 'unknown compression type ' + cmp;
+                  const d = sc < 0 ? new ctr(fn) : new ctr(fn, sc, su);
                   d.ondata = (err, dat, final) => { file.ondata(err, dat, final); }
-                  for (const [dat, final] of chks) d.push(dat, final);
+                  for (const dat of chks) d.push(dat, false);
                   if (this.k[0] == chks) this.d = d;
+                  else d.push(et, true);
                 }
               },
               terminate: () => {
                 if (this.k[0] == chks && this.d.terminate) this.d.terminate();
               }
             } as UnzipFile;
-            this.onfile(null, fn, file);
-            i += es;
+            if (sc >= 0) file.size = sc, file.originalSize = su;
+            this.onfile(file);
           }
           break;
+        } else if (oc) {
+          if (sig == 0x8074B50) {
+            is = i += 12 + (oc == -2 && 8), f = 2, this.c = 0;
+            break;
+          } else if (sig == 0x2014B50) {
+            is = i -= 4, f = 2, this.c = 0;
+            break;
+          }
         }
       }
-      if (add) add.push(f == 2 ? buf.subarray(0, is - 12 - (b4(buf, is - 12) == 0x8074B50 && 4)) : buf.subarray(0, i), !!f);
+      this.p = et
+      if (oc < 0) {
+        const dat = f ? buf.subarray(0, is - 12 - (oc == -2 && 8) - (b4(buf, is - 16) == 0x8074B50 && 4)) : buf.subarray(0, i);
+        if (add) add.push(dat, !!f);
+        else this.k[+(f == 2)].push(dat);
+      }
       if (f & 2) return this.push(buf.subarray(i), final);
-      else if (f & 1) this.p = buf;
-      if (final && (f || this.c)) throw 'invalid zip file';
+      this.p = buf.subarray(i);
     }
+    if (final && this.c) throw 'invalid zip file';
   }
 
   /**

+ 0 - 45
test/4-multiperf.ts

@@ -1,45 +0,0 @@
-import { testSuites } from './util';
-import { deflateSync, inflateSync } from '..';
-import { deflateRawSync, inflateRawSync } from 'zlib';
-import { deflateRaw, inflateRaw } from 'pako';
-import * as UZIP from 'uzip';
-import * as tinf from 'tiny-inflate';
-import { writeFileSync } from 'fs';
-import { join } from 'path';
-
-const cache: Record<string, Buffer> = {};
-
-testSuites({
-  'fflate compress 5x': file => {
-    for (let i = 0; i < 5; ++i) deflateSync(file);
-  },
-  'fflate decompress 5x': (file, name, resetTimer) => {
-    cache[name] = deflateRawSync(file), resetTimer();
-    for (let i = 0; i < 5; ++i) {
-      inflateSync(cache[name]);
-    }
-  },
-  'pako compress 5x': file => {
-    for (let i = 0; i < 5; ++i) deflateRaw(file);
-  },
-  'pako decompress 5x': (_, name) => {
-    for (let i = 0; i < 5; ++i) inflateRaw(cache[name]);
-  },
-  'uzip compress 5x': file => {
-    for (let i = 0; i < 5; ++i) UZIP.deflateRaw(file);
-  },
-  'uzip decompress 5x': (_, name) => {
-    for (let i = 0; i < 5; ++i) UZIP.inflateRaw(cache[name]);
-  },
-  'tiny-inflate decompress 5x': (file, name) => {
-    for (let i = 0; i < 5; ++i) tinf(cache[name], new Uint8Array(file.length));
-  },
-  'zlib compress 5x': file => {
-    for (let i = 0; i < 5; ++i) deflateRawSync(file);
-  },
-  'zlib decompress 5x': (_, name) => {
-    for (let i = 0; i < 5; ++i) inflateRawSync(cache[name]);
-  }
-}).then(perf => {
-  writeFileSync(join(__dirname, 'results', 'multiTimings.json'), JSON.stringify(perf, null, 2));
-})

+ 0 - 0
test/4-zip.ts


+ 27 - 13
test/util.ts

@@ -5,7 +5,7 @@ import { suite } from 'uvu';
 import { performance } from 'perf_hooks';
 import { Worker } from 'worker_threads';
 
-const testFilesRaw = {
+const testFiles = {
   basic: Buffer.from('Hello world!'),
   text: 'https://www.gutenberg.org/files/2701/old/moby10b.txt',
   smallImage: 'https://hlevkin.com/hlevkin/TestImages/new/Rainier.bmp',
@@ -13,16 +13,18 @@ const testFilesRaw = {
   largeImage: 'https://www.hlevkin.com/hlevkin/TestImages/new/Sunrise.bmp'
 };
 
-export type TestFile = keyof typeof testFilesRaw;
+const testZipFiles = {
 
-const testFilesPromise = (async () => {
-  let res = {} as Record<TestFile, Buffer>;
-  for (const name in testFilesRaw) {
-    let data = testFilesRaw[name];
+};
+
+const dlCached = async <T extends Record<string, string | Buffer>>(files: T) => {
+  let res = {} as Record<keyof T, Buffer>;
+  for (const name in files) {
+    let data: string | Buffer = files[name];
     if (typeof data == 'string') {
       const fn = resolve(__dirname, 'data', name);
       if (!existsSync(fn)) {
-        console.log('Downloading ' + data + '...');
+        console.log('\nDownloading ' + data + '...');
         data = await new Promise((r, re) => get(data as string, res => {
           const len = +res.headers['content-length'];
           const buf = Buffer.allocUnsafe(len);
@@ -43,11 +45,14 @@ const testFilesPromise = (async () => {
         );
       }
     }
-    res[name] = data as Buffer;
+    res[name as keyof T] = data as Buffer;
   }
   return res;
-})();
+}
+
+export type TestFile = keyof typeof testFiles;
 
+const testFilesPromise = dlCached(testFiles);
 export type TestHandler = (file: Buffer, name: string, resetTimer: () => void) => unknown | Promise<unknown>;
 
 export const testSuites = async <T extends Record<string, TestHandler>>(suites: T) => {
@@ -55,15 +60,15 @@ export const testSuites = async <T extends Record<string, TestHandler>>(suites:
   for (const k in suites) {
     perf[k] = new Promise(async setPerf => {
       const ste = suite(k);
-      let testFiles: typeof testFilesPromise extends Promise<infer T> ? T : never;
+      let localTestFiles: Record<TestFile, Buffer>;
       ste.before(() => testFilesPromise.then(v => {
-        testFiles = v;
+        localTestFiles = v;
       }));
       const localPerf: Record<string, number> = {};
-      for (const name in testFilesRaw) {
+      for (const name in testFiles) {
         ste(name, async () => {
           let ts = performance.now();
-          await suites[k](testFiles[name], name, () => {
+          await suites[k](localTestFiles[name], name, () => {
             ts = performance.now();
           });
           localPerf[name] = performance.now() - ts;
@@ -78,6 +83,15 @@ export const testSuites = async <T extends Record<string, TestHandler>>(suites:
   const resolvedPerf = {} as Record<keyof T, Record<TestFile, number>>;
   for (const k in suites) resolvedPerf[k] = await perf[k];
   return resolvedPerf;
+};
+
+export const stream = (src: Uint8Array, dst: {
+  push(dat: Uint8Array, final: boolean): void;
+}) => {
+  const off = Math.min(65536, src.length >>> 3);
+  for (let i = 0; i < src.length;) {
+    dst.push(src.slice(i, i + off), (i += off) >= src.length);
+  }
 }
 
 // create worker string