ソースを参照

Add streaming ZIP

Arjun Barrett 4 年 前
コミット
3442262c83

+ 14 - 1
README.md

@@ -54,12 +54,25 @@ If you want to load from a CDN in the browser:
 <!--
 You should use either UNPKG or jsDelivr (i.e. only one of the following)
 Note that tree shaking is completely unsupported from the CDN
+You may also want to specify the version, e.g. with [email protected]
 -->
-<script src="https://unpkg.com/fflate"></script>
+<script src="https://unpkg.com/fflate/umd/index.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/fflate/umd/index.js"></script>
 <!-- Now, the global variable fflate contains the library -->
 ```
 
+If your environment doesn't support bundling:
+```js
+// Again, try to import just what you need
+
+// For the browser:
+import * as fflate from 'fflate/esm/browser.js';
+// If for some reason the standard ESM import fails on Node:
+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

+ 34 - 0
docs/README.md

@@ -10,13 +10,19 @@
 * [AsyncGzip](classes/asyncgzip.md)
 * [AsyncInflate](classes/asyncinflate.md)
 * [AsyncUnzlib](classes/asyncunzlib.md)
+* [AsyncZipDeflate](classes/asynczipdeflate.md)
 * [AsyncZlib](classes/asynczlib.md)
+* [DecodeUTF8](classes/decodeutf8.md)
 * [Decompress](classes/decompress.md)
 * [Deflate](classes/deflate.md)
+* [EncodeUTF8](classes/encodeutf8.md)
 * [Gunzip](classes/gunzip.md)
 * [Gzip](classes/gzip.md)
 * [Inflate](classes/inflate.md)
 * [Unzlib](classes/unzlib.md)
+* [Zip](classes/zip.md)
+* [ZipDeflate](classes/zipdeflate.md)
+* [ZipPassThrough](classes/zippassthrough.md)
 * [Zlib](classes/zlib.md)
 
 ### Interfaces
@@ -34,6 +40,8 @@
 * [DeflateOptions](interfaces/deflateoptions.md)
 * [GzipOptions](interfaces/gzipoptions.md)
 * [Unzipped](interfaces/unzipped.md)
+* [ZipAttributes](interfaces/zipattributes.md)
+* [ZipInputFile](interfaces/zipinputfile.md)
 * [ZipOptions](interfaces/zipoptions.md)
 * [Zippable](interfaces/zippable.md)
 * [ZlibOptions](interfaces/zliboptions.md)
@@ -44,7 +52,9 @@
 * [AsyncZippableFile](README.md#asynczippablefile)
 * [FlateCallback](README.md#flatecallback)
 * [FlateStreamHandler](README.md#flatestreamhandler)
+* [StringStreamHandler](README.md#stringstreamhandler)
 * [UnzipCallback](README.md#unzipcallback)
+* [ZipProgressHandler](README.md#zipprogresshandler)
 * [ZippableFile](README.md#zippablefile)
 
 ### Functions
@@ -118,6 +128,18 @@ Handler for data (de)compression streams
 
 ___
 
+### StringStreamHandler
+
+Ƭ  **StringStreamHandler**: (data: string,final: boolean) => void
+
+Handler for string generation streams
+
+**`param`** The string output from the stream processor
+
+**`param`** Whether this is the final block
+
+___
+
 ### UnzipCallback
 
 Ƭ  **UnzipCallback**: (err: Error \| string,data: [Unzipped](interfaces/unzipped.md)) => void
@@ -130,6 +152,18 @@ Callback for asynchronous ZIP decompression
 
 ___
 
+### ZipProgressHandler
+
+Ƭ  **ZipProgressHandler**: (bytesRead: number,bytesOut: number) => void
+
+Callback for ZIP compression progress
+
+**`param`** Any error that occurred
+
+**`param`** The decompressed ZIP archive
+
+___
+
 ### ZippableFile
 
 Ƭ  **ZippableFile**: Uint8Array \| []

+ 155 - 0
docs/classes/asynczipdeflate.md

@@ -0,0 +1,155 @@
+# Class: AsyncZipDeflate
+
+Asynchronous streaming DEFLATE compression for ZIP archives
+
+## Hierarchy
+
+* **AsyncZipDeflate**
+
+## Implements
+
+* [ZipInputFile](../interfaces/zipinputfile.md)
+
+## Index
+
+### Constructors
+
+* [constructor](asynczipdeflate.md#constructor)
+
+### Properties
+
+* [attrs](asynczipdeflate.md#attrs)
+* [compression](asynczipdeflate.md#compression)
+* [crc](asynczipdeflate.md#crc)
+* [filename](asynczipdeflate.md#filename)
+* [flag](asynczipdeflate.md#flag)
+* [ondata](asynczipdeflate.md#ondata)
+* [os](asynczipdeflate.md#os)
+* [size](asynczipdeflate.md#size)
+* [terminate](asynczipdeflate.md#terminate)
+
+### Methods
+
+* [process](asynczipdeflate.md#process)
+* [push](asynczipdeflate.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new AsyncZipDeflate**(`filename`: string, `opts`: [DeflateOptions](../interfaces/deflateoptions.md)): [AsyncZipDeflate](asynczipdeflate.md)
+
+Creates a DEFLATE stream that can be added to ZIP archives
+
+#### Parameters:
+
+Name | Type | Default value | Description |
+------ | ------ | ------ | ------ |
+`filename` | string | - | The filename to associate with this data stream |
+`opts` | [DeflateOptions](../interfaces/deflateoptions.md) | {} | The compression options  |
+
+**Returns:** [AsyncZipDeflate](asynczipdeflate.md)
+
+## Properties
+
+### attrs
+
+• `Optional` **attrs**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[attrs](../interfaces/zipinputfile.md#attrs)*
+
+___
+
+### compression
+
+•  **compression**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[compression](../interfaces/zipinputfile.md#compression)*
+
+___
+
+### crc
+
+•  **crc**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[crc](../interfaces/zipinputfile.md#crc)*
+
+___
+
+### filename
+
+•  **filename**: string
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[filename](../interfaces/zipinputfile.md#filename)*
+
+___
+
+### flag
+
+•  **flag**: 0 \| 1 \| 2 \| 3
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[flag](../interfaces/zipinputfile.md#flag)*
+
+___
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[ondata](../interfaces/zipinputfile.md#ondata)*
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[os](../interfaces/zipinputfile.md#os)*
+
+___
+
+### size
+
+•  **size**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[size](../interfaces/zipinputfile.md#size)*
+
+___
+
+### terminate
+
+•  **terminate**: [AsyncTerminable](../interfaces/asyncterminable.md)
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[terminate](../interfaces/zipinputfile.md#terminate)*
+
+## Methods
+
+### process
+
+▸ **process**(`chunk`: Uint8Array, `final`: boolean): void
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`chunk` | Uint8Array |
+`final` | boolean |
+
+**Returns:** void
+
+___
+
+### push
+
+▸ **push**(`chunk`: Uint8Array, `final?`: boolean): void
+
+Pushes a chunk to be deflated
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`chunk` | Uint8Array | The chunk to push |
+`final?` | boolean | Whether this is the last chunk  |
+
+**Returns:** void

+ 62 - 0
docs/classes/decodeutf8.md

@@ -0,0 +1,62 @@
+# Class: DecodeUTF8
+
+Streaming UTF-8 decoding
+
+## Hierarchy
+
+* **DecodeUTF8**
+
+## Index
+
+### Constructors
+
+* [constructor](decodeutf8.md#constructor)
+
+### Properties
+
+* [ondata](decodeutf8.md#ondata)
+
+### Methods
+
+* [push](decodeutf8.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new DecodeUTF8**(`handler?`: [StringStreamHandler](../README.md#stringstreamhandler)): [DecodeUTF8](decodeutf8.md)
+
+Creates a UTF-8 decoding stream
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`handler?` | [StringStreamHandler](../README.md#stringstreamhandler) |
+
+**Returns:** [DecodeUTF8](decodeutf8.md)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [StringStreamHandler](../README.md#stringstreamhandler)
+
+The handler to call whenever data is available
+
+## Methods
+
+### push
+
+▸ **push**(`chunk`: Uint8Array, `final?`: boolean): void
+
+Pushes a chunk to be decoded from UTF-8 binary
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`chunk` | Uint8Array | The chunk to push |
+`final?` | boolean | Whether this is the last chunk  |
+
+**Returns:** void

+ 62 - 0
docs/classes/encodeutf8.md

@@ -0,0 +1,62 @@
+# Class: EncodeUTF8
+
+Streaming UTF-8 encoding
+
+## Hierarchy
+
+* **EncodeUTF8**
+
+## Index
+
+### Constructors
+
+* [constructor](encodeutf8.md#constructor)
+
+### Properties
+
+* [ondata](encodeutf8.md#ondata)
+
+### Methods
+
+* [push](encodeutf8.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new EncodeUTF8**(`handler?`: [FlateStreamHandler](../README.md#flatestreamhandler)): [EncodeUTF8](encodeutf8.md)
+
+Creates a UTF-8 decoding stream
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`handler?` | [FlateStreamHandler](../README.md#flatestreamhandler) |
+
+**Returns:** [EncodeUTF8](encodeutf8.md)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [FlateStreamHandler](../README.md#flatestreamhandler)
+
+The handler to call whenever data is available
+
+## Methods
+
+### push
+
+▸ **push**(`chunk`: string, `final?`: boolean): void
+
+Pushes a chunk to be encoded to UTF-8
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`chunk` | string | The string data to push |
+`final?` | boolean | Whether this is the last chunk  |
+
+**Returns:** void

+ 80 - 0
docs/classes/zip.md

@@ -0,0 +1,80 @@
+# Class: Zip
+
+A zippable archive to which files can incrementally be added
+
+## Hierarchy
+
+* **Zip**
+
+## Index
+
+### Constructors
+
+* [constructor](zip.md#constructor)
+
+### Properties
+
+* [ondata](zip.md#ondata)
+
+### Methods
+
+* [add](zip.md#add)
+* [end](zip.md#end)
+* [terminate](zip.md#terminate)
+
+## Constructors
+
+### constructor
+
+\+ **new Zip**(): [Zip](zip.md)
+
+Creates an empty ZIP archive to which files can be added
+
+**Returns:** [Zip](zip.md)
+
+## Properties
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+The handler to call whenever data is available
+
+## Methods
+
+### add
+
+▸ **add**(`file`: [ZipInputFile](../interfaces/zipinputfile.md)): void
+
+Adds a file to the ZIP archive
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`file` | [ZipInputFile](../interfaces/zipinputfile.md) | The file stream to add  |
+
+**Returns:** void
+
+___
+
+### end
+
+▸ **end**(): void
+
+Ends the process of adding files and prepares to emit the final chunks.
+This *must* be called after adding all desired files for the resulting
+ZIP file to work properly.
+
+**Returns:** void
+
+___
+
+### terminate
+
+▸ **terminate**(): void
+
+A method to terminate any internal workers used by the stream. Subsequent
+calls to add() will silently fail.
+
+**Returns:** void

+ 147 - 0
docs/classes/zipdeflate.md

@@ -0,0 +1,147 @@
+# Class: ZipDeflate
+
+Streaming DEFLATE compression for ZIP archives. Prefer using AsyncZipDeflate
+for better performance
+
+## Hierarchy
+
+* **ZipDeflate**
+
+## Implements
+
+* [ZipInputFile](../interfaces/zipinputfile.md)
+
+## Index
+
+### Constructors
+
+* [constructor](zipdeflate.md#constructor)
+
+### Properties
+
+* [attrs](zipdeflate.md#attrs)
+* [compression](zipdeflate.md#compression)
+* [crc](zipdeflate.md#crc)
+* [filename](zipdeflate.md#filename)
+* [flag](zipdeflate.md#flag)
+* [ondata](zipdeflate.md#ondata)
+* [os](zipdeflate.md#os)
+* [size](zipdeflate.md#size)
+
+### Methods
+
+* [process](zipdeflate.md#process)
+* [push](zipdeflate.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new ZipDeflate**(`filename`: string, `opts`: [DeflateOptions](../interfaces/deflateoptions.md)): [ZipDeflate](zipdeflate.md)
+
+Creates a DEFLATE stream that can be added to ZIP archives
+
+#### Parameters:
+
+Name | Type | Default value | Description |
+------ | ------ | ------ | ------ |
+`filename` | string | - | The filename to associate with this data stream |
+`opts` | [DeflateOptions](../interfaces/deflateoptions.md) | {} | The compression options  |
+
+**Returns:** [ZipDeflate](zipdeflate.md)
+
+## Properties
+
+### attrs
+
+• `Optional` **attrs**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[attrs](../interfaces/zipinputfile.md#attrs)*
+
+___
+
+### compression
+
+•  **compression**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[compression](../interfaces/zipinputfile.md#compression)*
+
+___
+
+### crc
+
+•  **crc**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[crc](../interfaces/zipinputfile.md#crc)*
+
+___
+
+### filename
+
+•  **filename**: string
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[filename](../interfaces/zipinputfile.md#filename)*
+
+___
+
+### flag
+
+•  **flag**: 0 \| 1 \| 2 \| 3
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[flag](../interfaces/zipinputfile.md#flag)*
+
+___
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[ondata](../interfaces/zipinputfile.md#ondata)*
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[os](../interfaces/zipinputfile.md#os)*
+
+___
+
+### size
+
+•  **size**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[size](../interfaces/zipinputfile.md#size)*
+
+## Methods
+
+### process
+
+▸ **process**(`chunk`: Uint8Array, `final`: boolean): void
+
+#### Parameters:
+
+Name | Type |
+------ | ------ |
+`chunk` | Uint8Array |
+`final` | boolean |
+
+**Returns:** void
+
+___
+
+### push
+
+▸ **push**(`chunk`: Uint8Array, `final?`: boolean): void
+
+Pushes a chunk to be deflated
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`chunk` | Uint8Array | The chunk to push |
+`final?` | boolean | Whether this is the last chunk  |
+
+**Returns:** void

+ 113 - 0
docs/classes/zippassthrough.md

@@ -0,0 +1,113 @@
+# Class: ZipPassThrough
+
+A pass-through stream to keep data uncompressed in a ZIP archive.
+
+## Hierarchy
+
+* **ZipPassThrough**
+
+## Implements
+
+* [ZipInputFile](../interfaces/zipinputfile.md)
+
+## Index
+
+### Constructors
+
+* [constructor](zippassthrough.md#constructor)
+
+### Properties
+
+* [attrs](zippassthrough.md#attrs)
+* [crc](zippassthrough.md#crc)
+* [filename](zippassthrough.md#filename)
+* [ondata](zippassthrough.md#ondata)
+* [os](zippassthrough.md#os)
+* [size](zippassthrough.md#size)
+
+### Methods
+
+* [push](zippassthrough.md#push)
+
+## Constructors
+
+### constructor
+
+\+ **new ZipPassThrough**(`filename`: string): [ZipPassThrough](zippassthrough.md)
+
+Creates a pass-through stream that can be added to ZIP archives
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`filename` | string | The filename to associate with this data stream  |
+
+**Returns:** [ZipPassThrough](zippassthrough.md)
+
+## Properties
+
+### attrs
+
+• `Optional` **attrs**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[attrs](../interfaces/zipinputfile.md#attrs)*
+
+___
+
+### crc
+
+•  **crc**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[crc](../interfaces/zipinputfile.md#crc)*
+
+___
+
+### filename
+
+•  **filename**: string
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[filename](../interfaces/zipinputfile.md#filename)*
+
+___
+
+### ondata
+
+•  **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[ondata](../interfaces/zipinputfile.md#ondata)*
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[os](../interfaces/zipinputfile.md#os)*
+
+___
+
+### size
+
+•  **size**: number
+
+*Implementation of [ZipInputFile](../interfaces/zipinputfile.md).[size](../interfaces/zipinputfile.md#size)*
+
+## Methods
+
+### push
+
+▸ **push**(`chunk`: Uint8Array, `final?`: boolean): void
+
+Pushes a chunk to be added. If you are subclassing this with a custom
+compression algorithm, note that you must push data from the source
+file only, pre-compression.
+
+#### Parameters:
+
+Name | Type | Description |
+------ | ------ | ------ |
+`chunk` | Uint8Array | The chunk to push |
+`final?` | boolean | Whether this is the last chunk  |
+
+**Returns:** void

+ 1 - 1
docs/interfaces/asyncgzipoptions.md

@@ -89,4 +89,4 @@ ___
 *Inherited from [GzipOptions](gzipoptions.md).[mtime](gzipoptions.md#mtime)*
 
 When the file was last modified. Defaults to the current time.
-If you're using GZIP, set this to 0 to avoid revealing a modification date entirely.
+Set this to 0 to avoid revealing a modification date entirely.

+ 52 - 1
docs/interfaces/asynczipoptions.md

@@ -6,7 +6,7 @@ Options for asynchronously creating a ZIP archive
 
 * [AsyncDeflateOptions](asyncdeflateoptions.md)
 
-* {}
+* [ZipAttributes](zipattributes.md)
 
   ↳ **AsyncZipOptions**
 
@@ -14,12 +14,41 @@ Options for asynchronously creating a ZIP archive
 
 ### Properties
 
+* [attrs](asynczipoptions.md#attrs)
 * [consume](asynczipoptions.md#consume)
 * [level](asynczipoptions.md#level)
 * [mem](asynczipoptions.md#mem)
+* [mtime](asynczipoptions.md#mtime)
+* [os](asynczipoptions.md#os)
 
 ## Properties
 
+### attrs
+
+• `Optional` **attrs**: number
+
+*Inherited from [ZipAttributes](zipattributes.md).[attrs](zipattributes.md#attrs)*
+
+The file's attributes. These are traditionally somewhat complicated
+and platform-dependent, so using them is scarcely necessary. However,
+here is a representation of what this is, bit by bit:
+
+`TTTTugtrwxrwxrwx0000000000ADVSHR`
+
+T = file type (rarely useful)
+
+u = setuid, g = setgid, t = sticky
+
+rwx = user permissions, rwx = group permissions, rwx = other permissions
+
+0000000000 = unused
+
+A = archive, D = directory, V = volume label, S = system file, H = hidden, R = read-only
+
+If you want to set the Unix permissions, for instance, just bit shift by 16, e.g. 0644 << 16
+
+___
+
 ### consume
 
 • `Optional` **consume**: boolean
@@ -66,3 +95,25 @@ It is recommended not to lower the value below 4, since that tends to hurt perfo
 In addition, values above 8 tend to help very little on most data and can even hurt performance.
 
 The default value is automatically determined based on the size of the input data.
+
+___
+
+### mtime
+
+• `Optional` **mtime**: GzipOptions[\"mtime\"]
+
+*Inherited from [ZipAttributes](zipattributes.md).[mtime](zipattributes.md#mtime)*
+
+When the file was last modified. Defaults to the current time.
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+*Inherited from [ZipAttributes](zipattributes.md).[os](zipattributes.md#os)*
+
+The operating system of origin for this file. The value is defined
+by PKZIP's APPNOTE.txt, section 4.4.2.2. For example, 0 (the default)
+is MS/DOS, 3 is UNIX, 19 is macOS.

+ 6 - 2
docs/interfaces/asynczippable.md

@@ -4,6 +4,10 @@ The complete directory structure of an asynchronously ZIPpable archive
 
 ## Hierarchy
 
-* {}
+* **AsyncZippable**
 
-  ↳ **AsyncZippable**
+## Indexable
+
+▪ [path: string]: [AsyncZippable](asynczippable.md) \| [AsyncZippableFile](../README.md#asynczippablefile)
+
+The complete directory structure of an asynchronously ZIPpable archive

+ 1 - 1
docs/interfaces/gzipoptions.md

@@ -73,4 +73,4 @@ ___
 • `Optional` **mtime**: Date \| string \| number
 
 When the file was last modified. Defaults to the current time.
-If you're using GZIP, set this to 0 to avoid revealing a modification date entirely.
+Set this to 0 to avoid revealing a modification date entirely.

+ 7 - 2
docs/interfaces/unzipped.md

@@ -5,6 +5,11 @@ and the file is the value
 
 ## Hierarchy
 
-* {}
+* **Unzipped**
 
-  ↳ **Unzipped**
+## Indexable
+
+▪ [path: string]: Uint8Array
+
+An unzipped archive. The full path of each file is used as the key,
+and the file is the value

+ 63 - 0
docs/interfaces/zipattributes.md

@@ -0,0 +1,63 @@
+# Interface: ZipAttributes
+
+Attributes for files added to a ZIP archive object
+
+## Hierarchy
+
+* **ZipAttributes**
+
+  ↳ [ZipOptions](zipoptions.md)
+
+  ↳ [AsyncZipOptions](asynczipoptions.md)
+
+  ↳ [ZipInputFile](zipinputfile.md)
+
+## Index
+
+### Properties
+
+* [attrs](zipattributes.md#attrs)
+* [mtime](zipattributes.md#mtime)
+* [os](zipattributes.md#os)
+
+## Properties
+
+### attrs
+
+• `Optional` **attrs**: number
+
+The file's attributes. These are traditionally somewhat complicated
+and platform-dependent, so using them is scarcely necessary. However,
+here is a representation of what this is, bit by bit:
+
+`TTTTugtrwxrwxrwx0000000000ADVSHR`
+
+T = file type (rarely useful)
+
+u = setuid, g = setgid, t = sticky
+
+rwx = user permissions, rwx = group permissions, rwx = other permissions
+
+0000000000 = unused
+
+A = archive, D = directory, V = volume label, S = system file, H = hidden, R = read-only
+
+If you want to set the Unix permissions, for instance, just bit shift by 16, e.g. 0644 << 16
+
+___
+
+### mtime
+
+• `Optional` **mtime**: GzipOptions[\"mtime\"]
+
+When the file was last modified. Defaults to the current time.
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+The operating system of origin for this file. The value is defined
+by PKZIP's APPNOTE.txt, section 4.4.2.2. For example, 0 (the default)
+is MS/DOS, 3 is UNIX, 19 is macOS.

+ 164 - 0
docs/interfaces/zipinputfile.md

@@ -0,0 +1,164 @@
+# Interface: ZipInputFile
+
+A stream that can be used to create a file in a ZIP archive
+
+## Hierarchy
+
+* [ZipAttributes](zipattributes.md)
+
+  ↳ **ZipInputFile**
+
+## Implemented by
+
+* [AsyncZipDeflate](../classes/asynczipdeflate.md)
+* [ZipDeflate](../classes/zipdeflate.md)
+* [ZipPassThrough](../classes/zippassthrough.md)
+
+## Index
+
+### Properties
+
+* [attrs](zipinputfile.md#attrs)
+* [compression](zipinputfile.md#compression)
+* [crc](zipinputfile.md#crc)
+* [filename](zipinputfile.md#filename)
+* [flag](zipinputfile.md#flag)
+* [mtime](zipinputfile.md#mtime)
+* [ondata](zipinputfile.md#ondata)
+* [os](zipinputfile.md#os)
+* [size](zipinputfile.md#size)
+* [terminate](zipinputfile.md#terminate)
+
+## Properties
+
+### attrs
+
+• `Optional` **attrs**: number
+
+*Inherited from [ZipAttributes](zipattributes.md).[attrs](zipattributes.md#attrs)*
+
+The file's attributes. These are traditionally somewhat complicated
+and platform-dependent, so using them is scarcely necessary. However,
+here is a representation of what this is, bit by bit:
+
+`TTTTugtrwxrwxrwx0000000000ADVSHR`
+
+T = file type (rarely useful)
+
+u = setuid, g = setgid, t = sticky
+
+rwx = user permissions, rwx = group permissions, rwx = other permissions
+
+0000000000 = unused
+
+A = archive, D = directory, V = volume label, S = system file, H = hidden, R = read-only
+
+If you want to set the Unix permissions, for instance, just bit shift by 16, e.g. 0644 << 16
+
+___
+
+### compression
+
+• `Optional` **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
+
+___
+
+### crc
+
+•  **crc**: number
+
+A CRC of the original file contents. This attribute may be invalid after
+the file is added to the ZIP archive; it must be correct only before the
+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
+
+___
+
+### filename
+
+•  **filename**: string
+
+The filename to associate with the data provided to this stream. If you
+want a file in a subdirectory, use forward slashes as a separator (e.g.
+`directory/filename.ext`). This will still work on Windows.
+
+___
+
+### flag
+
+• `Optional` **flag**: 0 \| 1 \| 2 \| 3
+
+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.
+
+___
+
+### mtime
+
+• `Optional` **mtime**: GzipOptions[\"mtime\"]
+
+*Inherited from [ZipAttributes](zipattributes.md).[mtime](zipattributes.md#mtime)*
+
+When the file was last modified. Defaults to the current time.
+
+___
+
+### ondata
+
+• `Optional` **ondata**: [AsyncFlateStreamHandler](../README.md#asyncflatestreamhandler)
+
+The handler to be called when data is added. After passing this stream to
+the ZIP file object, this handler will always be defined. To call it:
+
+`stream.ondata(error, chunk, final)`
+
+error = any error that occurred (null if there was no error)
+
+chunk = a Uint8Array of the data that was added (null if there was an
+error)
+
+final = boolean, whether this is the final chunk in the stream
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+*Inherited from [ZipAttributes](zipattributes.md).[os](zipattributes.md#os)*
+
+The operating system of origin for this file. The value is defined
+by PKZIP's APPNOTE.txt, section 4.4.2.2. For example, 0 (the default)
+is MS/DOS, 3 is UNIX, 19 is macOS.
+
+___
+
+### size
+
+•  **size**: number
+
+The size of the file in bytes. This attribute may be invalid after
+the file is added to the ZIP archive; it must be correct only before the
+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
+
+___
+
+### terminate
+
+• `Optional` **terminate**: [AsyncTerminable](asyncterminable.md)
+
+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
+processed if you have clean-up logic.

+ 52 - 1
docs/interfaces/zipoptions.md

@@ -6,7 +6,7 @@ Options for creating a ZIP archive
 
 * [DeflateOptions](deflateoptions.md)
 
-* {}
+* [ZipAttributes](zipattributes.md)
 
   ↳ **ZipOptions**
 
@@ -14,11 +14,40 @@ Options for creating a ZIP archive
 
 ### Properties
 
+* [attrs](zipoptions.md#attrs)
 * [level](zipoptions.md#level)
 * [mem](zipoptions.md#mem)
+* [mtime](zipoptions.md#mtime)
+* [os](zipoptions.md#os)
 
 ## Properties
 
+### attrs
+
+• `Optional` **attrs**: number
+
+*Inherited from [ZipAttributes](zipattributes.md).[attrs](zipattributes.md#attrs)*
+
+The file's attributes. These are traditionally somewhat complicated
+and platform-dependent, so using them is scarcely necessary. However,
+here is a representation of what this is, bit by bit:
+
+`TTTTugtrwxrwxrwx0000000000ADVSHR`
+
+T = file type (rarely useful)
+
+u = setuid, g = setgid, t = sticky
+
+rwx = user permissions, rwx = group permissions, rwx = other permissions
+
+0000000000 = unused
+
+A = archive, D = directory, V = volume label, S = system file, H = hidden, R = read-only
+
+If you want to set the Unix permissions, for instance, just bit shift by 16, e.g. 0644 << 16
+
+___
+
 ### level
 
 • `Optional` **level**: 0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8 \| 9
@@ -54,3 +83,25 @@ It is recommended not to lower the value below 4, since that tends to hurt perfo
 In addition, values above 8 tend to help very little on most data and can even hurt performance.
 
 The default value is automatically determined based on the size of the input data.
+
+___
+
+### mtime
+
+• `Optional` **mtime**: GzipOptions[\"mtime\"]
+
+*Inherited from [ZipAttributes](zipattributes.md).[mtime](zipattributes.md#mtime)*
+
+When the file was last modified. Defaults to the current time.
+
+___
+
+### os
+
+• `Optional` **os**: number
+
+*Inherited from [ZipAttributes](zipattributes.md).[os](zipattributes.md#os)*
+
+The operating system of origin for this file. The value is defined
+by PKZIP's APPNOTE.txt, section 4.4.2.2. For example, 0 (the default)
+is MS/DOS, 3 is UNIX, 19 is macOS.

+ 6 - 2
docs/interfaces/zippable.md

@@ -4,6 +4,10 @@ The complete directory structure of a ZIPpable archive
 
 ## Hierarchy
 
-* {}
+* **Zippable**
 
-  ↳ **Zippable**
+## Indexable
+
+▪ [path: string]: [Zippable](zippable.md) \| [ZippableFile](../README.md#zippablefile)
+
+The complete directory structure of a ZIPpable archive

+ 10 - 0
package.json

@@ -11,6 +11,13 @@
     "./lib/node-worker.js": "./lib/worker.js",
     "./esm/index.mjs": "./esm/browser.js"
   },
+  "exports": {
+    ".": {
+      "node": "./esm/index.mjs",
+      "require": "./lib/index.js",
+      "default": "./esm/browser.js"
+    }
+  },
   "targets": {
     "main": false,
     "module": false,
@@ -82,5 +89,8 @@
     "react": "preact/compat",
     "react-dom": "preact/compat",
     "react-dom/test-utils": "preact/test-utils"
+  },
+  "dependencies": {
+    "@msgpack/msgpack": "^2.3.0"
   }
 }

+ 608 - 89
src/index.ts

@@ -544,7 +544,7 @@ const et = /*#__PURE__*/new u8(0);
 // compresses data into a raw DEFLATE buffer
 const dflt = (dat: Uint8Array, lvl: number, plvl: number, pre: number, post: number, lst: 0 | 1) => {
   const s = dat.length;
-  const o = new u8(pre + s + 5 * (1 + Math.floor(s / 7000)) + post);
+  const o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post);
   // writing to this writes to the output buffer
   const w = o.subarray(pre, o.length - post);
   let pos = 0;
@@ -649,7 +649,7 @@ const dflt = (dat: Uint8Array, lvl: number, plvl: number, pre: number, post: num
     }
     pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos);
     // this is the easiest way to avoid needing to maintain state
-    if (!lst) pos = wfblk(w, pos, et);
+    if (!lst && pos & 7) pos = wfblk(w, pos + 1, et);
   }
   return slc(o, 0, pre + shft(pos) + post);
 }
@@ -742,7 +742,7 @@ export interface DeflateOptions {
 export interface GzipOptions extends DeflateOptions {
   /**
    * When the file was last modified. Defaults to the current time.
-   * If you're using GZIP, set this to 0 to avoid revealing a modification date entirely.
+   * Set this to 0 to avoid revealing a modification date entirely.
    */
   mtime?: Date | string | number;
   /**
@@ -840,11 +840,11 @@ const dopt = (dat: Uint8Array, opt: DeflateOptions, pre: number, post: number, s
   dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : (12 + opt.mem), pre, post, !st as unknown as 0 | 1);
 
 // Walmart object spread
-const mrg = <T extends {}>(a: T, b: T) => {
-  const o = {} as T;
+const mrg = <A, B>(a: A, b: B) => {
+  const o = {} as Record<string, unknown>;
   for (const k in a) o[k] = a[k];
   for (const k in b) o[k] = b[k];
-  return o;
+  return o as A & B;
 }
 
 // worker clone
@@ -1886,15 +1886,53 @@ export function decompressSync(data: Uint8Array, out?: Uint8Array) {
       : unzlibSync(data, out);
 }
 
+/**
+ * Attributes for files added to a ZIP archive object
+ */
+export interface ZipAttributes {
+  /**
+   * The operating system of origin for this file. The value is defined
+   * by PKZIP's APPNOTE.txt, section 4.4.2.2. For example, 0 (the default)
+   * is MS/DOS, 3 is UNIX, 19 is macOS.
+   */
+  os?: number;
+
+  /**
+   * The file's attributes. These are traditionally somewhat complicated
+   * and platform-dependent, so using them is scarcely necessary. However,
+   * here is a representation of what this is, bit by bit:
+   * 
+   * `TTTTugtrwxrwxrwx0000000000ADVSHR`
+   * 
+   * T = file type (rarely useful)
+   * 
+   * u = setuid, g = setgid, t = sticky
+   * 
+   * rwx = user permissions, rwx = group permissions, rwx = other permissions
+   * 
+   * 0000000000 = unused
+   * 
+   * A = archive, D = directory, V = volume label, S = system file, H = hidden, R = read-only
+   * 
+   * If you want to set the Unix permissions, for instance, just bit shift by 16, e.g. 0644 << 16
+   */
+  attrs?: number;
+
+  /**
+   * When the file was last modified. Defaults to the current time.
+   */
+  mtime?: GzipOptions['mtime'];
+}
+
 /**
  * Options for creating a ZIP archive
  */
-export interface ZipOptions extends DeflateOptions, Pick<GzipOptions, 'mtime'> {}
+export interface ZipOptions extends DeflateOptions, ZipAttributes {}
 
 /**
  * Options for asynchronously creating a ZIP archive
  */
-export interface AsyncZipOptions extends AsyncDeflateOptions, Pick<AsyncGzipOptions, 'mtime'> {}
+export interface AsyncZipOptions extends AsyncDeflateOptions, ZipAttributes {}
 
 /**
  * Options for asynchronously expanding a ZIP archive
@@ -1914,18 +1952,31 @@ export type AsyncZippableFile = Uint8Array | [Uint8Array, AsyncZipOptions];
 /**
  * The complete directory structure of a ZIPpable archive
  */
-export interface Zippable extends Record<string, Zippable | ZippableFile> {}
+export interface Zippable {
+  [path: string]: Zippable | ZippableFile;
+}
 
 /**
  * The complete directory structure of an asynchronously ZIPpable archive
  */
-export interface AsyncZippable extends Record<string, AsyncZippable | AsyncZippableFile> {}
+export interface AsyncZippable {
+  [path: string]: AsyncZippable | AsyncZippableFile;
+}
 
 /**
  * An unzipped archive. The full path of each file is used as the key,
  * and the file is the value
  */
-export interface Unzipped extends Record<string, Uint8Array> {}
+export interface Unzipped {
+  [path: string]: Uint8Array
+}
+
+/**
+ * Handler for string generation streams
+ * @param data The string output from the stream processor
+ * @param final Whether this is the final block
+ */
+export type StringStreamHandler = (data: string, final: boolean) => void;
 
 /**
  * Callback for asynchronous ZIP decompression
@@ -1934,6 +1985,13 @@ export interface Unzipped extends Record<string, Uint8Array> {}
  */
 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
+ */
+export type ZipProgressHandler = (bytesRead: number, bytesOut: number) => void;
+
 // flattened Zippable
 type FlatZippable<A extends boolean> = Record<string, [Uint8Array, (A extends true ? AsyncZipOptions : ZipOptions)]>;
 
@@ -1947,6 +2005,102 @@ const fltn = <A extends boolean>(d: A extends true ? AsyncZippable : Zippable, p
   }
 }
 
+// text encoder
+const te = typeof TextEncoder != 'undefined' && new TextEncoder();
+// text decoder
+const td = typeof TextDecoder != 'undefined' && new TextDecoder();
+// text decoder stream
+let tds = 0;
+try {
+  td.decode(et, { stream: true });
+  tds = 1;
+} catch(e) {}
+
+// decode UTF8
+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 (!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,
+      r += String.fromCharCode(55296 | (c >> 10), 56320 | (c & 1023));
+    } else if (eb & 1) r += String.fromCharCode((c & 31) << 6 | (d[i++] & 63));
+    else r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | (d[i++] & 63));
+  }
+}
+
+/**
+ * Streaming UTF-8 decoding
+ */
+export class DecodeUTF8 {
+  private p: Uint8Array;
+  private t: TextDecoder;
+  /**
+   * Creates a UTF-8 decoding stream
+   * @param opts The compression options
+   * @param cb The callback to call whenever data is deflated
+   */
+  constructor(handler?: StringStreamHandler) {
+    this.ondata = handler;
+    if (tds) this.t = new TextDecoder();
+    else this.p = et;
+  }
+
+  /**
+   * Pushes a chunk to be decoded from UTF-8 binary
+   * @param chunk The chunk to push
+   * @param final Whether this is the last chunk
+   */
+  push(chunk: Uint8Array, final?: boolean) {
+    if (!this.ondata) throw 'no callback';
+    if (!final) final = false;
+    if (this.t) return this.ondata(this.t.decode(chunk, { stream: !final }), final);
+    const dat = new u8(this.p.length + chunk.length);
+    dat.set(this.p);
+    dat.set(chunk, this.p.length);
+    const [ch, np] = dutf8(dat);
+    if (final && np.length) throw 'invalid utf-8 data';
+    this.p = np;
+    this.ondata(ch, final);
+  }
+
+  /**
+   * The handler to call whenever data is available
+   */
+  ondata: StringStreamHandler;
+}
+
+/**
+ * Streaming UTF-8 encoding
+ */
+export class EncodeUTF8 {
+  /**
+   * Creates a UTF-8 decoding stream
+   * @param opts The compression options
+   * @param cb The callback to call whenever data is deflated
+   */
+  constructor(handler?: FlateStreamHandler) {
+    this.ondata = handler;
+  }
+
+  /**
+   * Pushes a chunk to be encoded to UTF-8
+   * @param chunk The string data to push
+   * @param final Whether this is the last chunk
+   */
+  push(chunk: string, final?: boolean) {
+    if (!this.ondata) throw 'no callback';
+    this.ondata(strToU8(chunk), final || false);
+  }
+
+  /**
+   * The handler to call whenever data is available
+   */
+  ondata: FlateStreamHandler;
+}
+
 /**
  * Converts a string into a Uint8Array for use with compression/decompression methods
  * @param str The string to encode
@@ -1955,9 +2109,14 @@ const fltn = <A extends boolean>(d: A extends true ? AsyncZippable : Zippable, p
  * @returns The string encoded in UTF-8/Latin-1 binary
  */
 export function strToU8(str: string, latin1?: boolean): Uint8Array {
+  if (latin1) {
+    const ar = new u8(str.length);
+    for (let i = 0; i < str.length; ++i) ar[i] = str.charCodeAt(i);
+    return ar;
+  }
+  if (te) return te.encode(str);
   const l = str.length;
-  if (!latin1 && typeof TextEncoder != 'undefined') return new TextEncoder().encode(str);
-  let ar = new u8(str.length + (str.length >>> 1));
+  let ar = new u8(str.length + (str.length >> 1));
   let ai = 0;
   const w = (v: number) => { ar[ai++] = v; };
   for (let i = 0; i < l; ++i) {
@@ -1985,20 +2144,22 @@ export function strToU8(str: string, latin1?: boolean): Uint8Array {
  * @returns The original UTF-8/Latin-1 string
  */
 export function strFromU8(dat: Uint8Array, latin1?: boolean) {
-  let r = '';
-  if (!latin1 && typeof TextDecoder != 'undefined') return new TextDecoder().decode(dat);
-  for (let i = 0; i < dat.length;) {
-    let c = dat[i++];
-    if (c < 128 || latin1) r += String.fromCharCode(c);
-    else if (c < 224) r += String.fromCharCode((c & 31) << 6 | (dat[i++] & 63));
-    else if (c < 240) r += String.fromCharCode((c & 15) << 12 | (dat[i++] & 63) << 6 | (dat[i++] & 63));
-    else
-      c = ((c & 15) << 18 | (dat[i++] & 63) << 12 | (dat[i++] & 63) << 6 | (dat[i++] & 63)) - 65536,
-      r += String.fromCharCode(55296 | (c >> 10), 56320 | (c & 1023));
-  }
-  return r;
+  if (latin1) {
+    let r = '';
+    for (let i = 0; i < dat.length; i += 16384)
+      r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
+    return r;
+  } else if (td) return td.decode(dat)
+  else {
+    const [out, ext] = dutf8(dat);
+    if (ext.length) throw 'invalid utf-8 data';
+    return out;
+  } 
 };
 
+// deflate bit flag
+const dbf = (l: number) => l == 1 ? 3 : l < 6 ? 2 : l == 9 ? 1 : 0;
+
 // skip local zip header
 const slzh = (d: Uint8Array, b: number) => b + 30 + b2(d, b + 26) + b2(d, b + 28);
 
@@ -2015,26 +2176,43 @@ const z64e = (d: Uint8Array, b: number) => {
   return [b4(d, b + 12), b4(d, b + 4), b4(d, b + 20)] as const;
 }
 
+// zip header file
+type ZHF = Omit<ZipInputFile, 'terminate' | 'ondata' | 'filename'>;
+
+
 // write zip header
-const wzh = (d: Uint8Array, b: number, c: number, cmp: Uint8Array, su: number, fn: Uint8Array, u: boolean, o: ZipOptions, ce: number | null, t: number) => {
-  const fl = fn.length, l = cmp.length;
+const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c?: number, ce?: number) => {
+  const fl = fn.length;
   wbytes(d, b, ce != null ? 0x2014B50 : 0x4034B50), b += 4;
-  if (ce != null) d[b] = 20, b += 2;
+  if (ce != null) d[b++] = 20, d[b++] = f.os;
   d[b] = 20, b += 2; // spec compliance? what's that?
-  d[b++] = (t == 8 && (o.level == 1 ? 6 : o.level < 6 ? 4 : o.level == 9 ? 2 : 0)), d[b++] = u && 8;
-  d[b] = t, b += 2;
-  const dt = new Date(o.mtime || Date.now()), y = dt.getFullYear() - 1980;
+  d[b++] = (f.flag << 1) | (c == null && 8), d[b++] = u && 8;
+  d[b++] = f.compression & 255, d[b++] = f.compression >> 8;
+  const dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980;
   if (y < 0 || y > 119) throw 'date not in range 1980-2099';
-  wbytes(d, b, ((y << 24) * 2) | ((dt.getMonth() + 1) << 21) | (dt.getDate() << 16) | (dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() >>> 1));
-  b += 4;
-  wbytes(d, b, c);
-  wbytes(d, b + 4, l);
-  wbytes(d, b + 8, su);
-  wbytes(d, b + 12, fl), b += 16; // skip extra field, comment
-  if (ce != null) wbytes(d, b += 10, ce), b += 4;
+  wbytes(d, b, ((y << 24) * 2) | ((dt.getMonth() + 1) << 21) | (dt.getDate() << 16) | (dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() >>> 1)), b += 4;
+  if (c != null) {
+    wbytes(d, b, f.crc);
+    wbytes(d, b + 4, c);
+    wbytes(d, b + 8, f.size);
+  }
+  wbytes(d, b + 12, fl), b += 16;
+  if (ce != null) {
+    wbytes(d, b + 6, f.attrs);
+    wbytes(d, b + 10, ce), b += 14;
+  }
   d.set(fn, b);
-  b += fl;
-  if (ce == null) d.set(cmp, b);
+  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)
@@ -2046,30 +2224,367 @@ const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => {
   wbytes(o, b + 16, e);
 }
 
-// internal zip data
-type AsyncZipDat = {
+/**
+ * A stream that can be used to create a file in a ZIP archive
+ */
+export interface ZipInputFile extends ZipAttributes {
+  /**
+   * The filename to associate with the data provided to this stream. If you
+   * want a file in a subdirectory, use forward slashes as a separator (e.g.
+   * `directory/filename.ext`). This will still work on Windows.
+   */
+  filename: string;
+
+  /**
+   * The size of the file in bytes. This attribute may be invalid after
+   * the file is added to the ZIP archive; it must be correct only before the
+   * 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
+   */
+  size: number;
+
+  /**
+   * A CRC of the original file contents. This attribute may be invalid after
+   * the file is added to the ZIP archive; it must be correct only before the
+   * 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
+   */
+  crc: 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
+   */
+  compression?: 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.
+   */
+  flag?: 0 | 1 | 2 | 3;
+
+  /**
+   * The handler to be called when data is added. After passing this stream to
+   * the ZIP file object, this handler will always be defined. To call it:
+   * 
+   * `stream.ondata(error, chunk, final)`
+   * 
+   * error = any error that occurred (null if there was no error)
+   * 
+   * chunk = a Uint8Array of the data that was added (null if there was an
+   * error)
+   * 
+   * final = boolean, whether this is the final chunk in the stream
+   */
+  ondata?: AsyncFlateStreamHandler;
+  
+  /**
+   * 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
+   * processed if you have clean-up logic.
+   */
+  terminate?: AsyncTerminable;
+}
+
+type AsyncZipDat = ZHF & {
   // compressed data
-  d: Uint8Array;
-  // uncompressed length
-  m: number;
-  // type (0 = uncompressed, 8 = DEFLATE)
-  t: number;
-  // filename as Uint8Array
-  n: Uint8Array;
-  // Unicode filename
+  c: Uint8Array;
+  // filename
+  f: Uint8Array;
+  // unicode
   u: boolean;
-  // CRC32
-  c: number;
-  // zip options
-  p: ZipOptions;
 };
 
 type ZipDat = AsyncZipDat & {
-  // total offset
+  // offset
   o: number;
 }
 
-// TODO: Support streams as ZIP input
+/**
+ * A pass-through stream to keep data uncompressed in a ZIP archive.
+ */
+export class ZipPassThrough implements ZipInputFile {
+  filename: string;
+  crc: number;
+  size: number;
+  os?: number;
+  attrs?: number;
+  ondata: AsyncFlateStreamHandler;
+  private c: CRCV;
+
+  /**
+   * Creates a pass-through stream that can be added to ZIP archives
+   * @param filename The filename to associate with this data stream
+   */
+  constructor(filename: string) {
+    this.filename = filename;
+    this.c = crc();
+    this.size = 0;
+  }
+
+  /**
+   * Processes a chunk and pushes to the output stream. You can override this
+   * method in a subclass for custom behavior, but by default this passes
+   * the data through. You must call this.ondata(err, chunk, final) at some
+   * point in this method.
+   * @param chunk The chunk to process
+   * @param final Whether this is the last chunk
+   */
+  protected process(chunk: Uint8Array, final: boolean) {
+    this.ondata(null, chunk, final);
+  }
+
+  /**
+   * Pushes a chunk to be added. If you are subclassing this with a custom
+   * compression algorithm, note that you must push data from the source
+   * file only, pre-compression.
+   * @param chunk The chunk to push
+   * @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';
+    this.c.p(chunk);
+    this.size += chunk.length;
+    if (final) this.crc = this.c.d();
+    this.process(chunk, final || false);
+  }
+}
+
+// I don't extend because TypeScript extension adds 1kB of runtime bloat
+
+/**
+ * Streaming DEFLATE compression for ZIP archives. Prefer using AsyncZipDeflate
+ * for better performance
+ */
+export class ZipDeflate implements ZipInputFile {
+  filename: string;
+  crc: number;
+  size: number;
+  compression: number;
+  flag: 0 | 1 | 2 | 3;
+  os?: number;
+  attrs?: number;
+  ondata: AsyncFlateStreamHandler;
+  private d: Deflate;
+
+  /**
+   * Creates a DEFLATE stream that can be added to ZIP archives
+   * @param filename The filename to associate with this data stream
+   * @param opts The compression options
+   */
+  constructor(filename: string, opts: DeflateOptions = {}) {
+    ZipPassThrough.call(this, filename);
+    this.d = new Deflate(opts, (dat, final) => {
+      this.ondata(null, dat, final);
+    });
+    this.compression = 8;
+    this.flag = dbf(opts.level);
+  }
+  
+  process(chunk: Uint8Array, final: boolean) {
+    try {
+      this.d.push(chunk, final);
+    } catch(e) {
+      this.ondata(e, null, final);
+    }
+  }
+
+  /**
+   * Pushes a chunk to be deflated
+   * @param chunk The chunk to push
+   * @param final Whether this is the last chunk
+   */
+  push(chunk: Uint8Array, final?: boolean) {
+    ZipPassThrough.prototype.push.call(this, chunk, final);
+  }
+}
+
+/**
+ * Asynchronous streaming DEFLATE compression for ZIP archives
+ */
+export class AsyncZipDeflate implements ZipInputFile {
+  filename: string;
+  crc: number;
+  size: number;
+  compression: number;
+  flag: 0 | 1 | 2 | 3;
+  os?: number;
+  attrs?: number;
+  ondata: AsyncFlateStreamHandler;
+  private d: AsyncDeflate;
+  terminate: AsyncTerminable;
+
+  /**
+   * Creates a DEFLATE stream that can be added to ZIP archives
+   * @param filename The filename to associate with this data stream
+   * @param opts The compression options
+   */
+  constructor(filename: string, opts: DeflateOptions = {}) {
+    ZipPassThrough.call(this, filename);
+    this.d = new AsyncDeflate(opts, (err, dat, final) => {
+      this.ondata(err, dat, final);
+    });
+    this.compression = 8;
+    this.flag = dbf(opts.level);
+    this.terminate = this.d.terminate;
+  }
+  
+  process(chunk: Uint8Array, final: boolean) {
+    this.d.push(chunk, final);
+  }
+
+  /**
+   * Pushes a chunk to be deflated
+   * @param chunk The chunk to push
+   * @param final Whether this is the last chunk
+   */
+  push(chunk: Uint8Array, final?: boolean) {
+    ZipPassThrough.prototype.push.call(this, chunk, final);
+  }
+}
+
+type ZIFE = {
+  // compressed size
+  c: number;
+  // filename
+  f: Uint8Array;
+  // unicode
+  u: boolean;
+  // byte offset
+  b: number;
+  // header offset
+  h: number;
+  // terminator
+  t: () => void;
+  // turn
+  r: () => void;
+};
+
+type ZipInternalFile = ZHF & ZIFE;
+
+/**
+ * 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
+   */
+  constructor() {
+    this.u = [];
+    this.d = 1;
+  }
+  /**
+   * Adds a file to the ZIP archive
+   * @param file The file stream to add
+   */
+  add(file: ZipInputFile) {
+    if (this.d & 2) throw 'stream finished';
+    const f = strToU8(file.filename), fl = f.length, u = fl != file.filename.length, hl = fl + 30;
+    if (fl > 65535) throw 'filename too long';
+    const header = new u8(hl);
+    wzh(header, 0, file, f, u);
+    let chks: Uint8Array[] = [header];
+    const pAll = () => {
+      for (const chk of chks) this.ondata(null, chk, false);
+      chks = [];
+    };
+    let tr = this.d;
+    this.d = 0;
+    const ind = this.u.length;
+    const uf = mrg(file, {
+      f,
+      u,
+      t: () => { 
+        if (file.terminate) file.terminate();
+      },
+      r: () => {
+        pAll();
+        if (tr) {
+          const nxt = this.u[ind + 1];
+          if (nxt) nxt.r();
+          else this.d = 1;
+        }
+        tr = 1;
+      }
+    } as ZIFE);
+    let cl = 0;
+    file.ondata = (err, dat, final) => {
+      if (err) {
+        this.ondata(err, dat, final);
+        this.terminate();
+      } else {
+        cl += dat.length;
+        chks.push(dat);
+        if (final) {
+          chks.push(czdd(file, cl));
+          uf.c = cl, uf.b = hl + cl + 16, uf.crc = file.crc, uf.size = file.size;
+          if (tr) uf.r();
+          tr = 1;
+        } else if (tr) pAll();
+      }
+    }
+    this.u.push(uf);
+  }
+
+  /**
+   * Ends the process of adding files and prepares to emit the final chunks.
+   * This *must* be called after adding all desired files for the resulting
+   * ZIP file to work properly.
+   */
+  end() {
+    if (this.d & 2) {
+      if (this.d & 1) throw 'stream finishing';
+      throw 'stream finished';
+    }
+    if (this.d) this.e();
+    else this.u.push({
+      r: () => {
+        if (!(this.d & 1)) return;
+        this.u.splice(-1, 1);
+        this.e();
+      },
+      t: () => {}
+    } as unknown as ZipInternalFile);
+    this.d = 3;
+  }
+
+  private e() {
+    let bt = 0, l = 0, tl = 0;
+    for (const f of this.u) tl += 46 + f.f.length;
+    const out = new u8(tl + 22);
+    for (const f of this.u) {
+      wzh(out, bt, f, f.f, f.u, f.c, l);
+      bt += 46 + f.f.length, l += f.b;
+    }
+    wzf(out, bt, this.u.length, tl, l)
+    this.ondata(null, out, true);
+    this.d = 2;
+  }
+
+  /**
+   * A method to terminate any internal workers used by the stream. Subsequent
+   * calls to add() will silently fail.
+   */
+  terminate() {
+    for (const f of this.u) f.t();
+    this.d = 2;
+  }
+
+  /**
+   * The handler to call whenever data is available
+   */
+  ondata: AsyncFlateStreamHandler;
+}
 
 /**
  * Asynchronously creates a ZIP file
@@ -2104,8 +2619,11 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback,
     for (let i = 0; i < slft; ++i) {
       const f = files[i];
       try {
-        wzh(out, tot, f.c, f.d, f.m, f.n, f.u, f.p, null, f.t);
-        wzh(out, o, f.c, f.d, f.m, f.n, f.u, f.p, tot, f.t), o += 46 + f.n.length, tot += 30 + f.n.length + f.d.length;
+        const l = f.c.length;
+        wzh(out, tot, f, f.f, f.u, l);
+        const loc = tot + 30 + f.f.length;
+        out.set(f.c, loc);
+        wzh(out, o, f, f.f, f.u, l, tot), o += 46 + f.f.length, tot = loc + l;
       } catch(e) {
         return cb(e, null);
       }
@@ -2118,33 +2636,32 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback,
   for (let i = 0; i < slft; ++i) {
     const fn = k[i];
     const [file, p] = r[fn];
-    const c = crc(), m = file.length;
+    const c = crc(), size = file.length;
     c.p(file);
-    const n = strToU8(fn), s = n.length;
-    const t = p.level == 0 ? 0 : 8;
+    const f = strToU8(fn), s = f.length;
+    const compression = p.level == 0 ? 0 : 8;
     const cbl: FlateCallback = (e, d) => {
       if (e) {
         tAll();
         cb(e, null);
       } else {
         const l = d.length;
-        files[i] = {
-          t,
-          d,
-          m,
-          c: c.d(),
-          u: fn.length != l,
-          n,
-          p
-        };
+        files[i] = mrg(p, {
+          size,
+          crc: c.d(),
+          c: d,
+          f,
+          u: s != fn.length,
+          compression
+        });
         o += 30 + s + l;
         tot += 76 + 2 * s + l;
         if (!--lft) cbf();
       }
     }
-    if (n.length > 65535) cbl('filename too long', null);
-    if (!t) cbl(null, file);
-    else if (m < 160000) {
+    if (s > 65535) cbl('filename too long', null);
+    if (!compression) cbl(null, file);
+    else if (size < 160000) {
       try {
         cbl(null, deflateSync(file, p));
       } catch(e) {
@@ -2170,30 +2687,29 @@ export function zipSync(data: Zippable, opts: ZipOptions = {}) {
   let tot = 0;
   for (const fn in r) {
     const [file, p] = r[fn];
-    const t = p.level == 0 ? 0 : 8;
-    const n = strToU8(fn), s = n.length;
-    if (n.length > 65535) throw 'filename too long';
-    const d = t ? deflateSync(file, p) : file, l = d.length;
+    const compression = p.level == 0 ? 0 : 8;
+    const f = strToU8(fn), s = f.length;
+    if (s > 65535) throw 'filename too long';
+    const d = compression ? deflateSync(file, p) : file, l = d.length;
     const c = crc();
     c.p(file);
-    files.push({
-      t,
-      d,
-      m: file.length,
-      c: c.d(),
-      u: fn.length != s,
-      n,
+    files.push(mrg(p, {
+      size: file.length,
+      crc: c.d(),
+      c: d,
+      f,
+      u: s != fn.length,
       o,
-      p
-    });
+      compression
+    }));
     o += 30 + s + l;
     tot += 76 + 2 * s + l;
   }
   const out = new u8(tot + 22), oe = o, cdl = tot - o;
   for (let i = 0; i < files.length; ++i) {
     const f = files[i];
-    wzh(out, f.o, f.c, f.d, f.m, f.n, f.u, f.p, null, f.t);
-    wzh(out, o, f.c, f.d, f.m, f.n, f.u, f.p, f.o, f.t), o += 46 + f.n.length;
+    wzh(out, f.o, f, f.f, f.u, f.c.length);
+    wzh(out, o, f, f.f, f.u, f.c.length, f.o), o += 46 + f.f.length;
   }
   wzf(out, o, files.length, cdl, oe);
   return out;
@@ -2226,7 +2742,10 @@ export function unzip(data: Uint8Array, cb: UnzipCallback): AsyncTerminable {
   const z = o == 4294967295;
   if (z) {
     e = b4(data, e - 12);
-    if (b4(data, e) != 0x6064B50) throw 'invalid zip file';
+    if (b4(data, e) != 0x6064B50) {
+      cb('invalid zip file', null);
+      return;
+    }
     c = lft = b4(data, e + 32);
     o = b4(data, e + 48);
   }

+ 1 - 1
src/node-worker.ts

@@ -3,7 +3,7 @@ let Worker: typeof import('worker_threads').Worker;
 const workerAdd = ";var __w=require('worker_threads');__w.parentPort.on('message',function(m){onmessage({data:m})}),postMessage=function(m,t){__w.parentPort.postMessage(m,t)},close=process.exit;self=global";
 
 try {
-  Worker = require('worker_threads').Worker;
+  Worker = /*#__PURE__*/require('worker_threads').Worker;
 } catch(e) {
 }
 export default Worker ? <T>(c: string, _: number, msg: unknown, transfer: ArrayBuffer[], cb: (err: Error, msg: T) => void) => {

+ 5 - 0
yarn.lock

@@ -1540,6 +1540,11 @@
     "@material/feature-targeting" "^5.1.0"
     "@material/theme" "^5.1.0"
 
+"@msgpack/msgpack@^2.3.0":
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-2.3.0.tgz#a9043b920837b2dd63482e7bf6b8345813e9816b"
+  integrity sha512-xxRejzNpiVQ2lzxMG/yo2ocfZSk+cKo2THq54AimaubMucg66DpQm9Yj7ESMr/l2EqDkmF2Dx4r0F/cbsitAaw==
+
 "@nodelib/[email protected]":
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"