Browse Source

Add streaming ZIP and unzip; fix import issues

Arjun Barrett 4 years ago
parent
commit
73ae08efee

+ 4 - 0
CHANGELOG.md

@@ -1,3 +1,7 @@
+## 0.5.0
+- 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
 - Support strict Content Security Policy
   - Remove `new Function`

+ 142 - 9
README.md

@@ -19,6 +19,7 @@ In addition to the base decompression and compression APIs, `fflate` supports hi
 | Supports files up to 4GB    | ✅     | ❌                      | ❌                    | ✅                             |
 | Doesn't hang on error       | ✅     | ❌                      | ❌                    | ✅                             |
 | Multi-thread/Asynchronous   | ❌     | ❌                      | ❌                    | ✅                             |
+| Streaming ZIP support       | ❌     | ❌                      | ❌                    | ✅                             |
 | Uses ES Modules             | ❌     | ❌                      | ❌                    | ✅                             |
 
 ## Demo
@@ -53,7 +54,11 @@ If you want to load from a CDN in the browser:
 ```html
 <!--
 You should use either UNPKG or jsDelivr (i.e. only one of the following)
-Note that tree shaking is completely unsupported from the CDN
+
+Note that tree shaking is completely unsupported from the CDN. If you want
+a small build without build tools, please ask me and I will make one manually
+with only the features you need.
+
 You may also want to specify the version, e.g. with [email protected]
 -->
 <script src="https://unpkg.com/fflate/umd/index.js"></script>
@@ -178,20 +183,45 @@ const deflateStream = new fflate.Deflate((chunk, final) => {
   console.log(chunk, final);
 });
 
+// If you want to create a stream from strings, use EncodeUTF8
+const utfEncode = new fflate.EncodeUTF8((data, final) => {
+  // Chaining streams together is done by pushing to the
+  // next stream in the handler for the previous stream
+  deflateStream.push(data, final);
+});
+
+utfEncode.push('Hello'.repeat(1000));
+utfEncode.push(' '.repeat(100));
+utfEncode.push('world!'.repeat(10), true);
+
+// The deflateStream has logged the compressed data
+
 const inflateStream = new fflate.Inflate();
 inflateStream.ondata = (decompressedChunk, final) => {
   // print out a string of the compressed data
   console.log(fflate.strFromU8(decompressedChunk));
 };
 
+let stringData = '';
+
+// Streaming UTF-8 decode is available too
+const utfDecode = new fflate.DecodeUTF8((data, final) => {
+  stringData += data;
+});
+
 // Decompress streams auto-detect the compression method, as the
 // non-streaming decompress() method does.
 const dcmpStrm = new fflate.Decompress((chunk, final) => {
-  console.log(
-    'This chunk was encoded in either GZIP, Zlib, or DEFLATE',
-    chunk
-  );
+  console.log(chunk, 'was encoded with GZIP, Zlib, or DEFLATE');
+  utfDecode.push(chunk, final);
 });
+
+dcmpStrm.push(zlibJSONData1);
+dcmpStrm.push(zlibJSONData2, true);
+
+// This succeeds; the UTF-8 decoder chained with the unknown compression format
+// stream to reach a string as a sink.
+console.log(JSON.parse(stringData));
 ```
 
 You can create multi-file ZIP archives easily as well. Note that by default, compression is enabled for all files, which is not useful when ZIPping many PNGs, JPEGs, PDFs, etc. because those formats are already compressed. You should either override the level on a per-file basis or globally to avoid wasting resources.
@@ -242,11 +272,83 @@ const zipped = fflate.zipSync({
 // { 'nested/directory/a2.txt': Uint8Array(2) [97, 97] })
 const decompressed = fflate.unzipSync(zipped);
 ```
+
+If you need extremely high performance or custom ZIP compression formats, you can use the highly-extensible ZIP streams. They take streams as both input and output. You can even use custom compression/decompression algorithms from other libraries, as long as they [are defined in the ZIP spec](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) (see section 4.4.5). If you'd like more info on using custom compressors, [feel free to ask](https://github.com/101arrowz/fflate/discussions).
+```js
+// ZIP object
+// Can also specify zip.ondata outside of the constructor
+const zip = new fflate.Zip((err, dat, final) => {
+  if (!err) {
+    // output of the streams
+    console.log(dat, final);
+  }
+});
+
+const helloTxt = new fflate.ZipDeflate('hello.txt', {
+  level: 9
+});
+
+// Always add streams to ZIP archives before pushing to those streams
+zip.add(helloTxt);
+
+helloTxt.push(chunk1);
+// Last chunk
+helloTxt.push(chunk2, true);
+
+// ZipPassThrough is like ZipDeflate with level 0, but allows for tree shaking
+const nonStreamingFile = new fflate.ZipPassThrough('test.png');
+zip.add(nonStreamingFile);
+// If you have data already loaded, just .push(data, true)
+nonStreamingFile.push(pngData, true);
+
+// You need to call .end() after finishing
+// This ensures the ZIP is valid
+zip.end();
+
+// Unzip object
+const unzipper = new fflate.Unzip();
+
+// This function will almost always have to be called. It is used to support
+// compression algorithms such as BZIP2 or LZMA in ZIP files if just DEFLATE
+// is not enough (though it almost always is).
+// If your ZIP files are not compressed, this line is not needed.
+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)) {
+    file.ondata = (err, dat, final) => {
+      // Stream output here
+      console.log(dat, final);
+    };
+    
+    // You should only start the stream if you plan to use it to improve
+    // performance. Only after starting the stream will ondata be called.
+    file.start();
+  }
+};
+
+unzipper.push(zipChunk1);
+unzipper.push(zipChunk2);
+unzipper.push(zipChunk3, true);
+```
+
 As you may have guessed, there is an asynchronous version of every method as well. Unlike most libraries, this will cause the compression or decompression run in a separate thread entirely and automatically by using Web (or Node) Workers. This means that the processing will not block the main thread at all.
 
 Note that there is a significant initial overhead to using workers of about 70ms, so it's best to avoid the asynchronous API unless necessary. However, if you're compressing multiple large files at once, or the synchronous API causes the main thread to hang for too long, the callback APIs are an order of magnitude better.
 ```js
-import { gzip, zlib, AsyncGzip, zip, strFromU8 } from 'fflate';
+import {
+  gzip, zlib, AsyncGzip, zip, unzip, strFromU8,
+  Zip, AsyncZipDeflate, Unzip, AsyncUnzipInflate
+} from 'fflate';
 
 // Workers will work in almost any browser (even IE11!)
 // However, they fail below Node v12 without the --experimental-worker
@@ -329,21 +431,52 @@ unzip(aMassiveZIPFile, (err, unzipped) => {
   console.log(unzipped['data.xml']);
   // Conversion to string
   console.log(strFromU8(unzipped['data.xml']))
-})
+});
+
+// Streaming ZIP archives can accept asynchronous streams. This automatically
+// uses multicore compression.
+const zip = new Zip();
+zip.ondata = (err, chunk, final) => { ... };
+// The JSON and BMP are compressed in parallel
+const exampleFile = new AsyncZipDeflate('example.json');
+exampleFile.push(JSON.stringify({ large: 'object' }), true);
+const exampleFile2 = new AsyncZipDeflate('example2.bmp', { level: 9 });
+exampleFile.push(ec2a);
+exampleFile.push(ec2b);
+exampleFile.push(ec2c);
+...
+exampleFile.push(ec2Final, true);
+zip.end();
+
+// Streaming Unzip should register the asynchronous inflation algorithm
+// for parallel processing.
+const unzip = new Unzip((err, fn, stream) => {
+  if (fn.endsWith('.json')) {
+    stream.ondata = (err, chunk, final) => { ... };
+    stream.start();
+
+    if (needToCancel) {
+      // To cancel these streams, call file.terminate()
+      file.terminate();
+    }
+  }
+});
+unzip.register(AsyncUnzipInflate);
+unzip.push(data, true);
 ```
 
 See the [documentation](https://github.com/101arrowz/fflate/blob/master/docs/README.md) for more detailed information about the API.
 
 ## Bundle size estimates
 
-Since `fflate` uses ES Modules, this table should give you a general idea of `fflate`'s bundle size for the features you need. The maximum bundle size that is possible with `fflate` is about 22kB if you use every single feature, but feature parity with `pako` is only around 10kB (as opposed to 45kB from `pako`). If your bundle size increases dramatically after adding `fflate`, please [create an issue](https://github.com/101arrowz/fflate/issues/new).
+Since `fflate` uses ES Modules, this table should give you a general idea of `fflate`'s bundle size for the features you need. The maximum bundle size that is possible with `fflate` is about 27kB if you use every single feature, but feature parity with `pako` is only around 10kB (as opposed to 45kB from `pako`). If your bundle size increases dramatically after adding `fflate`, please [create an issue](https://github.com/101arrowz/fflate/issues/new).
 
 | Feature                 | Bundle size (minified)         | Nearest competitor     |
 |-------------------------|--------------------------------|------------------------|
 | Decompression           | 3kB                            | `tiny-inflate`         |
 | Compression             | 5kB                            | `UZIP.js`, 184% larger |
 | Async decompression     | 4kB (1kB + raw decompression)  | N/A                    |
-| Async compression       | 5kB (1kB + raw compression)    | N/A                    |
+| Async compression       | 6kB (1kB + raw compression)    | N/A                    |
 | ZIP decompression       | 5kB (2kB + raw decompression)  | `UZIP.js`, 184% larger |
 | ZIP compression         | 7kB (2kB + raw compression)    | `UZIP.js`, 103% larger |
 | GZIP/Zlib decompression | 4kB (1kB + raw decompression)  | `pako`, 1040% larger   |

+ 15 - 6
docs/README.md

@@ -9,6 +9,7 @@
 * [AsyncGunzip](classes/asyncgunzip.md)
 * [AsyncGzip](classes/asyncgzip.md)
 * [AsyncInflate](classes/asyncinflate.md)
+* [AsyncUnzipInflate](classes/asyncunzipinflate.md)
 * [AsyncUnzlib](classes/asyncunzlib.md)
 * [AsyncZipDeflate](classes/asynczipdeflate.md)
 * [AsyncZlib](classes/asynczlib.md)
@@ -19,6 +20,9 @@
 * [Gunzip](classes/gunzip.md)
 * [Gzip](classes/gzip.md)
 * [Inflate](classes/inflate.md)
+* [Unzip](classes/unzip.md)
+* [UnzipInflate](classes/unzipinflate.md)
+* [UnzipPassThrough](classes/unzippassthrough.md)
 * [Unzlib](classes/unzlib.md)
 * [Zip](classes/zip.md)
 * [ZipDeflate](classes/zipdeflate.md)
@@ -39,6 +43,9 @@
 * [AsyncZlibOptions](interfaces/asynczliboptions.md)
 * [DeflateOptions](interfaces/deflateoptions.md)
 * [GzipOptions](interfaces/gzipoptions.md)
+* [UnzipDecoder](interfaces/unzipdecoder.md)
+* [UnzipDecoderConstructor](interfaces/unzipdecoderconstructor.md)
+* [UnzipFile](interfaces/unzipfile.md)
 * [Unzipped](interfaces/unzipped.md)
 * [ZipAttributes](interfaces/zipattributes.md)
 * [ZipInputFile](interfaces/zipinputfile.md)
@@ -54,7 +61,7 @@
 * [FlateStreamHandler](README.md#flatestreamhandler)
 * [StringStreamHandler](README.md#stringstreamhandler)
 * [UnzipCallback](README.md#unzipcallback)
-* [ZipProgressHandler](README.md#zipprogresshandler)
+* [UnzipFileHandler](README.md#unzipfilehandler)
 * [ZippableFile](README.md#zippablefile)
 
 ### Functions
@@ -152,15 +159,17 @@ Callback for asynchronous ZIP decompression
 
 ___
 
-### ZipProgressHandler
+### UnzipFileHandler
 
-Ƭ  **ZipProgressHandler**: (bytesRead: number,bytesOut: number) => void
+Ƭ  **UnzipFileHandler**: (err: Error \| string,name: string,file: [UnzipFile](interfaces/unzipfile.md)) => void
 
-Callback for ZIP compression progress
+Handler for streaming ZIP decompression
 
-**`param`** Any error that occurred
+**`param`** Any errors that have occurred
 
-**`param`** The decompressed ZIP archive
+**`param`** The name of the file being processed
+
+**`param`** The file that was found in the archive
 
 ___
 

+ 76 - 0
docs/classes/asyncunzipinflate.md

@@ -0,0 +1,76 @@
+# Class: AsyncUnzipInflate
+
+Asynchronous streaming DEFLATE decompression for ZIP archives
+
+## Hierarchy
+
+* **AsyncUnzipInflate**
+
+## Implements
+
+* [UnzipDecoder](../interfaces/unzipdecoder.md)
+
+## Index
+
+### Constructors
+
+* [constructor](asyncunzipinflate.md#constructor)
+
+### Properties
+
+* [ondata](asyncunzipinflate.md#ondata)
+* [terminate](asyncunzipinflate.md#terminate)
+* [compression](asyncunzipinflate.md#compression)
+
+### Methods
+
+* [push](asyncunzipinflate.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new AsyncUnzipInflate**(): [AsyncUnzipInflate](asyncunzipinflate.md)
+
+Creates a DEFLATE decompression that can be used in ZIP archives
+
+**Returns:** [AsyncUnzipInflate](asyncunzipinflate.md)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md).[ondata](../interfaces/unzipdecoder.md#ondata)*
+
+___
+
+### terminate
+
+•  **terminate**: [AsyncTerminable](../interfaces/asyncterminable.md)
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md).[terminate](../interfaces/unzipdecoder.md#terminate)*
+
+___
+
+### compression
+
+▪ `Static` **compression**: number = 8
+
+## Methods
+
+### push
+
+▸ **push**(`data`: Uint8Array, `final`: boolean): void
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md)*
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`data` | Uint8Array |
+`final` | boolean |
+
+**Returns:** void

+ 4 - 4
docs/classes/decodeutf8.md

@@ -24,15 +24,15 @@ Streaming UTF-8 decoding
 
 ### constructor
 
-\+ **new DecodeUTF8**(`handler?`: [StringStreamHandler](../README.md#stringstreamhandler)): [DecodeUTF8](decodeutf8.md)
+\+ **new DecodeUTF8**(`cb?`: [StringStreamHandler](../README.md#stringstreamhandler)): [DecodeUTF8](decodeutf8.md)
 
 Creates a UTF-8 decoding stream
 
 #### Parameters:
 
-Name | Type |
------- | ------ |
-`handler?` | [StringStreamHandler](../README.md#stringstreamhandler) |
+Name | Type | Description |
+------ | ------ | ------ |
+`cb?` | [StringStreamHandler](../README.md#stringstreamhandler) | The callback to call whenever data is decoded  |
 
 **Returns:** [DecodeUTF8](decodeutf8.md)
 

+ 4 - 4
docs/classes/encodeutf8.md

@@ -24,15 +24,15 @@ Streaming UTF-8 encoding
 
 ### constructor
 
-\+ **new EncodeUTF8**(`handler?`: [FlateStreamHandler](../README.md#flatestreamhandler)): [EncodeUTF8](encodeutf8.md)
+\+ **new EncodeUTF8**(`cb?`: [FlateStreamHandler](../README.md#flatestreamhandler)): [EncodeUTF8](encodeutf8.md)
 
 Creates a UTF-8 decoding stream
 
 #### Parameters:
 
-Name | Type |
------- | ------ |
-`handler?` | [FlateStreamHandler](../README.md#flatestreamhandler) |
+Name | Type | Description |
+------ | ------ | ------ |
+`cb?` | [FlateStreamHandler](../README.md#flatestreamhandler) | The callback to call whenever data is encoded  |
 
 **Returns:** [EncodeUTF8](encodeutf8.md)
 

+ 80 - 0
docs/classes/unzip.md

@@ -0,0 +1,80 @@
+# Class: Unzip
+
+A ZIP archive decompression stream that emits files as they are discovered
+
+## Hierarchy
+
+* **Unzip**
+
+## Index
+
+### Constructors
+
+* [constructor](unzip.md#constructor)
+
+### Properties
+
+* [onfile](unzip.md#onfile)
+
+### Methods
+
+* [push](unzip.md#push)
+* [register](unzip.md#register)
+
+## Constructors
+
+### constructor
+
+\+ **new Unzip**(`cb?`: [UnzipFileHandler](../README.md#unzipfilehandler)): [Unzip](unzip.md)
+
+Creates a ZIP decompression stream
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`cb?` | [UnzipFileHandler](../README.md#unzipfilehandler) | The callback to call whenever a file in the ZIP archive is found  |
+
+**Returns:** [Unzip](unzip.md)
+
+## Properties
+
+### onfile
+
+•  **onfile**: [UnzipFileHandler](../README.md#unzipfilehandler)
+
+The handler to call whenever a file is discovered
+
+## Methods
+
+### push
+
+▸ **push**(`chunk`: Uint8Array, `final`: boolean): any
+
+Pushes a chunk to be unzipped
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`chunk` | Uint8Array | The chunk to push |
+`final` | boolean | Whether this is the last chunk  |
+
+**Returns:** any
+
+___
+
+### register
+
+▸ **register**(`decoder`: [UnzipDecoderConstructor](../interfaces/unzipdecoderconstructor.md)): void
+
+Registers a decoder with the stream, allowing for files compressed with
+the compression type provided to be expanded correctly
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`decoder` | [UnzipDecoderConstructor](../interfaces/unzipdecoderconstructor.md) | The decoder constructor  |
+
+**Returns:** void

+ 68 - 0
docs/classes/unzipinflate.md

@@ -0,0 +1,68 @@
+# Class: UnzipInflate
+
+Streaming DEFLATE decompression for ZIP archives. Prefer AsyncZipInflate for
+better performance.
+
+## Hierarchy
+
+* **UnzipInflate**
+
+## Implements
+
+* [UnzipDecoder](../interfaces/unzipdecoder.md)
+
+## Index
+
+### Constructors
+
+* [constructor](unzipinflate.md#constructor)
+
+### Properties
+
+* [ondata](unzipinflate.md#ondata)
+* [compression](unzipinflate.md#compression)
+
+### Methods
+
+* [push](unzipinflate.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new UnzipInflate**(): [UnzipInflate](unzipinflate.md)
+
+Creates a DEFLATE decompression that can be used in ZIP archives
+
+**Returns:** [UnzipInflate](unzipinflate.md)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md).[ondata](../interfaces/unzipdecoder.md#ondata)*
+
+___
+
+### compression
+
+▪ `Static` **compression**: number = 8
+
+## Methods
+
+### push
+
+▸ **push**(`data`: Uint8Array, `final`: boolean): void
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md)*
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`data` | Uint8Array |
+`final` | boolean |
+
+**Returns:** void

+ 53 - 0
docs/classes/unzippassthrough.md

@@ -0,0 +1,53 @@
+# Class: UnzipPassThrough
+
+Streaming pass-through decompression for ZIP archives
+
+## Hierarchy
+
+* **UnzipPassThrough**
+
+## Implements
+
+* [UnzipDecoder](../interfaces/unzipdecoder.md)
+
+## Index
+
+### Properties
+
+* [ondata](unzippassthrough.md#ondata)
+* [compression](unzippassthrough.md#compression)
+
+### Methods
+
+* [push](unzippassthrough.md#push)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md).[ondata](../interfaces/unzipdecoder.md#ondata)*
+
+___
+
+### compression
+
+▪ `Static` **compression**: number = 0
+
+## Methods
+
+### push
+
+▸ **push**(`data`: Uint8Array, `final`: boolean): void
+
+*Implementation of [UnzipDecoder](../interfaces/unzipdecoder.md)*
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`data` | Uint8Array |
+`final` | boolean |
+
+**Returns:** void

+ 7 - 1
docs/classes/zip.md

@@ -26,10 +26,16 @@ A zippable archive to which files can incrementally be added
 
 ### constructor
 
-\+ **new Zip**(): [Zip](zip.md)
+\+ **new Zip**(`cb?`: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)): [Zip](zip.md)
 
 Creates an empty ZIP archive to which files can be added
 
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`cb?` | [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler) | The callback to call whenever data for the generated ZIP archive           is available  |
+
 **Returns:** [Zip](zip.md)
 
 ## Properties

+ 9 - 0
docs/classes/zippassthrough.md

@@ -19,6 +19,7 @@ A pass-through stream to keep data uncompressed in a ZIP archive.
 ### Properties
 
 * [attrs](zippassthrough.md#attrs)
+* [compression](zippassthrough.md#compression)
 * [crc](zippassthrough.md#crc)
 * [filename](zippassthrough.md#filename)
 * [ondata](zippassthrough.md#ondata)
@@ -55,6 +56,14 @@ Name | Type | Description |
 
 ___
 
+### compression
+
+•  **compression**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[compression](../interfaces/zipinputfile.md#compression)*
+
+___
+
 ### crc
 
 •  **crc**: number

+ 58 - 0
docs/interfaces/unzipdecoder.md

@@ -0,0 +1,58 @@
+# Interface: UnzipDecoder
+
+A decoder for files in ZIP streams
+
+## Hierarchy
+
+* **UnzipDecoder**
+
+## Implemented by
+
+* [AsyncUnzipInflate](../classes/asyncunzipinflate.md)
+* [UnzipInflate](../classes/unzipinflate.md)
+* [UnzipPassThrough](../classes/unzippassthrough.md)
+
+## Index
+
+### Properties
+
+* [ondata](unzipdecoder.md#ondata)
+* [terminate](unzipdecoder.md#terminate)
+
+### Methods
+
+* [push](unzipdecoder.md#push)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+The handler to call whenever data is available
+
+___
+
+### terminate
+
+• `Optional` **terminate**: [AsyncTerminable](asyncterminable.md)
+
+A method to terminate any internal workers used by the stream. Subsequent
+calls to push() should silently fail.
+
+## Methods
+
+### push
+
+▸ **push**(`data`: Uint8Array, `final`: boolean): void
+
+Pushes a chunk to be decompressed
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`data` | Uint8Array | The data in this chunk. Do not consume (detach) this data. |
+`final` | boolean | Whether this is the last chunk in the data stream  |
+
+**Returns:** void

+ 37 - 0
docs/interfaces/unzipdecoderconstructor.md

@@ -0,0 +1,37 @@
+# Interface: UnzipDecoderConstructor
+
+A constructor for a decoder for unzip streams
+
+## Hierarchy
+
+* **UnzipDecoderConstructor**
+
+## Index
+
+### Constructors
+
+* [constructor](unzipdecoderconstructor.md#constructor)
+
+### Properties
+
+* [compression](unzipdecoderconstructor.md#compression)
+
+## Constructors
+
+### constructor
+
+\+ **new UnzipDecoderConstructor**(): [UnzipDecoder](unzipdecoder.md)
+
+Creates an instance of the decoder
+
+**Returns:** [UnzipDecoder](unzipdecoder.md)
+
+## 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

+ 47 - 0
docs/interfaces/unzipfile.md

@@ -0,0 +1,47 @@
+# Interface: UnzipFile
+
+Streaming file extraction from ZIP archives
+
+## Hierarchy
+
+* **UnzipFile**
+
+## Index
+
+### Properties
+
+* [ondata](unzipfile.md#ondata)
+* [terminate](unzipfile.md#terminate)
+
+### Methods
+
+* [start](unzipfile.md#start)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+The handler to call whenever data is available
+
+___
+
+### terminate
+
+•  **terminate**: [AsyncTerminable](asyncterminable.md)
+
+A method to terminate any internal workers used by the stream. ondata
+will not be called any further.
+
+## Methods
+
+### start
+
+▸ **start**(): void
+
+Starts reading from the stream. Calling this function will always enable
+this stream, but ocassionally the stream will be enabled even without
+this being called.
+
+**Returns:** void

+ 1 - 1
docs/interfaces/zipinputfile.md

@@ -59,7 +59,7 @@ ___
 
 ### compression
 
-• `Optional` **compression**: number
+•  **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

+ 12 - 4
package.json

@@ -1,9 +1,9 @@
 {
   "name": "fflate",
-  "version": "0.4.8",
+  "version": "0.5.0",
   "description": "High performance (de)compression in an 8kB package",
   "main": "./lib/index.js",
-  "module": "./esm/index.mjs",
+  "module": "./esm/browser.js",
   "types": "./lib/index.d.ts",
   "unpkg": "./umd/index.js",
   "jsdelivr": "./umd/index.js",
@@ -16,6 +16,14 @@
       "node": "./esm/index.mjs",
       "require": "./lib/index.js",
       "default": "./esm/browser.js"
+    },
+    "./node": {
+      "import": "./esm/index.mjs",
+      "require": "./lib/node.js"
+    },
+    "./browser": {
+      "import": "./esm/browser.js",
+      "require": "./lib/browser.js"
     }
   },
   "targets": {
@@ -52,9 +60,9 @@
     "non-blocking"
   ],
   "scripts": {
-    "build": "yarn build:lib && yarn build:docs && yarn build:rewrite && yarn build:demo",
+    "build": "yarn build:lib && yarn build:docs && yarn build:demo",
     "script": "node -r ts-node/register scripts/$SC.ts",
-    "build:lib": "tsc && tsc --project tsconfig.esm.json && yarn build:umd",
+    "build:lib": "tsc && tsc --project tsconfig.esm.json && yarn build:umd && yarn build:rewrite",
     "build:umd": "SC=buildUMD yarn script",
     "build:rewrite": "SC=rewriteBuilds yarn script",
     "build:demo": "tsc --project tsconfig.demo.json && parcel build demo/index.html --public-url \"./\" && SC=cpGHPages yarn script",

+ 10 - 5
scripts/rewriteBuilds.ts

@@ -1,9 +1,12 @@
 import { readFileSync, writeFileSync, unlinkSync } from 'fs';
 import { join } from 'path';
 const atClass = /\/\*\* \@class \*\//g, pure = '/*#__PURE__*/';
+const extraneousExports = /exports\.(.*) = void 0;\n/;
 const libDir = join(__dirname, '..', 'lib');
 const libIndex = join(libDir, 'index.js');
-writeFileSync(libIndex, readFileSync(libIndex, 'utf-8').replace(atClass, pure));
+const lib = readFileSync(libIndex, 'utf-8').replace(atClass, pure).replace(extraneousExports, '');
+
+writeFileSync(libIndex, lib);
 const esmDir = join(__dirname, '..', 'esm');
 const esmIndex = join(esmDir, 'index.js'),
       esmWK = join(esmDir, 'worker.js'),
@@ -13,8 +16,10 @@ const wk = readFileSync(esmWK, 'utf-8'),
       nwk = readFileSync(esmNWK, 'utf-8');
 unlinkSync(esmIndex), unlinkSync(esmWK), unlinkSync(esmNWK);
 unlinkSync(join(libDir, 'worker.d.ts')), unlinkSync(join(libDir, 'node-worker.d.ts'));
-const workerImport = /import wk from '\.\/node-worker';/;
+const workerImport = /import (.*) from '\.\/node-worker';/;
+const workerRequire = /var (.*) = require\("\.\/node-worker"\);/;
 const defaultExport = /export default/;
-const constDecl = 'var wk =';
-writeFileSync(join(esmDir, 'index.mjs'), esm.replace(workerImport, nwk.replace(defaultExport, constDecl)));
-writeFileSync(join(esmDir, 'browser.js'), esm.replace(workerImport, wk.replace(defaultExport, constDecl)));
+writeFileSync(join(esmDir, 'index.mjs'), esm.replace(workerImport, name => nwk.replace(defaultExport, `var ${name.slice(7, name.indexOf(' ', 8))} =`)));
+writeFileSync(join(esmDir, 'browser.js'), esm.replace(workerImport, name => wk.replace(defaultExport, `var ${name.slice(7, name.indexOf(' ', 8))} =`)));
+writeFileSync(join(libDir, 'node.js'), lib.replace(workerRequire, name => nwk.replace(defaultExport, `var ${name.slice(4, name.indexOf(' ', 5))} =`)));
+writeFileSync(join(libDir, 'browser.js'), lib.replace(workerRequire, name => wk.replace(defaultExport, `var ${name.slice(4, name.indexOf(' ', 5))} =`)));

+ 265 - 17
src/index.ts

@@ -979,6 +979,8 @@ const b2 = (d: Uint8Array, b: number) => d[b] | (d[b + 1] << 8);
 // read 4 bytes
 const b4 = (d: Uint8Array, b: number) => (d[b] | (d[b + 1] << 8) | (d[b + 2] << 16)) + (d[b + 3] << 23) * 2;
 
+const b8 = (d: Uint8Array, b: number) => b4(d, b) | (b4(d, b) * 4294967296);
+
 // write bytes
 const wbytes = (d: Uint8Array, b: number, v: number) => {
   for (; v; ++b) d[b] = v, v >>>= 8;
@@ -1986,11 +1988,12 @@ export type StringStreamHandler = (data: string, final: boolean) => void;
 export type UnzipCallback = (err: Error | string, data: Unzipped) => void;
 
 /**
- * Callback for ZIP compression progress
- * @param err Any error that occurred
- * @param data The decompressed ZIP archive
+ * 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 ZipProgressHandler = (bytesRead: number, bytesOut: number) => void;
+export type UnzipFileHandler = (err: Error | string, name: string, file: UnzipFile) => void;
 
 // flattened Zippable
 type FlatZippable<A extends boolean> = Record<string, [Uint8Array, (A extends true ? AsyncZipOptions : ZipOptions)]>;
@@ -2039,11 +2042,10 @@ export class DecodeUTF8 {
   private t: TextDecoder;
   /**
    * Creates a UTF-8 decoding stream
-   * @param opts The compression options
-   * @param cb The callback to call whenever data is deflated
+   * @param cb The callback to call whenever data is decoded
    */
-  constructor(handler?: StringStreamHandler) {
-    this.ondata = handler;
+  constructor(cb?: StringStreamHandler) {
+    this.ondata = cb;
     if (tds) this.t = new TextDecoder();
     else this.p = et;
   }
@@ -2078,11 +2080,10 @@ export class DecodeUTF8 {
 export class EncodeUTF8 {
   /**
    * Creates a UTF-8 decoding stream
-   * @param opts The compression options
-   * @param cb The callback to call whenever data is deflated
+   * @param cb The callback to call whenever data is encoded
    */
-  constructor(handler?: FlateStreamHandler) {
-    this.ondata = handler;
+  constructor(cb?: FlateStreamHandler) {
+    this.ondata = cb;
   }
 
   /**
@@ -2165,15 +2166,15 @@ const slzh = (d: Uint8Array, b: number) => b + 30 + b2(d, b + 26) + b2(d, b + 28
 
 // read zip header
 const zh = (d: Uint8Array, b: number, z: boolean) => {
-  const fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
-  const [sc, su, off] = z ? z64e(d, es) : [b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)];
+  const fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20);
+  const [sc, su, off] = z && bs == 4294967295 ? z64e(d, es) : [bs, b4(d, b + 24), b4(d, b + 42)];
   return [b2(d, b + 10), sc, su, fn, es + b2(d, b + 30) + b2(d, b + 32), off] as const;
 }
 
 // read zip64 extra field
 const z64e = (d: Uint8Array, b: number) => {
   for (; b2(d, b) != 1; b += 4 + b2(d, b + 2));
-  return [b4(d, b + 12), b4(d, b + 4), b4(d, b + 20)] as const;
+  return [b8(d, b + 12), b8(d, b + 4), b8(d, b + 20)] as const;
 }
 
 // zip header file
@@ -2262,7 +2263,7 @@ export interface ZipInputFile extends ZipAttributes {
    * the spec in PKZIP's APPNOTE.txt, section 4.4.5. For example, 0 = no
    * compression, 8 = deflate, 14 = LZMA
    */
-  compression?: number;
+  compression: number;
 
   /**
    * Bits 1 and 2 of the general purpose bit flag, specified in PKZIP's
@@ -2317,6 +2318,7 @@ export class ZipPassThrough implements ZipInputFile {
   size: number;
   os?: number;
   attrs?: number;
+  compression: number;
   ondata: AsyncFlateStreamHandler;
   private c: CRCV;
 
@@ -2328,6 +2330,7 @@ export class ZipPassThrough implements ZipInputFile {
     this.filename = filename;
     this.c = crc();
     this.size = 0;
+    this.compression = 0;
   }
 
   /**
@@ -2470,16 +2473,22 @@ type ZIFE = {
 
 type ZipInternalFile = ZHF & ZIFE;
 
+// TODO: Better tree shaking
+
 /**
  * A zippable archive to which files can incrementally be added
  */
 export class Zip {
   private u: ZipInternalFile[];
   private d: number;
+
   /**
    * Creates an empty ZIP archive to which files can be added
+   * @param cb The callback to call whenever data for the generated ZIP archive
+   *           is available
    */
-  constructor() {
+  constructor(cb?: AsyncFlateStreamHandler) {
+    this.ondata = cb;
     this.u = [];
     this.d = 1;
   }
@@ -2715,6 +2724,245 @@ export function zipSync(data: Zippable, opts: ZipOptions = {}) {
   return out;
 }
 
+/**
+ * A decoder for files in ZIP streams
+ */
+export interface UnzipDecoder {  
+  /**
+   * The handler to call whenever data is available
+   */
+  ondata: AsyncFlateStreamHandler;
+  
+  /**
+   * Pushes a chunk to be decompressed
+   * @param data The data in this chunk. Do not consume (detach) this data.
+   * @param final Whether this is the last chunk in the data stream
+   */
+  push(data: Uint8Array, final: boolean): void;
+
+  /**
+   * A method to terminate any internal workers used by the stream. Subsequent
+   * calls to push() should silently fail.
+   */
+  terminate?: AsyncTerminable
+}
+
+/**
+ * A constructor for a decoder for unzip streams
+ */
+export interface UnzipDecoderConstructor {
+  /**
+   * Creates an instance of the decoder
+   */
+  new(): UnzipDecoder;
+
+  /**
+   * 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
+   */
+  compression: number;
+}
+
+/**
+ * Streaming file extraction from ZIP archives
+ */
+export interface UnzipFile {
+  /**
+   * The handler to call whenever data is available
+   */
+  ondata: AsyncFlateStreamHandler;
+
+  /**
+   * Starts reading from the stream. Calling this function will always enable
+   * this stream, but ocassionally the stream will be enabled even without
+   * this being called.
+   */
+  start(): void;
+
+  /**
+   * A method to terminate any internal workers used by the stream. ondata
+   * will not be called any further.
+   */
+  terminate: AsyncTerminable
+}
+
+/**
+ * Streaming pass-through decompression for ZIP archives
+ */
+export class UnzipPassThrough implements UnzipDecoder {
+  static compression = 0;
+  ondata: AsyncFlateStreamHandler;
+  push(data: Uint8Array, final: boolean) {
+    this.ondata(null, data, final);
+  }
+}
+
+/**
+ * Streaming DEFLATE decompression for ZIP archives. Prefer AsyncZipInflate for
+ * better performance.
+ */
+export class UnzipInflate implements UnzipDecoder {
+  static compression = 8;
+  private i: Inflate;
+  ondata: AsyncFlateStreamHandler;
+
+  /**
+   * Creates a DEFLATE decompression that can be used in ZIP archives
+   */
+  constructor() {
+    this.i = new Inflate((dat, final) => {
+      this.ondata(null, dat, final);
+    });
+  }
+
+  push(data: Uint8Array, final: boolean) {
+    try {
+      this.i.push(data, final);
+    } catch(e) {
+      this.ondata(e, data, final);
+    }
+  }
+}
+
+/**
+ * Asynchronous streaming DEFLATE decompression for ZIP archives
+ */
+export class AsyncUnzipInflate implements UnzipDecoder {
+  static compression = 8;
+  private i: AsyncInflate;
+  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;
+  }
+
+  push(data: Uint8Array, final: boolean) {
+    this.i.push(slc(data, 0), final);
+  }
+}
+
+/**
+ * A ZIP archive decompression stream that emits files as they are discovered
+ */
+export class Unzip {
+  private d: UnzipDecoder;
+  private c: number;
+  private p: Uint8Array;
+  private k: Array<[Uint8Array, boolean]>[];
+  private o: Record<number, UnzipDecoderConstructor>;
+
+  /**
+   * Creates a ZIP decompression stream
+   * @param cb The callback to call whenever a file in the ZIP archive is found
+   */
+  constructor(cb?: UnzipFileHandler) {
+    this.onfile = cb;
+    this.k = [];
+    this.o = {
+      0: UnzipPassThrough
+    };
+    this.p = et;
+  }
+  
+  /**
+   * Pushes a chunk to be unzipped
+   * @param chunk The chunk to push
+   * @param final Whether this is the last chunk
+   */
+  push(chunk: Uint8Array, final: boolean) {
+    const add = this.c == -1 && this.d;
+    if (this.c && !add) {
+      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]);
+      chunk = chunk.subarray(len);
+    }
+    let f = 0, i = 0, buf: Uint8Array;
+    if (add || !this.c) {
+      const dl = chunk.length, pl = this.p.length, l = dl + pl;
+      if (!dl) {
+        if (!pl) return;
+        buf = this.p
+      } else if (!pl) buf = chunk;
+      else {
+        buf = new Uint8Array(l);
+        buf.set(this.p), buf.set(chunk, this.p.length);
+      }
+      this.p = et;
+      for (; i < l - 4; ++i) {
+        const sig = b4(buf, i);
+        if (sig == 0x4034B50) {
+          f = 1;
+          if (add) add.push(et, true);
+          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 = [];
+            this.k.unshift(chks);
+            f = 2;
+            let sc = b4(buf, i + 18);
+            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;
+            }
+            this.c = sc;
+            const file = {
+              start: () => {
+                if (!file.ondata) throw 'no callback';
+                if (!sc) file.ondata(null, new u8(0), true);
+                else {
+                  const d = new this.o[cmp]();
+                  d.ondata = (err, dat, final) => { file.ondata(err, dat, final); }
+                  for (const [dat, final] of chks) d.push(dat, final);
+                  if (this.k[0] == chks) this.d = d;
+                }
+              },
+              terminate: () => {
+                if (this.k[0] == chks && this.d.terminate) this.d.terminate();
+              }
+            } as UnzipFile;
+            this.onfile(null, fn, file);
+            i += es;
+          }
+          break;
+        }
+      }
+      if (add) add.push(f ? buf.subarray(0, i - 12 - (b4(buf, i - 12) == 0x8074B50 && 4)) : buf, !!f);
+      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';
+    }
+  }
+
+  /**
+   * Registers a decoder with the stream, allowing for files compressed with
+   * the compression type provided to be expanded correctly
+   * @param decoder The decoder constructor
+   */
+  register(decoder: UnzipDecoderConstructor) {
+    this.o[decoder.compression] = decoder;
+  }
+
+  /**
+   * The handler to call whenever a file is discovered
+   */
+  onfile: UnzipFileHandler;
+}
+
 /**
  * Asynchronously decompresses a ZIP archive
  * @param data The raw compressed ZIP file

+ 0 - 1
tsconfig.esm.json

@@ -2,7 +2,6 @@
   "extends": "./tsconfig.json",
   "compilerOptions": {
     "declaration": false,
-    "moduleResolution": "node",
     "module": "ESNext",
     "outDir": "esm"
   }