Browse Source

Fix Zip64 unzip

Arjun Barrett 4 years ago
parent
commit
de70e83f06

+ 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
 ## 0.5.0
-- Add streaming ZIP, UNZIP
+- Add streaming zip, unzip
 - Fix import issues with certain environments
 - Fix import issues with certain environments
   - If you had problems with `worker_threads` being included in your bundle, try updating!
   - If you had problems with `worker_threads` being included in your bundle, try updating!
 ## 0.4.8
 ## 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';
 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:
 And use:
 ```js
 ```js
 // This is an ArrayBuffer of data
 // This is an ArrayBuffer of data
@@ -314,21 +312,24 @@ unzipper.register(fflate.UnzipInflate);
 const neededFiles = ['file1.txt', 'example.json'];
 const neededFiles = ['file1.txt', 'example.json'];
 
 
 // Can specify handler in constructor too
 // 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) => {
     file.ondata = (err, dat, final) => {
       // Stream output here
       // Stream output here
       console.log(dat, final);
       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
     // You should only start the stream if you plan to use it to improve
     // performance. Only after starting the stream will ondata be called.
     // performance. Only after starting the stream will ondata be called.
+    // This method will throw if the compression method hasn't been registered
     file.start();
     file.start();
   }
   }
 };
 };
@@ -447,14 +448,14 @@ zip.end();
 
 
 // Streaming Unzip should register the asynchronous inflation algorithm
 // Streaming Unzip should register the asynchronous inflation algorithm
 // for parallel processing.
 // 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.ondata = (err, chunk, final) => { ... };
     stream.start();
     stream.start();
 
 
     if (needToCancel) {
     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.
 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
 ## 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.
 `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
 
 
-Ƭ  **UnzipFileHandler**: (err: Error \| string,name: string,file: [UnzipFile](interfaces/unzipfile.md)) => void
+Ƭ  **UnzipFileHandler**: (file: [UnzipFile](interfaces/unzipfile.md)) => void
 
 
 Handler for streaming ZIP decompression
 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
 **`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
 ### 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
 Creates a DEFLATE decompression that can be used in ZIP archives
 
 
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`_` | string |
+`sz` | number |
+
 **Returns:** [AsyncUnzipInflate](asyncunzipinflate.md)
 **Returns:** [AsyncUnzipInflate](asyncunzipinflate.md)
 
 
 ## Properties
 ## Properties

+ 9 - 1
docs/interfaces/unzipdecoderconstructor.md

@@ -20,10 +20,18 @@ A constructor for a decoder for unzip streams
 
 
 ### constructor
 ### constructor
 
 
-\+ **new UnzipDecoderConstructor**(): [UnzipDecoder](unzipdecoder.md)
+\+ **new UnzipDecoderConstructor**(`filename`: string, `size?`: number, `originalSize?`: number): [UnzipDecoder](unzipdecoder.md)
 
 
 Creates an instance of the decoder
 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)
 **Returns:** [UnzipDecoder](unzipdecoder.md)
 
 
 ## Properties
 ## Properties

+ 39 - 0
docs/interfaces/unzipfile.md

@@ -10,7 +10,11 @@ Streaming file extraction from ZIP archives
 
 
 ### Properties
 ### Properties
 
 
+* [compression](unzipfile.md#compression)
+* [name](unzipfile.md#name)
 * [ondata](unzipfile.md#ondata)
 * [ondata](unzipfile.md#ondata)
+* [originalSize](unzipfile.md#originalsize)
+* [size](unzipfile.md#size)
 * [terminate](unzipfile.md#terminate)
 * [terminate](unzipfile.md#terminate)
 
 
 ### Methods
 ### Methods
@@ -19,6 +23,25 @@ Streaming file extraction from ZIP archives
 
 
 ## Properties
 ## 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
 
 
 •  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
 •  **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
 
 
 •  **terminate**: [AsyncTerminable](asyncterminable.md)
 •  **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
 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
 ZipPassThrough class and overriding its process() method, or using one of
-ZipDeflate or AsyncZipDeflate
+ZipDeflate or AsyncZipDeflate.
 
 
 ___
 ___
 
 
@@ -93,10 +93,11 @@ ___
 
 
 ### flag
 ### flag
 
 
-• `Optional` **flag**: 0 \| 1 \| 2 \| 3
+• `Optional` **flag**: number
 
 
 Bits 1 and 2 of the general purpose bit flag, specified in PKZIP's
 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
 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
 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
 A method called when the stream is no longer needed, for clean-up
 purposes. This will not always be called after the stream completes,
 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.
 processed if you have clean-up logic.

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "fflate",
   "name": "fflate",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "description": "High performance (de)compression in an 8kB package",
   "description": "High performance (de)compression in an 8kB package",
   "main": "./lib/index.js",
   "main": "./lib/index.js",
   "module": "./esm/browser.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:
 // You may also wish to take a look at the guide I made about this program:
 // https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
 // 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
 // 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
 // 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*).
 // 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
 // read d, starting at bit p and mask with m
 const bits = (d: Uint8Array, p: number, m: number) => {
 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;
   return ((d[o] | (d[o + 1] << 8)) >>> (p & 7)) & m;
 }
 }
 
 
 // read d, starting at bit p continuing for at least 16 bits
 // read d, starting at bit p continuing for at least 16 bits
 const bits16 = (d: Uint8Array, p: number) => {
 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));
   return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >>> (p & 7));
 }
 }
 
 
 // get end of byte
 // 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,
 // typed array slice - allows garbage collector to free original reference,
 // while being more compatible than .slice
 // 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
 // starting at p, write the minimum number of bits that can hold v to d
 const wbits = (d: Uint8Array, p: number, v: number) => {
 const wbits = (d: Uint8Array, p: number, v: number) => {
   v <<= p & 7;
   v <<= p & 7;
-  const o = (p / 8) >> 0;
+  const o = (p / 8) | 0;
   d[o] |= v;
   d[o] |= v;
   d[o + 1] |= v >>> 8;
   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
 // starting at p, write the minimum number of bits (>8) that can hold v to d
 const wbits16 = (d: Uint8Array, p: number, v: number) => {
 const wbits16 = (d: Uint8Array, p: number, v: number) => {
   v <<= p & 7;
   v <<= p & 7;
-  const o = (p / 8) >> 0;
+  const o = (p / 8) | 0;
   d[o] |= v;
   d[o] |= v;
   d[o + 1] |= v >>> 8;
   d[o + 1] |= v >>> 8;
   d[o + 2] |= v >>> 16;
   d[o + 2] |= v >>> 16;
@@ -356,7 +356,7 @@ const hTree = (d: Uint16Array, mb: number) => {
   }
   }
   const s = t.length;
   const s = t.length;
   const t2 = t.slice();
   const t2 = t.slice();
-  if (!s) return [new u8(0), 0] as const;
+  if (!s) return [et, 0] as const;
   if (s == 1) {
   if (s == 1) {
     const v = new u8(t[0].s + 1);
     const v = new u8(t[0].s + 1);
     v[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);
     const dt = inflt(this.p, this.o, this.s);
     this.ondata(slc(dt, bts, this.s.b), this.d);
     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.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
  * 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
  * @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
 // flattened Zippable
 type FlatZippable<A extends boolean> = Record<string, [Uint8Array, (A extends true ? AsyncZipOptions : ZipOptions)]>;
 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;;) {
   for (let r = '', i = 0;;) {
     let c = d[i++];
     let c = d[i++];
     const eb = ((c > 127) as unknown as number) + ((c > 223) as unknown as number) + ((c > 239) as unknown as number);
     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)
     if (!eb) r += String.fromCharCode(c)
     else if (eb == 3) {
     else if (eb == 3) {
       c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | (d[i++] & 63)) - 65536,
       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;
   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)
 // write zip footer (end of central directory)
 const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => {
 const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => {
   wbytes(o, b, 0x6054B50); // skip disk
   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
    * 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
    * ZipPassThrough class and overriding its process() method, or using one of
-   * ZipDeflate or AsyncZipDeflate
+   * ZipDeflate or AsyncZipDeflate.
    */
    */
   size: number;
   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
    * 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
    * ZipPassThrough class and overriding its process() method, or using one of
-   * ZipDeflate or AsyncZipDeflate
+   * ZipDeflate or AsyncZipDeflate.
    */
    */
   crc: number;
   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
    * 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
    * 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
    * A method called when the stream is no longer needed, for clean-up
    * purposes. This will not always be called after the stream completes,
    * 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.
    * processed if you have clean-up logic.
    */
    */
   terminate?: AsyncTerminable;
   terminate?: AsyncTerminable;
@@ -2353,7 +2342,7 @@ export class ZipPassThrough implements ZipInputFile {
    * @param final Whether this is the last chunk
    * @param final Whether this is the last chunk
    */
    */
   push(chunk: Uint8Array, final?: boolean) {
   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.c.p(chunk);
     this.size += chunk.length;
     this.size += chunk.length;
     if (final) this.crc = this.c.d();
     if (final) this.crc = this.c.d();
@@ -2535,7 +2524,12 @@ export class Zip {
         cl += dat.length;
         cl += dat.length;
         chks.push(dat);
         chks.push(dat);
         if (final) {
         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;
           uf.c = cl, uf.b = hl + cl + 16, uf.crc = file.crc, uf.size = file.size;
           if (tr) uf.r();
           if (tr) uf.r();
           tr = 1;
           tr = 1;
@@ -2754,8 +2748,11 @@ export interface UnzipDecoder {
 export interface UnzipDecoderConstructor {
 export interface UnzipDecoderConstructor {
   /**
   /**
    * Creates an instance of the decoder
    * 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
    * The compression format for the data stream. This number is determined by
@@ -2774,6 +2771,29 @@ export interface UnzipFile {
    */
    */
   ondata: AsyncFlateStreamHandler;
   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
    * Starts reading from the stream. Calling this function will always enable
    * this stream, but ocassionally the stream will be enabled even without
    * 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 {
 export class AsyncUnzipInflate implements UnzipDecoder {
   static compression = 8;
   static compression = 8;
-  private i: AsyncInflate;
+  private i: AsyncInflate | Inflate;
   ondata: AsyncFlateStreamHandler;
   ondata: AsyncFlateStreamHandler;
   terminate: AsyncTerminable;
   terminate: AsyncTerminable;
 
 
   /**
   /**
    * Creates a DEFLATE decompression that can be used in ZIP archives
    * 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) {
   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 d: UnzipDecoder;
   private c: number;
   private c: number;
   private p: Uint8Array;
   private p: Uint8Array;
-  private k: Array<[Uint8Array, boolean]>[];
+  private k: Uint8Array[][];
   private o: Record<number, UnzipDecoderConstructor>;
   private o: Record<number, UnzipDecoderConstructor>;
 
 
   /**
   /**
@@ -2879,75 +2906,85 @@ export class Unzip {
    * @param final Whether this is the last chunk
    * @param final Whether this is the last chunk
    */
    */
   push(chunk: Uint8Array, final: boolean) {
   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 len = Math.min(this.c, chunk.length);
       const toAdd = chunk.subarray(0, len);
       const toAdd = chunk.subarray(0, len);
       this.c -= len;
       this.c -= len;
       if (this.d) this.d.push(toAdd, !this.c);
       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);
       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;
       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 {
       else {
-        buf = new Uint8Array(l);
+        buf = new u8(this.p.length + chunk.length)
         buf.set(this.p), buf.set(chunk, this.p.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);
         const sig = b4(buf, i);
         if (sig == 0x4034B50) {
         if (sig == 0x4034B50) {
-          f = 1;
-          if (add) add.push(et, true);
+          f = 1, is = i;
           this.d = null;
           this.d = null;
           this.c = 0;
           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);
           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) {
           if (l > i + 30 + fnl + es) {
-            const chks = [];
+            const chks: Uint8Array[] = [];
             this.k.unshift(chks);
             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);
             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;
             this.c = sc;
             const file = {
             const file = {
+              name: fn,
+              compression: cmp,
               start: () => {
               start: () => {
                 if (!file.ondata) throw 'no callback';
                 if (!file.ondata) throw 'no callback';
-                if (!sc) file.ondata(null, new u8(0), true);
+                if (!sc) file.ondata(null, et, true);
                 else {
                 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); }
                   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;
                   if (this.k[0] == chks) this.d = d;
+                  else d.push(et, true);
                 }
                 }
               },
               },
               terminate: () => {
               terminate: () => {
                 if (this.k[0] == chks && this.d.terminate) this.d.terminate();
                 if (this.k[0] == chks && this.d.terminate) this.d.terminate();
               }
               }
             } as UnzipFile;
             } as UnzipFile;
-            this.onfile(null, fn, file);
-            i += es;
+            if (sc >= 0) file.size = sc, file.originalSize = su;
+            this.onfile(file);
           }
           }
           break;
           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);
       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 { performance } from 'perf_hooks';
 import { Worker } from 'worker_threads';
 import { Worker } from 'worker_threads';
 
 
-const testFilesRaw = {
+const testFiles = {
   basic: Buffer.from('Hello world!'),
   basic: Buffer.from('Hello world!'),
   text: 'https://www.gutenberg.org/files/2701/old/moby10b.txt',
   text: 'https://www.gutenberg.org/files/2701/old/moby10b.txt',
   smallImage: 'https://hlevkin.com/hlevkin/TestImages/new/Rainier.bmp',
   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'
   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') {
     if (typeof data == 'string') {
       const fn = resolve(__dirname, 'data', name);
       const fn = resolve(__dirname, 'data', name);
       if (!existsSync(fn)) {
       if (!existsSync(fn)) {
-        console.log('Downloading ' + data + '...');
+        console.log('\nDownloading ' + data + '...');
         data = await new Promise((r, re) => get(data as string, res => {
         data = await new Promise((r, re) => get(data as string, res => {
           const len = +res.headers['content-length'];
           const len = +res.headers['content-length'];
           const buf = Buffer.allocUnsafe(len);
           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;
   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 type TestHandler = (file: Buffer, name: string, resetTimer: () => void) => unknown | Promise<unknown>;
 
 
 export const testSuites = async <T extends Record<string, TestHandler>>(suites: T) => {
 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) {
   for (const k in suites) {
     perf[k] = new Promise(async setPerf => {
     perf[k] = new Promise(async setPerf => {
       const ste = suite(k);
       const ste = suite(k);
-      let testFiles: typeof testFilesPromise extends Promise<infer T> ? T : never;
+      let localTestFiles: Record<TestFile, Buffer>;
       ste.before(() => testFilesPromise.then(v => {
       ste.before(() => testFilesPromise.then(v => {
-        testFiles = v;
+        localTestFiles = v;
       }));
       }));
       const localPerf: Record<string, number> = {};
       const localPerf: Record<string, number> = {};
-      for (const name in testFilesRaw) {
+      for (const name in testFiles) {
         ste(name, async () => {
         ste(name, async () => {
           let ts = performance.now();
           let ts = performance.now();
-          await suites[k](testFiles[name], name, () => {
+          await suites[k](localTestFiles[name], name, () => {
             ts = performance.now();
             ts = performance.now();
           });
           });
           localPerf[name] = performance.now() - ts;
           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>>;
   const resolvedPerf = {} as Record<keyof T, Record<TestFile, number>>;
   for (const k in suites) resolvedPerf[k] = await perf[k];
   for (const k in suites) resolvedPerf[k] = await perf[k];
   return resolvedPerf;
   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
 // create worker string