Arjun Barrett 4 éve
szülő
commit
ad6e684cc5
6 módosított fájl, 281 hozzáadás és 104 törlés
  1. 6 0
      CHANGELOG.md
  2. 1 1
      README.md
  3. 36 3
      docs/README.md
  4. 57 0
      docs/interfaces/flateerror.md
  5. 166 97
      src/index.ts
  6. 15 3
      src/worker.ts

+ 6 - 0
CHANGELOG.md

@@ -1,3 +1,9 @@
+## 0.7.0
+- Improved errors
+  - Now errors are error objects instead of strings
+  - Check the error code to apply custom logic based on error type
+- Made async operations always call callbacks asynchronously
+- Fixed bug that caused errors to not appear in asynchronous operations in browsers
 ## 0.6.10
 - Fixed async operations on Node.js with native ESM
 ## 0.6.5

+ 1 - 1
README.md

@@ -485,7 +485,7 @@ See the [documentation](https://github.com/101arrowz/fflate/blob/master/docs/REA
 
 ## 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 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).
+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 30kB 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     |
 |-------------------------|--------------------------------|------------------------|

+ 36 - 3
docs/README.md

@@ -42,6 +42,7 @@
 * [AsyncZippable](interfaces/asynczippable.md)
 * [AsyncZlibOptions](interfaces/asynczliboptions.md)
 * [DeflateOptions](interfaces/deflateoptions.md)
+* [FlateError](interfaces/flateerror.md)
 * [GzipOptions](interfaces/gzipoptions.md)
 * [UnzipDecoder](interfaces/unzipdecoder.md)
 * [UnzipDecoderConstructor](interfaces/unzipdecoderconstructor.md)
@@ -64,6 +65,10 @@
 * [UnzipFileHandler](README.md#unzipfilehandler)
 * [ZippableFile](README.md#zippablefile)
 
+### Variables
+
+* [FlateErrorCode](README.md#flateerrorcode)
+
 ### Functions
 
 * [decompress](README.md#decompress)
@@ -91,7 +96,7 @@
 
 ### AsyncFlateStreamHandler
 
-Ƭ  **AsyncFlateStreamHandler**: (err: Error,data: Uint8Array,final: boolean) => void
+Ƭ  **AsyncFlateStreamHandler**: (err: [Error](interfaces/flateerror.md#error),data: Uint8Array,final: boolean) => void
 
 Handler for asynchronous data (de)compression streams
 
@@ -113,7 +118,7 @@ ___
 
 ### FlateCallback
 
-Ƭ  **FlateCallback**: (err: Error \| string,data: Uint8Array) => void
+Ƭ  **FlateCallback**: (err: [Error](interfaces/flateerror.md#error) \| string,data: Uint8Array) => void
 
 Callback for asynchronous (de)compression methods
 
@@ -149,7 +154,7 @@ ___
 
 ### UnzipCallback
 
-Ƭ  **UnzipCallback**: (err: Error \| string,data: [Unzipped](interfaces/unzipped.md)) => void
+Ƭ  **UnzipCallback**: (err: [Error](interfaces/flateerror.md#error) \| string,data: [Unzipped](interfaces/unzipped.md)) => void
 
 Callback for asynchronous ZIP decompression
 
@@ -175,6 +180,34 @@ ___
 
 A file that can be used to create a ZIP archive
 
+## Variables
+
+### FlateErrorCode
+
+• `Const` **FlateErrorCode**: object = { UnexpectedEOF: 0, InvalidBlockType: 1, InvalidLengthLiteral: 2, InvalidDistance: 3, StreamFinished: 4, NoStreamHandler: 5, InvalidHeader: 6, NoCallback: 7, InvalidUTF8: 8, ExtraFieldTooLong: 9, InvalidDate: 10, FilenameTooLong: 11, StreamFinishing: 12, InvalidZipData: 13, UnknownCompressionMethod: 14} as const
+
+Codes for errors generated within this library
+
+#### Type declaration:
+
+Name | Type |
+------ | ------ |
+`ExtraFieldTooLong` | 9 |
+`FilenameTooLong` | 11 |
+`InvalidBlockType` | 1 |
+`InvalidDate` | 10 |
+`InvalidDistance` | 3 |
+`InvalidHeader` | 6 |
+`InvalidLengthLiteral` | 2 |
+`InvalidUTF8` | 8 |
+`InvalidZipData` | 13 |
+`NoCallback` | 7 |
+`NoStreamHandler` | 5 |
+`StreamFinished` | 4 |
+`StreamFinishing` | 12 |
+`UnexpectedEOF` | 0 |
+`UnknownCompressionMethod` | 14 |
+
 ## Functions
 
 ### decompress

+ 57 - 0
docs/interfaces/flateerror.md

@@ -0,0 +1,57 @@
+# Interface: FlateError
+
+An error generated within this library
+
+## Hierarchy
+
+* [Error](flateerror.md#error)
+
+  ↳ **FlateError**
+
+## Index
+
+### Properties
+
+* [Error](flateerror.md#error)
+* [code](flateerror.md#code)
+* [message](flateerror.md#message)
+* [name](flateerror.md#name)
+* [stack](flateerror.md#stack)
+
+## Properties
+
+### Error
+
+•  **Error**: ErrorConstructor
+
+___
+
+### code
+
+•  **code**: number
+
+The code associated with this error
+
+___
+
+### message
+
+•  **message**: string
+
+*Inherited from [FlateError](flateerror.md).[message](flateerror.md#message)*
+
+___
+
+### name
+
+•  **name**: string
+
+*Inherited from [FlateError](flateerror.md).[name](flateerror.md#name)*
+
+___
+
+### stack
+
+• `Optional` **stack**: string
+
+*Inherited from [FlateError](flateerror.md).[stack](flateerror.md#stack)*

+ 166 - 97
src/index.ts

@@ -174,6 +174,64 @@ type InflateState = {
   i?: boolean;
 };
 
+/**
+ * Codes for errors generated within this library
+ */
+export const FlateErrorCode = {
+  UnexpectedEOF: 0,
+  InvalidBlockType: 1,
+  InvalidLengthLiteral: 2,
+  InvalidDistance: 3,
+  StreamFinished: 4,
+  NoStreamHandler: 5,
+  InvalidHeader: 6,
+  NoCallback: 7,
+  InvalidUTF8: 8,
+  ExtraFieldTooLong: 9,
+  InvalidDate: 10,
+  FilenameTooLong: 11,
+  StreamFinishing: 12,
+  InvalidZipData: 13,
+  UnknownCompressionMethod: 14
+} as const;
+
+// error codes
+const ec = [
+  'unexpected EOF',
+  'invalid block type',
+  'invalid length/literal',
+  'invalid distance',
+  'stream finished',
+  'no stream handler',
+  0 as unknown as string, // determined by compression function
+  'no callback',
+  'invalid UTF-8 data',
+  'extra field too long',
+  'date not in range 1980-2099',
+  'filename too long',
+  'stream finishing',
+  'invalid zip data'
+  // determined by unknown compression method
+];
+
+/**
+ * An error generated within this library
+ */
+export interface FlateError extends Error {
+  /**
+   * The code associated with this error
+   */
+  code: number;
+};
+
+const err = (ind: number, msg?: string | 0, nt?: 1) => {
+  const e: Partial<FlateError> = new Error(msg || ec[ind]);
+  e.code = ind;
+  if (Error.captureStackTrace) Error.captureStackTrace(e, err);
+  if (!nt) throw e;
+  return e as FlateError;
+}
+
 // expands raw DEFLATE data
 const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
   // source length
@@ -212,7 +270,7 @@ const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
         // go to end of byte boundary
         const s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l;
         if (t > sl) {
-          if (noSt) throw 'unexpected EOF';
+          if (noSt) err(0);
           break;
         }
         // ensure size
@@ -268,9 +326,9 @@ const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
         dbt = max(dt);
         lm = hMap(lt, lbt, 1);
         dm = hMap(dt, dbt, 1);
-      } else throw 'invalid block type';
+      } else err(1);
       if (pos > tbts) {
-        if (noSt) throw 'unexpected EOF';
+        if (noSt) err(0);
         break;
       }
     }
@@ -284,10 +342,10 @@ const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
       const c = lm[bits16(dat, pos) & lms], sym = c >>> 4;
       pos += c & 15;
       if (pos > tbts) {
-        if (noSt) throw 'unexpected EOF';
+        if (noSt) err(0);
         break;
       }
-      if (!c) throw 'invalid length/literal';
+      if (!c) err(2);
       if (sym < 256) buf[bt++] = sym;
       else if (sym == 256) {
         lpos = pos, lm = null;
@@ -303,7 +361,7 @@ const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
         }
         // dist
         const d = dm[bits16(dat, pos) & dms], dsym = d >>> 4;
-        if (!d) throw 'invalid distance';
+        if (!d) err(3);
         pos += d & 15;
         let dt = fd[dsym];
         if (dsym > 3) {
@@ -311,7 +369,7 @@ const inflt = (dat: Uint8Array, buf?: Uint8Array, st?: InflateState) => {
           dt += bits16(dat, pos) & ((1 << b) - 1), pos += b;
         }
         if (pos > tbts) {
-          if (noSt) throw 'unexpected EOF';
+          if (noSt) err(0);
           break;
         }
         if (noBuf) cbuf(bt + 131072);
@@ -784,14 +842,14 @@ export type FlateStreamHandler = (data: Uint8Array, final: boolean) => void;
  * @param data The data output from the stream processor
  * @param final Whether this is the final block
  */
-export type AsyncFlateStreamHandler = (err: Error, data: Uint8Array, final: boolean) => void;
+export type AsyncFlateStreamHandler = (err: FlateError, data: Uint8Array, final: boolean) => void;
 
 /**
  * Callback for asynchronous (de)compression methods
  * @param err Any error that occurred
  * @param data The resulting data. Only present if `err` is null
  */
-export type FlateCallback = (err: Error | string, data: Uint8Array) => void;
+export type FlateCallback = (err: FlateError, data: Uint8Array) => void;
 
 // async callback-based compression
 interface AsyncOptions {
@@ -907,7 +965,7 @@ const cbfs = (v: Record<string, unknown>) => {
 }
 
 // use a worker to execute code
-const wrkr = <T, R>(fns: (() => unknown[])[], init: (ev: MessageEvent<T>) => void, id: number, cb: (err: Error, msg: R) => void) => {
+const wrkr = <T, R>(fns: (() => unknown[])[], init: (ev: MessageEvent<T>) => void, id: number, cb: (err: FlateError, msg: R) => void) => {
   if (!ch[id]) {
     let fnStr = '', td: Record<string, unknown> = {}, m = fns.length - 1;
     for (let i = 0; i < m; ++i)
@@ -919,7 +977,7 @@ const wrkr = <T, R>(fns: (() => unknown[])[], init: (ev: MessageEvent<T>) => voi
 }
 
 // base async inflate fn
-const bInflt = () => [u8, u16, u32, fleb, fdeb, clim, fl, fd, flrm, fdrm, rev, hMap, max, bits, bits16, shft, slc, inflt, inflateSync, pbf, gu8];
+const bInflt = () => [u8, u16, u32, fleb, fdeb, clim, fl, fd, flrm, fdrm, rev, ec, hMap, max, bits, bits16, shft, slc, err, inflt, inflateSync, pbf, gu8];
 const bDflt = () => [u8, u16, u32, fleb, fdeb, clim, revfl, revfd, flm, flt, fdm, fdt, rev, deo, et, hMap, wbits, wbits16, hTree, ln, lc, clen, wfblk, wblk, shft, slc, dflt, dopt, deflateSync, pbf]
 
 // gzip extra
@@ -978,9 +1036,9 @@ const astrmify = <T>(fns: (() => unknown[])[], strm: Astrm, opts: T | 0, init: (
     }
   )
   w.postMessage(opts);
-  strm.push = function(d, f) {
-    if (t) throw 'stream finished';
-    if (!strm.ondata) throw 'no stream handler';
+  strm.push = (d, f) => {
+    if (t) err(4);
+    if (!strm.ondata) err(5);
     w.postMessage([d, t = f], [d.buffer]);
   };
   strm.terminate = () => { w.terminate(); };
@@ -1014,7 +1072,7 @@ const gzh = (c: Uint8Array, o: GzipOptions) => {
 
 // gzip start
 const gzs = (d: Uint8Array) => {
-  if (d[0] != 31 || d[1] != 139 || d[2] != 8) throw 'invalid gzip data';
+  if (d[0] != 31 || d[1] != 139 || d[2] != 8) err(6, 'invalid gzip data');
   const flg = d[3];
   let st = 10;
   if (flg & 4) st += d[10] | (d[11] << 8) + 2;
@@ -1039,8 +1097,8 @@ const zlh = (c: Uint8Array, o: ZlibOptions) => {
 
 // zlib valid
 const zlv = (d: Uint8Array) => {
-  if ((d[0] & 15) != 8 || (d[0] >>> 4) > 7 || ((d[0] << 8 | d[1]) % 31)) throw 'invalid zlib data';
-  if (d[1] & 32) throw 'invalid zlib data: preset dictionaries not supported';
+  if ((d[0] & 15) != 8 || (d[0] >>> 4) > 7 || ((d[0] << 8 | d[1]) % 31)) err(6, 'invalid zlib data');
+  if (d[1] & 32) err(6, 'invalid zlib data: preset dictionaries not supported');
 }
 
 /**
@@ -1095,8 +1153,8 @@ export class Deflate {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final?: boolean) {
-    if (this.d) throw 'stream finished';
-    if (!this.ondata) throw 'no stream handler';
+    if (this.d) err(4);
+    if (!this.ondata) err(5);
     this.d = final;
     this.p(chunk, final || false);
   }
@@ -1163,7 +1221,7 @@ export function deflate(data: Uint8Array, opts: AsyncDeflateOptions, cb: FlateCa
 export function deflate(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function deflate(data: Uint8Array, opts: AsyncDeflateOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return cbify(data, opts as AsyncDeflateOptions, [
     bDflt,
   ], ev => pbf(deflateSync(ev.data[0], ev.data[1])), 0, cb);
@@ -1198,8 +1256,8 @@ export class Inflate {
   ondata: FlateStreamHandler;
 
   private e(c: Uint8Array) {
-    if (this.d) throw 'stream finished';
-    if (!this.ondata) throw 'no stream handler';
+    if (this.d) err(4);
+    if (!this.ondata) err(5);
     const l = this.p.length;
     const n = new u8(l + c.length);
     n.set(this.p), n.set(c, l), this.p = n;
@@ -1280,7 +1338,7 @@ export function inflate(data: Uint8Array, opts: AsyncInflateOptions, cb: FlateCa
 export function inflate(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function inflate(data: Uint8Array, opts: AsyncInflateOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return cbify(data, opts as AsyncInflateOptions, [
     bInflt
   ], ev => pbf(inflateSync(ev.data[0], gu8(ev.data[1]))), 1, cb);
@@ -1408,7 +1466,7 @@ export function gzip(data: Uint8Array, opts: AsyncGzipOptions, cb: FlateCallback
 export function gzip(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function gzip(data: Uint8Array, opts: AsyncGzipOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return cbify(data, opts as AsyncGzipOptions, [
     bDflt,
     gze,
@@ -1460,7 +1518,7 @@ export class Gunzip {
       this.p = this.p.subarray(s), this.v = 0;
     }
     if (final) {
-      if (this.p.length < 8) throw 'invalid gzip stream';
+      if (this.p.length < 8) err(6, 'invalid gzip data');
       this.p = this.p.subarray(0, -8);
     }
     // necessary to prevent TS from using the closure value
@@ -1526,7 +1584,7 @@ export function gunzip(data: Uint8Array, opts: AsyncGunzipOptions, cb: FlateCall
 export function gunzip(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function gunzip(data: Uint8Array, opts: AsyncGunzipOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return cbify(data, opts as AsyncGunzipOptions, [
     bInflt,
     guze,
@@ -1651,7 +1709,7 @@ export function zlib(data: Uint8Array, opts: AsyncZlibOptions, cb: FlateCallback
 export function zlib(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function zlib(data: Uint8Array, opts: AsyncZlibOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return cbify(data, opts as AsyncZlibOptions, [
     bDflt,
     zle,
@@ -1701,7 +1759,7 @@ export class Unzlib {
       this.p = this.p.subarray(2), this.v = 0;
     }
     if (final) {
-      if (this.p.length < 4) throw 'invalid zlib stream';
+      if (this.p.length < 4) err(6, 'invalid zlib data');
       this.p = this.p.subarray(0, -4);
     }
     // necessary to prevent TS from using the closure value
@@ -1767,7 +1825,7 @@ export function unzlib(data: Uint8Array, opts: AsyncGunzipOptions, cb: FlateCall
 export function unzlib(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function unzlib(data: Uint8Array, opts: AsyncGunzipOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return cbify(data, opts as AsyncUnzlibOptions, [
     bInflt,
     zule,
@@ -1815,7 +1873,7 @@ export class Decompress {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final?: boolean) {
-    if (!this.ondata) throw 'no stream handler';
+    if (!this.ondata) err(5);
     if (!this.s) {
       if (this.p && this.p.length) {
         const n = new u8(this.p.length + chunk.length);
@@ -1881,7 +1939,7 @@ export function decompress(data: Uint8Array, opts: AsyncInflateOptions, cb: Flat
 export function decompress(data: Uint8Array, cb: FlateCallback): AsyncTerminable;
 export function decompress(data: Uint8Array, opts: AsyncInflateOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   return (data[0] == 31 && data[1] == 139 && data[2] == 8)
     ? gunzip(data, opts as AsyncInflateOptions, cb)
     : ((data[0] & 15) != 8 || (data[0] >> 4) > 7 || ((data[0] << 8 | data[1]) % 31))
@@ -2017,7 +2075,7 @@ export type StringStreamHandler = (data: string, final: boolean) => void;
  * @param err Any error that occurred
  * @param data The decompressed ZIP archive
  */
-export type UnzipCallback = (err: Error | string, data: Unzipped) => void;
+export type UnzipCallback = (err: FlateError, data: Unzipped) => void;
 
 /**
  * Handler for streaming ZIP decompression
@@ -2086,23 +2144,23 @@ export class DecodeUTF8 {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final?: boolean) {
-    if (!this.ondata) throw 'no callback';
+    if (!this.ondata) err(5);
     final = !!final;
     if (this.t) {
       this.ondata(this.t.decode(chunk, { stream: true }), final);
       if (final) {
-        if (this.t.decode().length) throw 'invalid utf-8 data';
+        if (this.t.decode().length) err(8);
         this.t = null;
       }
       return;
     }
-    if (!this.p) throw 'stream finished'
+    if (!this.p) err(4);
     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) {
-      if (np.length) throw 'invalid utf-8 data';
+      if (np.length) err(8);
       this.p = null;
     } else this.p = np;
     this.ondata(ch, final);
@@ -2133,8 +2191,8 @@ export class EncodeUTF8 {
    * @param final Whether this is the last chunk
    */
   push(chunk: string, final?: boolean) {
-    if (!this.ondata) throw 'no callback';
-    if (this.d) throw 'stream finished';
+    if (!this.ondata) err(5);
+    if (this.d) err(4);
     this.ondata(strToU8(chunk), this.d = final || false);
   }
 
@@ -2195,7 +2253,7 @@ export function strFromU8(dat: Uint8Array, latin1?: boolean) {
   } else if (td) return td.decode(dat)
   else {
     const [out, ext] = dutf8(dat);
-    if (ext.length) throw 'invalid utf-8 data';
+    if (ext.length) err(8);
     return out;
   } 
 };
@@ -2228,7 +2286,7 @@ const exfl = (ex?: ZHF['extra']) => {
   if (ex) {
     for (const k in ex) {
       const l = ex[k].length;
-      if (l > 65535) throw 'extra field too long';
+      if (l > 65535) err(9);
       le += l + 4;
     }
   }
@@ -2245,7 +2303,7 @@ const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c?: n
   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';
+  if (y < 0 || y > 119) err(10);
   wbytes(d, b, (y << 25) | ((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);
@@ -2416,7 +2474,7 @@ export class ZipPassThrough implements ZipInputFile {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final?: boolean) {
-    if (!this.ondata) throw 'no callback - add to ZIP archive before pushing';
+    if (!this.ondata) err(5);
     this.c.p(chunk);
     this.size += chunk.length;
     if (final) this.crc = this.c.d();
@@ -2570,12 +2628,12 @@ export class Zip {
    * @param file The file stream to add
    */
   add(file: ZipInputFile) {
-    if (this.d & 2) throw 'stream finished';
+    if (this.d & 2) err(4);
     const f = strToU8(file.filename), fl = f.length;
     const com = file.comment, o = com && strToU8(com);
     const u = fl != file.filename.length || (o && (com.length != o.length));
     const hl = fl + exfl(file.extra) + 30;
-    if (fl > 65535) throw 'filename too long';
+    if (fl > 65535) err(11);
     const header = new u8(hl);
     wzh(header, 0, file, f, u);
     let chks: Uint8Array[] = [header];
@@ -2634,8 +2692,8 @@ export class Zip {
    */
   end() {
     if (this.d & 2) {
-      if (this.d & 1) throw 'stream finishing';
-      throw 'stream finished';
+      if (this.d & 1) err(12);
+      err(4);
     }
     if (this.d) this.e();
     else this.u.push({
@@ -2694,7 +2752,7 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions, cb: FlateCallbac
 export function zip(data: AsyncZippable, cb: FlateCallback): AsyncTerminable;
 export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback, cb?: FlateCallback) {
   if (!cb) cb = opts as FlateCallback, opts = {};
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   const r: FlatZippable<true> = {};
   fltn(data, '', r, opts as AsyncZipOptions);
   const k = Object.keys(r);
@@ -2704,6 +2762,10 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback,
   const tAll = () => {
     for (let i = 0; i < term.length; ++i) term[i]();
   }
+  let cbd: FlateCallback = (a, b) => {
+    mt(() => { cb(a, b); });
+  }
+  mt(() => { cbd = cb; });
   const cbf = () => {
     const out = new u8(tot + 22), oe = o, cdl = tot - o;
     tot = 0;
@@ -2717,11 +2779,11 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback,
         out.set(f.c, loc);
         wzh(out, o, f, f.f, f.u, l, tot, f.m), o += 16 + badd + (f.m ? f.m.length : 0), tot = loc + l;
       } catch(e) {
-        return cb(e, null);
+        return cbd(e, null);
       }
     }
     wzf(out, o, files.length, cdl, oe);
-    cb(null, out);
+    cbd(null, out);
   }
   if (!lft) cbf();
   // Cannot use lft because it can decrease
@@ -2737,7 +2799,7 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback,
     const cbl: FlateCallback = (e, d) => {
       if (e) {
         tAll();
-        cb(e, null);
+        cbd(e, null);
       } else {
         const l = d.length;
         files[i] = mrg(p, {
@@ -2754,7 +2816,7 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback,
         if (!--lft) cbf();
       }
     }
-    if (s > 65535) cbl('filename too long', null);
+    if (s > 65535) cbl(err(11, 0, 1), null);
     if (!compression) cbl(null, file);
     else if (size < 160000) {
       try {
@@ -2787,7 +2849,7 @@ export function zipSync(data: Zippable, opts?: ZipOptions) {
     const f = strToU8(fn), s = f.length;
     const com = p.comment, m = com && strToU8(com), ms = m && m.length;
     const exl = exfl(p.extra);
-    if (s > 65535) throw 'filename too long';
+    if (s > 65535) err(11);
     const d = compression ? deflateSync(file, p) : file, l = d.length;
     const c = crc();
     c.p(file);
@@ -3003,8 +3065,8 @@ export class Unzip {
    * @param final Whether this is the last chunk
    */
   push(chunk: Uint8Array, final?: boolean) {
-    if (!this.onfile) throw 'no callback';
-    if (!this.p) throw 'stream finished';
+    if (!this.onfile) err(5);
+    if (!this.p) err(4);
     if (this.c > 0) {
       const len = Math.min(this.c, chunk.length);
       const toAdd = chunk.subarray(0, len);
@@ -3044,11 +3106,11 @@ export class Unzip {
               name: fn,
               compression: cmp,
               start: () => {
-                if (!file.ondata) throw 'no callback';
+                if (!file.ondata) err(5);
                 if (!sc) file.ondata(null, et, true);
                 else {
                   const ctr = this.o[cmp];
-                  if (!ctr) throw 'unknown compression type ' + cmp;
+                  if (!ctr) err(14, 'unknown compression type ' + cmp);
                   d = sc < 0 ? new ctr(fn) : new ctr(fn, sc, su);
                   d.ondata = (err, dat, final) => { file.ondata(err, dat, final); }
                   for (const dat of chks) d.push(dat, false);
@@ -3084,7 +3146,7 @@ export class Unzip {
       this.p = buf.subarray(i);
     }
     if (final) {
-      if (this.c) throw 'invalid zip file';
+      if (this.c) err(13);
       this.p = null;
     }
   }
@@ -3104,6 +3166,8 @@ export class Unzip {
   onfile: UnzipFileHandler;
 }
 
+const mt = typeof queueMicrotask == 'function' ? queueMicrotask : setTimeout;
+
 /**
  * Asynchronously decompresses a ZIP archive
  * @param data The raw compressed ZIP file
@@ -3111,58 +3175,63 @@ export class Unzip {
  * @returns A function that can be used to immediately terminate the unzipping
  */
 export function unzip(data: Uint8Array, cb: UnzipCallback): AsyncTerminable {
-  if (typeof cb != 'function') throw 'no callback';
+  if (typeof cb != 'function') err(7);
   const term: AsyncTerminable[] = [];
   const tAll = () => {
     for (let i = 0; i < term.length; ++i) term[i]();
   }
   const files: Unzipped = {};
+  let cbd: UnzipCallback = (a, b) => {
+    mt(() => { cb(a, b); });
+  }
+  mt(() => { cbd = cb; });
   let e = data.length - 22;
   for (; b4(data, e) != 0x6054B50; --e) {
     if (!e || data.length - e > 65558) {
-      cb('invalid zip file', null);
-      return;
+      cbd(err(13, 0, 1), null);
+      return tAll;
     }
   };
   let lft = b2(data, e + 8);
-  if (!lft) cb(null, {});
-  let c = lft;
-  let o = b4(data, e + 16);
-  const z = o == 4294967295;
-  if (z) {
-    e = b4(data, e - 12);
-    if (b4(data, e) != 0x6064B50) {
-      cb('invalid zip file', null);
-      return;
-    }
-    c = lft = b4(data, e + 32);
-    o = b4(data, e + 48);
-  }
-  for (let i = 0; i < c; ++i) {
-    const [c, sc, su, fn, no, off] = zh(data, o, z), b = slzh(data, off);
-    o = no
-    const cbl: FlateCallback = (e, d) => {
-      if (e) {
-        tAll();
-        cb(e, null);
-      } else {
-        files[fn] = d;
-        if (!--lft) cb(null, files);
+  if (lft) {
+    let c = lft;
+    let o = b4(data, e + 16);
+    const z = o == 4294967295;
+    if (z) {
+      e = b4(data, e - 12);
+      if (b4(data, e) != 0x6064B50) {
+        cbd(err(13, 0, 1), null);
+        return tAll;
       }
+      c = lft = b4(data, e + 32);
+      o = b4(data, e + 48);
     }
-    if (!c) cbl(null, slc(data, b, b + sc))
-    else if (c == 8) {
-      const infl = data.subarray(b, b + sc);
-      if (sc < 320000) {
-        try {
-          cbl(null, inflateSync(infl, new u8(su)));
-        } catch(e) {
-          cbl(e, null);
+    for (let i = 0; i < c; ++i) {
+      const [c, sc, su, fn, no, off] = zh(data, o, z), b = slzh(data, off);
+      o = no
+      const cbl: FlateCallback = (e, d) => {
+        if (e) {
+          tAll();
+          cbd(e, null);
+        } else {
+          files[fn] = d;
+          if (!--lft) cbd(null, files);
         }
       }
-      else term.push(inflate(infl, { size: su }, cbl));
-    } else cbl('unknown compression type ' + c, null);
-  }
+      if (!c) cbl(null, slc(data, b, b + sc))
+      else if (c == 8) {
+        const infl = data.subarray(b, b + sc);
+        if (sc < 320000) {
+          try {
+            cbl(null, inflateSync(infl, new u8(su)));
+          } catch(e) {
+            cbl(e, null);
+          }
+        }
+        else term.push(inflate(infl, { size: su }, cbl));
+      } else cbl(err(14, 'unknown compression type ' + c, 1), null);
+    }
+  } else cbd(null, {});
   return tAll;
 }
 
@@ -3176,7 +3245,7 @@ export function unzipSync(data: Uint8Array) {
   const files: Unzipped = {};
   let e = data.length - 22;
   for (; b4(data, e) != 0x6054B50; --e) {
-    if (!e || data.length - e > 65558) throw 'invalid zip file';
+    if (!e || data.length - e > 65558) err(13);
   };
   let c = b2(data, e + 8);
   if (!c) return {};
@@ -3184,7 +3253,7 @@ export function unzipSync(data: Uint8Array) {
   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) err(13);
     c = b4(data, e + 32);
     o = b4(data, e + 48);
   }
@@ -3193,7 +3262,7 @@ export function unzipSync(data: Uint8Array) {
     o = no;
     if (!c) files[fn] = slc(data, b, b + sc);
     else if (c == 8) files[fn] = inflateSync(data.subarray(b, b + sc), new u8(su));
-    else throw 'unknown compression type ' + c;
+    else err(14, 'unknown compression type ' + c);
   }
   return files;
 }

+ 15 - 3
src/worker.ts

@@ -1,9 +1,21 @@
 const ch2: Record<string, string> = {};
 
 export default <T>(c: string, id: number, msg: unknown, transfer: ArrayBuffer[], cb: (err: Error, msg: T) => void) => {
-  const w = new Worker(ch2[id] ||= URL.createObjectURL(new Blob([c], { type: 'text/javascript' })));
-  w.onerror = e => cb(e.error, null);
-  w.onmessage = e => cb(null, e.data);
+  const w = new Worker(ch2[id] ||= URL.createObjectURL(
+    new Blob([
+      c + ';addEventListener("error",function(e){e=e.error;postMessage([e.message,e.code,e.stack])})'
+    ], { type: 'text/javascript' })
+  ));
+  w.onmessage = e => {
+    const d = e.data;
+    if (Array.isArray(d)) {
+      const err = new Error(d[0]);
+      err['code'] = d[1];
+      // if Error#stack DNE, still undefined
+      err.stack = d[2];
+      cb(err, null);
+    } else cb(null, d);
+  }
   w.postMessage(msg, transfer);
   return w;
 }