mhtml-stream 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Erik Brinkman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ MHTML Stream
2
+ ============
3
+ [![build](https://github.com/erikbrinkman/mhtml-stream/actions/workflows/node.js.yml/badge.svg)](https://github.com/erikbrinkman/mhtml-stream/actions/workflows/node.js.yml)
4
+ [![docs](https://img.shields.io/badge/docs-docs-blue)](https://erikbrinkman.github.io/mhtml-stream/)
5
+
6
+ Zero-dependency library for parsing MHTML data as streams using modern WHATWG
7
+ streams and async iterators. Because it relies on modern cross javascript
8
+ standards it works out-of-the-box in all javascript environments, with only a
9
+ little tweaking necessary for module definitions.
10
+
11
+ Usage
12
+ -----
13
+
14
+ ```javascript
15
+ import { parseMhtml } from "mhtml-stream";
16
+
17
+ for await (const { headers, content } of parseMhtml(...)) {
18
+ // headers : a key-value object with the header information
19
+
20
+ // content : a Uint8Array of the raw data, if you want as a string, `new
21
+ // TextDecoder().decode(content)` should work if the contents were utf-8 /
22
+ // ascii encoded
23
+
24
+ // NOTE in many MHTML files, the initial file is empty and contains headers
25
+ // for how to parse each individual included file.
26
+ }
27
+ ```
@@ -0,0 +1,8 @@
1
+ /**
2
+ * decode base 64 string as bytes
3
+ */
4
+ export declare function decodeBase64(sBase64: string, nBlocksSize?: number): Uint8Array;
5
+ /**
6
+ * encode bytes as base64 string
7
+ */
8
+ export declare function encodeBase64(aBytes: Uint8Array): string;
package/dist/base64.js ADDED
@@ -0,0 +1,77 @@
1
+ /*\
2
+ |*|
3
+ |*| Base64 / binary data / UTF-8 strings utilities
4
+ |*|
5
+ |*| https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
6
+ |*|
7
+ \*/
8
+ /* Array of bytes to Base64 string decoding */
9
+ function b64ToUint6(nChr) {
10
+ return nChr > 64 && nChr < 91
11
+ ? nChr - 65
12
+ : nChr > 96 && nChr < 123
13
+ ? nChr - 71
14
+ : nChr > 47 && nChr < 58
15
+ ? nChr + 4
16
+ : nChr === 43
17
+ ? 62
18
+ : nChr === 47
19
+ ? 63
20
+ : /* istanbul ignore next */
21
+ 0;
22
+ }
23
+ /**
24
+ * decode base 64 string as bytes
25
+ */
26
+ export function decodeBase64(sBase64, nBlocksSize) {
27
+ const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, "");
28
+ const nInLen = sB64Enc.length;
29
+ const nOutLen = nBlocksSize
30
+ ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
31
+ : (nInLen * 3 + 1) >> 2;
32
+ const taBytes = new Uint8Array(nOutLen);
33
+ for (let nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
34
+ const nMod4 = nInIdx & 3;
35
+ nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
36
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
37
+ for (let nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
38
+ taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
39
+ }
40
+ nUint24 = 0;
41
+ }
42
+ }
43
+ return taBytes;
44
+ }
45
+ /* Base64 string to array encoding */
46
+ function uint6ToB64(nUint6) {
47
+ return nUint6 < 26
48
+ ? nUint6 + 65
49
+ : nUint6 < 52
50
+ ? nUint6 + 71
51
+ : nUint6 < 62
52
+ ? nUint6 - 4
53
+ : nUint6 === 62
54
+ ? 43
55
+ : nUint6 === 63
56
+ ? 47
57
+ : /* istanbul ignore next */
58
+ 65;
59
+ }
60
+ /**
61
+ * encode bytes as base64 string
62
+ */
63
+ export function encodeBase64(aBytes) {
64
+ const nLen = aBytes.length;
65
+ let nMod3 = 2;
66
+ let sB64Enc = "";
67
+ for (let nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
68
+ nMod3 = nIdx % 3;
69
+ nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
70
+ if (nMod3 === 2 || aBytes.length - nIdx === 1) {
71
+ sB64Enc += String.fromCodePoint(uint6ToB64((nUint24 >>> 18) & 63), uint6ToB64((nUint24 >>> 12) & 63), uint6ToB64((nUint24 >>> 6) & 63), uint6ToB64(nUint24 & 63));
72
+ nUint24 = 0;
73
+ }
74
+ }
75
+ return (sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) +
76
+ (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "=="));
77
+ }
@@ -0,0 +1 @@
1
+ var y=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var q=e=>y(e,"__esModule",{value:!0});var D=(e,t)=>{for(var r in t)y(e,r,{get:t[r],enumerable:!0})},L=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $(t))!k.call(e,o)&&(r||o!=="default")&&y(e,o,{get:()=>t[o],enumerable:!(n=S(t,o))||n.enumerable});return e};var P=(e=>(t,r)=>e&&e.get(t)||(r=L(q({}),t,1),e&&e.set(t,r),r))(typeof WeakMap!="undefined"?new WeakMap:0);var K={};D(K,{MhtmlHeaders:()=>h,parseMhtml:()=>G});function Q(e){return e>64&&e<91?e-65:e>96&&e<123?e-71:e>47&&e<58?e+4:e===43?62:e===47?63:0}function f(e,t){let r=e.replace(/[^A-Za-z0-9+/]/g,""),n=r.length,o=t?Math.ceil((n*3+1>>2)/t)*t:n*3+1>>2,s=new Uint8Array(o);for(let a=0,i=0,d=0;d<n;d++){let l=d&3;if(a|=Q(r.charCodeAt(d))<<6*(3-l),l===3||n-d===1){for(let c=0;c<3&&i<o;c++,i++)s[i]=a>>>(16>>>c&24)&255;a=0}}return s}function j(e){return ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e)}function g(e,t){if(e.length!==t.length)return!1;if(e.byteOffset%4===t.byteOffset%4){let r=(4-e.byteOffset%4)%4,n=Math.floor((e.byteLength-r)/4),o=r+n*4,s=new Uint32Array(e.buffer,e.byteOffset+r,n),a=new Uint32Array(t.buffer,t.byteOffset+r,n);for(let i=0;i<r;++i)if(e[i]!==t[i])return!1;for(let i=0;i<n;++i)if(s[i]!==a[i])return!1;for(let i=o;i<e.length;++i)if(e[i]!==t[i])return!1;return!0}else{for(let r of e.keys())if(e[r]!==t[r])return!1;return!0}}function A(e){return{[Symbol.asyncIterator](){let t=e.getReader();return{async next(){let r=await t.read();return r.done?{done:!0,value:void 0}:{done:!1,value:r.value}}}}}}function H(e,t){return e.findIndex((r,n)=>{if(r!==t[0]||n+t.length>e.length)return!1;for(let o=1;o<t.length;++o)if(e[n+o]!==t[o])return!1;return!0})}async function*p(e,t){let r=new Uint8Array(0);for await(let n of e){let o=j(n);r=r.length?I([r,o]):o;let s;for(;(s=H(r,t))!==-1;)yield r.subarray(0,s),r=r.subarray(s+t.length)}yield r}function I(e){let t=e.reduce((o,s)=>o+s.length,0),r=new Uint8Array(t),n=0;for(let o of e)r.set(o,n),n+=o.length;return r}async function m(e){let t=[];for await(let r of e)t.push(r);return I(t)}var R=new Uint8Array([10]);async function*U(e,t=R){for await(let r of e){let n=new Uint8Array(r.length+t.length),o=!1,s=0;for(let a=0;a<r.length;++a){let i=r[a];if(i>=128)throw new Error(`got non-ascii character when decoding quoted printable: ${r}`);if(i!==61)n[s++]=i;else{let d=r[++a];if(d===void 0)o=!0;else{let l=r[++a];if(l===void 0)throw new Error("quoted printable escape (=) was not followed by two bytes");let c=parseInt(String.fromCharCode(d),16);c*=16,c+=parseInt(String.fromCharCode(l),16),n[s++]=c}}}o||(n.set(t,s),s+=t.length),yield n.subarray(0,s)}}var W=new TextDecoder;async function*x(e){for await(let t of e)yield f(W.decode(t))}async function*b(e){for await(let t of e)yield t}function v(){throw new Error("binary transfer-encoding is explicitly not supported and trying to add an implementation will likely result in unexpected results, but if you want to ignore anyway, set binary to `decode8bit`")}var E=new TextEncoder,M=new TextDecoder,h=class{#e=new Map;[Symbol.iterator](){return this.entries()}append(t,r){let n=this.#e.get(t);n===void 0?this.#e.set(t,[r]):n.push(r)}*entries(t=", "){for(let[r,n]of this.#e.entries())yield[r,n.join(t)]}*entriesAll(){for(let[t,r]of this.#e.entries())for(let n of r)yield[t,n]}get(t,r=", "){let n=this.#e.get(t);return n===void 0?null:n.join(r)}getAll(t){return this.#e.get(t)??[]}has(t){return this.#e.has(t)}keys(){return this.#e.keys()}*values(t=", "){for(let r of this.#e.values())yield r.join(t)}*valuesAll(){for(let t of this.#e.values())yield*t}};function F(e){let t=new Uint8Array(e.length),r=0;for(let n=0;n<e.length;++n){let o=e.charCodeAt(n),s;if(o>=128)throw new Error(`got non-ascii character when decoding q-quoted word: "${e}"`);o===95?s=32:o===61?(s=parseInt(e[++n],16),s*=16,s+=parseInt(e[++n],16)):s=o,t[r++]=s}return t.subarray(0,r)}var N=/^=\?([^?\s]+)\?([BQ])\?([^?\s]+)\?=$/;function B(e){let t=N.exec(e);if(t){let[,r,n,o]=t,s;if(n==="Q")s=F(o);else if(n==="B")s=f(o);else throw new Error(`unknown encoding for header value encoding: ${n}`);return new TextDecoder(r).decode(s)}else return e}async function J(e){let t=new h,r="",n="";for(;;){let{done:o,value:s}=await e.next();if(o)throw new Error("didn't find an empty line to signify the end of header parsing");let a=M.decode(s);if(/^\s/.test(a))n+=" ",n+=B(a.trimStart());else if(r&&t.append(r,n),a){let i=a.indexOf(": ");if(i===-1)throw new Error(`header line didn't have key-value delimiter: "${a}"`);r=a.slice(0,i),n=B(a.slice(i+2))}else return t}}var V=new Map([["7bit",b],["base64",x],["quoted-printable",U],["8bit",b],["binary",v]]);function Z(e){let t=e.get("Content-Type");if(t===null)throw new Error(`first headers didn't contain a content type: ${JSON.stringify(Object.fromEntries(e))}`);let r,n=!1;for(let a of t.split("; "))a.startsWith("multipart/")?n=!0:a.startsWith("boundary=")&&(r=a.slice(9),r.startsWith('"')&&r.endsWith('"')&&(r=r.slice(1,-1)));if(!n||r===void 0)throw new Error("first content type header didn't contain 'multipart/...' and a boundary string");let o=E.encode(`--${r}`),s=E.encode(`--${r}--`);return[o,s]}async function*G(e,{decoderOverrides:t=new Map}={}){let r=new Map([...V.entries(),...t.entries()]),n=new Uint8Array([13,10]),o=p(A(e),n),s=null,a=!0;for(;a;){let i=await J(o),[d,l]=s??(s=Z(i)),c=i.get("Content-Transfer-Encoding")??"7bit",w=r.get(c);if(w===void 0)throw new Error(`unhandled encoding type: ${c}`);let O=await m(w({[Symbol.asyncIterator](){return{async next(){let{done:T,value:u}=await o.next();if(T)throw new Error(`stream didn't end with the appropriate termination boundary: ${M.decode(l)}`);return g(u,d)?{done:!0,value:void 0}:g(u,l)?(a=!1,{done:!0,value:void 0}):{value:u}}}}}));yield{headers:i,content:O}}}module.exports=P(K);
@@ -0,0 +1,81 @@
1
+ /**
2
+ * class for storing headers parse from MHTML
3
+ *
4
+ * Tries to somewhat mimic the behavior of the fetch-api Headers object, with
5
+ * some differences, notably that it does no validation.
6
+ */
7
+ export declare class MhtmlHeaders {
8
+ #private;
9
+ [Symbol.iterator](): Iterator<[string, string]>;
10
+ /**
11
+ * add a key-value pair
12
+ *
13
+ * If key is already present it will be appended.
14
+ */
15
+ append(key: string, value: string): void;
16
+ /**
17
+ * iterate over all key-value pairs
18
+ *
19
+ * Multiple values for the same key will be joined in the order they were
20
+ * added by `delim`.
21
+ */
22
+ entries(delim?: string): IterableIterator<[string, string]>;
23
+ /**
24
+ * iterate over all key-value pairs
25
+ *
26
+ * Multiple values for the same key will get iterated in the order they were
27
+ * added.
28
+ */
29
+ entriesAll(): IterableIterator<[string, string]>;
30
+ /**
31
+ * get the value for a key
32
+ *
33
+ * If the key is missing, null will be returned, if multiple values for the
34
+ * key are present, they will be joined be `delim`.
35
+ */
36
+ get(key: string, delim?: string): string | null;
37
+ /**
38
+ * get all values for a key
39
+ */
40
+ getAll(key: string): string[];
41
+ /**
42
+ * whether key has any values associated with it
43
+ */
44
+ has(key: string): boolean;
45
+ /**
46
+ * all keys with at least one value
47
+ */
48
+ keys(): Iterable<string>;
49
+ /**
50
+ * iterate over all values
51
+ *
52
+ * If a key has multiple values they will be joined by `delim`.
53
+ */
54
+ values(delim?: string): IterableIterator<string>;
55
+ /**
56
+ * iterate over all values added
57
+ */
58
+ valuesAll(): IterableIterator<string>;
59
+ }
60
+ /**
61
+ * a file that was encoded in MHTML format
62
+ */
63
+ export interface MhtmlFile {
64
+ headers: MhtmlHeaders;
65
+ content: Uint8Array;
66
+ }
67
+ /**
68
+ * The decoder of any Content-Transfer-Encoding
69
+ */
70
+ export interface Decoder {
71
+ (lines: AsyncIterable<Uint8Array>): AsyncIterable<Uint8Array>;
72
+ }
73
+ /**
74
+ * parse a readable stream into an async iterator of MHTML files
75
+ *
76
+ * decoderOverrides can be used to overwrite default encoders or specify your
77
+ * own if a Content-Transfer-Encoding isn't handled properly
78
+ */
79
+ export declare function parseMhtml(stream: ReadableStream<ArrayBuffer>, { decoderOverrides, }?: {
80
+ decoderOverrides?: Map<string, Decoder>;
81
+ }): AsyncIterableIterator<MhtmlFile>;
@@ -0,0 +1 @@
1
+ var mhtmlStream=(()=>{var y=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var q=e=>y(e,"__esModule",{value:!0});var D=(e,t)=>{for(var r in t)y(e,r,{get:t[r],enumerable:!0})},L=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $(t))!k.call(e,o)&&(r||o!=="default")&&y(e,o,{get:()=>t[o],enumerable:!(n=S(t,o))||n.enumerable});return e};var P=(e=>(t,r)=>e&&e.get(t)||(r=L(q({}),t,1),e&&e.set(t,r),r))(typeof WeakMap!="undefined"?new WeakMap:0);var K={};D(K,{MhtmlHeaders:()=>h,parseMhtml:()=>G});function Q(e){return e>64&&e<91?e-65:e>96&&e<123?e-71:e>47&&e<58?e+4:e===43?62:e===47?63:0}function f(e,t){let r=e.replace(/[^A-Za-z0-9+/]/g,""),n=r.length,o=t?Math.ceil((n*3+1>>2)/t)*t:n*3+1>>2,s=new Uint8Array(o);for(let a=0,i=0,d=0;d<n;d++){let l=d&3;if(a|=Q(r.charCodeAt(d))<<6*(3-l),l===3||n-d===1){for(let c=0;c<3&&i<o;c++,i++)s[i]=a>>>(16>>>c&24)&255;a=0}}return s}function j(e){return ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e)}function g(e,t){if(e.length!==t.length)return!1;if(e.byteOffset%4===t.byteOffset%4){let r=(4-e.byteOffset%4)%4,n=Math.floor((e.byteLength-r)/4),o=r+n*4,s=new Uint32Array(e.buffer,e.byteOffset+r,n),a=new Uint32Array(t.buffer,t.byteOffset+r,n);for(let i=0;i<r;++i)if(e[i]!==t[i])return!1;for(let i=0;i<n;++i)if(s[i]!==a[i])return!1;for(let i=o;i<e.length;++i)if(e[i]!==t[i])return!1;return!0}else{for(let r of e.keys())if(e[r]!==t[r])return!1;return!0}}function A(e){return{[Symbol.asyncIterator](){let t=e.getReader();return{async next(){let r=await t.read();return r.done?{done:!0,value:void 0}:{done:!1,value:r.value}}}}}}function H(e,t){return e.findIndex((r,n)=>{if(r!==t[0]||n+t.length>e.length)return!1;for(let o=1;o<t.length;++o)if(e[n+o]!==t[o])return!1;return!0})}async function*p(e,t){let r=new Uint8Array(0);for await(let n of e){let o=j(n);r=r.length?I([r,o]):o;let s;for(;(s=H(r,t))!==-1;)yield r.subarray(0,s),r=r.subarray(s+t.length)}yield r}function I(e){let t=e.reduce((o,s)=>o+s.length,0),r=new Uint8Array(t),n=0;for(let o of e)r.set(o,n),n+=o.length;return r}async function m(e){let t=[];for await(let r of e)t.push(r);return I(t)}var R=new Uint8Array([10]);async function*U(e,t=R){for await(let r of e){let n=new Uint8Array(r.length+t.length),o=!1,s=0;for(let a=0;a<r.length;++a){let i=r[a];if(i>=128)throw new Error(`got non-ascii character when decoding quoted printable: ${r}`);if(i!==61)n[s++]=i;else{let d=r[++a];if(d===void 0)o=!0;else{let l=r[++a];if(l===void 0)throw new Error("quoted printable escape (=) was not followed by two bytes");let c=parseInt(String.fromCharCode(d),16);c*=16,c+=parseInt(String.fromCharCode(l),16),n[s++]=c}}}o||(n.set(t,s),s+=t.length),yield n.subarray(0,s)}}var W=new TextDecoder;async function*x(e){for await(let t of e)yield f(W.decode(t))}async function*b(e){for await(let t of e)yield t}function v(){throw new Error("binary transfer-encoding is explicitly not supported and trying to add an implementation will likely result in unexpected results, but if you want to ignore anyway, set binary to `decode8bit`")}var E=new TextEncoder,M=new TextDecoder,h=class{#e=new Map;[Symbol.iterator](){return this.entries()}append(t,r){let n=this.#e.get(t);n===void 0?this.#e.set(t,[r]):n.push(r)}*entries(t=", "){for(let[r,n]of this.#e.entries())yield[r,n.join(t)]}*entriesAll(){for(let[t,r]of this.#e.entries())for(let n of r)yield[t,n]}get(t,r=", "){let n=this.#e.get(t);return n===void 0?null:n.join(r)}getAll(t){return this.#e.get(t)??[]}has(t){return this.#e.has(t)}keys(){return this.#e.keys()}*values(t=", "){for(let r of this.#e.values())yield r.join(t)}*valuesAll(){for(let t of this.#e.values())yield*t}};function F(e){let t=new Uint8Array(e.length),r=0;for(let n=0;n<e.length;++n){let o=e.charCodeAt(n),s;if(o>=128)throw new Error(`got non-ascii character when decoding q-quoted word: "${e}"`);o===95?s=32:o===61?(s=parseInt(e[++n],16),s*=16,s+=parseInt(e[++n],16)):s=o,t[r++]=s}return t.subarray(0,r)}var N=/^=\?([^?\s]+)\?([BQ])\?([^?\s]+)\?=$/;function B(e){let t=N.exec(e);if(t){let[,r,n,o]=t,s;if(n==="Q")s=F(o);else if(n==="B")s=f(o);else throw new Error(`unknown encoding for header value encoding: ${n}`);return new TextDecoder(r).decode(s)}else return e}async function J(e){let t=new h,r="",n="";for(;;){let{done:o,value:s}=await e.next();if(o)throw new Error("didn't find an empty line to signify the end of header parsing");let a=M.decode(s);if(/^\s/.test(a))n+=" ",n+=B(a.trimStart());else if(r&&t.append(r,n),a){let i=a.indexOf(": ");if(i===-1)throw new Error(`header line didn't have key-value delimiter: "${a}"`);r=a.slice(0,i),n=B(a.slice(i+2))}else return t}}var V=new Map([["7bit",b],["base64",x],["quoted-printable",U],["8bit",b],["binary",v]]);function Z(e){let t=e.get("Content-Type");if(t===null)throw new Error(`first headers didn't contain a content type: ${JSON.stringify(Object.fromEntries(e))}`);let r,n=!1;for(let a of t.split("; "))a.startsWith("multipart/")?n=!0:a.startsWith("boundary=")&&(r=a.slice(9),r.startsWith('"')&&r.endsWith('"')&&(r=r.slice(1,-1)));if(!n||r===void 0)throw new Error("first content type header didn't contain 'multipart/...' and a boundary string");let o=E.encode(`--${r}`),s=E.encode(`--${r}--`);return[o,s]}async function*G(e,{decoderOverrides:t=new Map}={}){let r=new Map([...V.entries(),...t.entries()]),n=new Uint8Array([13,10]),o=p(A(e),n),s=null,a=!0;for(;a;){let i=await J(o),[d,l]=s??(s=Z(i)),c=i.get("Content-Transfer-Encoding")??"7bit",w=r.get(c);if(w===void 0)throw new Error(`unhandled encoding type: ${c}`);let O=await m(w({[Symbol.asyncIterator](){return{async next(){let{done:T,value:u}=await o.next();if(T)throw new Error(`stream didn't end with the appropriate termination boundary: ${M.decode(l)}`);return g(u,d)?{done:!0,value:void 0}:g(u,l)?(a=!1,{done:!0,value:void 0}):{value:u}}}}}));yield{headers:i,content:O}}}return P(K);})();
package/dist/index.js ADDED
@@ -0,0 +1,295 @@
1
+ import { decodeBase64 as decodeBEncoding } from "./base64";
2
+ import { asIterable, bytesEqual, collect, decodeBase64, decodeBinary, decodeIdentity, decodeQuotedPrintable, splitStream, } from "./utils";
3
+ // NOTE we use this for ascii because it's faster than manual, but may cause
4
+ // unexpected errors if the assumption is violated
5
+ const encoder = new TextEncoder();
6
+ const decoder = new TextDecoder();
7
+ /**
8
+ * class for storing headers parse from MHTML
9
+ *
10
+ * Tries to somewhat mimic the behavior of the fetch-api Headers object, with
11
+ * some differences, notably that it does no validation.
12
+ */
13
+ export class MhtmlHeaders {
14
+ #raw = new Map();
15
+ [Symbol.iterator]() {
16
+ return this.entries();
17
+ }
18
+ /**
19
+ * add a key-value pair
20
+ *
21
+ * If key is already present it will be appended.
22
+ */
23
+ append(key, value) {
24
+ const list = this.#raw.get(key);
25
+ if (list === undefined) {
26
+ this.#raw.set(key, [value]);
27
+ }
28
+ else {
29
+ list.push(value);
30
+ }
31
+ }
32
+ /**
33
+ * iterate over all key-value pairs
34
+ *
35
+ * Multiple values for the same key will be joined in the order they were
36
+ * added by `delim`.
37
+ */
38
+ *entries(delim = ", ") {
39
+ for (const [key, values] of this.#raw.entries()) {
40
+ yield [key, values.join(delim)];
41
+ }
42
+ }
43
+ /**
44
+ * iterate over all key-value pairs
45
+ *
46
+ * Multiple values for the same key will get iterated in the order they were
47
+ * added.
48
+ */
49
+ *entriesAll() {
50
+ for (const [key, values] of this.#raw.entries()) {
51
+ for (const val of values) {
52
+ yield [key, val];
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * get the value for a key
58
+ *
59
+ * If the key is missing, null will be returned, if multiple values for the
60
+ * key are present, they will be joined be `delim`.
61
+ */
62
+ get(key, delim = ", ") {
63
+ const vals = this.#raw.get(key);
64
+ if (vals === undefined) {
65
+ return null;
66
+ }
67
+ else {
68
+ return vals.join(delim);
69
+ }
70
+ }
71
+ /**
72
+ * get all values for a key
73
+ */
74
+ getAll(key) {
75
+ return this.#raw.get(key) ?? [];
76
+ }
77
+ /**
78
+ * whether key has any values associated with it
79
+ */
80
+ has(key) {
81
+ return this.#raw.has(key);
82
+ }
83
+ /**
84
+ * all keys with at least one value
85
+ */
86
+ keys() {
87
+ return this.#raw.keys();
88
+ }
89
+ /**
90
+ * iterate over all values
91
+ *
92
+ * If a key has multiple values they will be joined by `delim`.
93
+ */
94
+ *values(delim = ", ") {
95
+ for (const vals of this.#raw.values()) {
96
+ yield vals.join(delim);
97
+ }
98
+ }
99
+ /**
100
+ * iterate over all values added
101
+ */
102
+ *valuesAll() {
103
+ for (const vals of this.#raw.values()) {
104
+ yield* vals;
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * decode a q-encoded token
110
+ */
111
+ function decodeQEncoding(text) {
112
+ const res = new Uint8Array(text.length);
113
+ let destInd = 0;
114
+ for (let ind = 0; ind < text.length; ++ind) {
115
+ const code = text.charCodeAt(ind);
116
+ let val;
117
+ if (code >= 128) {
118
+ throw new Error(`got non-ascii character when decoding q-quoted word: "${text}"`);
119
+ }
120
+ else if (code === 95) {
121
+ // Q encoding replaces underscore with space
122
+ val = 32;
123
+ }
124
+ else if (code === 61) {
125
+ // encoded character
126
+ val = parseInt(text[++ind], 16);
127
+ val *= 16;
128
+ val += parseInt(text[++ind], 16);
129
+ }
130
+ else {
131
+ // residual ascii
132
+ val = code;
133
+ }
134
+ res[destInd++] = val;
135
+ }
136
+ return res.subarray(0, destInd);
137
+ }
138
+ const encodeFormat = /^=\?([^?\s]+)\?([BQ])\?([^?\s]+)\?=$/;
139
+ /**
140
+ * decode a header line that may have an extra encoding in it
141
+ */
142
+ function decodeLine(line) {
143
+ // TODO this might make more sense as a function as part of a regex replaceAll
144
+ const match = encodeFormat.exec(line);
145
+ if (match) {
146
+ const [, charset, encoding, text] = match;
147
+ let buff;
148
+ /* istanbul ignore else */
149
+ if (encoding === "Q") {
150
+ buff = decodeQEncoding(text);
151
+ }
152
+ else if (encoding === "B") {
153
+ buff = decodeBEncoding(text);
154
+ }
155
+ else {
156
+ // NOTE should be impossible
157
+ throw new Error(`unknown encoding for header value encoding: ${encoding}`);
158
+ }
159
+ return new TextDecoder(charset).decode(buff);
160
+ }
161
+ else {
162
+ return line;
163
+ }
164
+ }
165
+ /**
166
+ * parse headers from line delimited buffers
167
+ *
168
+ * @remarks
169
+ * This is not fully compatible header parsing, since we overwrite identical
170
+ * keys instead of storing multiple
171
+ */
172
+ async function parseHeaders(iter) {
173
+ const headers = new MhtmlHeaders();
174
+ let key = "";
175
+ let val = "";
176
+ for (;;) {
177
+ const { done, value } = await iter.next();
178
+ if (done) {
179
+ throw new Error("didn't find an empty line to signify the end of header parsing");
180
+ }
181
+ const line = decoder.decode(value);
182
+ if (/^\s/.test(line)) {
183
+ // header folded
184
+ val += " ";
185
+ val += decodeLine(line.trimStart());
186
+ }
187
+ else {
188
+ if (key) {
189
+ headers.append(key, val);
190
+ }
191
+ if (line) {
192
+ const delim = line.indexOf(": ");
193
+ if (delim === -1) {
194
+ throw new Error(`header line didn't have key-value delimiter: "${line}"`);
195
+ }
196
+ key = line.slice(0, delim);
197
+ val = decodeLine(line.slice(delim + 2));
198
+ }
199
+ else {
200
+ return headers;
201
+ }
202
+ }
203
+ }
204
+ }
205
+ // Default decoders
206
+ const defaultDecoders = new Map([
207
+ ["7bit", decodeIdentity],
208
+ ["base64", decodeBase64],
209
+ ["quoted-printable", decodeQuotedPrintable],
210
+ ["8bit", decodeIdentity],
211
+ ["binary", decodeBinary],
212
+ ]);
213
+ /**
214
+ * extract the boundary condition from a multipart header
215
+ *
216
+ * returns the boundary and terminating condition as Uint8Arrays
217
+ */
218
+ function getBoundary(headers) {
219
+ const contentType = headers.get("Content-Type");
220
+ if (contentType === null) {
221
+ throw new Error(`first headers didn't contain a content type: ${JSON.stringify(Object.fromEntries(headers))}`);
222
+ }
223
+ let bound = undefined;
224
+ let multipart = false;
225
+ for (const field of contentType.split("; ")) {
226
+ if (field.startsWith("multipart/")) {
227
+ multipart = true;
228
+ }
229
+ else if (field.startsWith("boundary=")) {
230
+ bound = field.slice(9);
231
+ // TODO handling of quoted fields is not great
232
+ if (bound.startsWith('"') && bound.endsWith('"')) {
233
+ bound = bound.slice(1, -1);
234
+ }
235
+ }
236
+ }
237
+ if (!multipart || bound === undefined) {
238
+ throw new Error(`first content type header didn't contain 'multipart/...' and a boundary string`);
239
+ }
240
+ const boundary = encoder.encode(`--${bound}`);
241
+ const terminus = encoder.encode(`--${bound}--`);
242
+ return [boundary, terminus];
243
+ }
244
+ /**
245
+ * parse a readable stream into an async iterator of MHTML files
246
+ *
247
+ * decoderOverrides can be used to overwrite default encoders or specify your
248
+ * own if a Content-Transfer-Encoding isn't handled properly
249
+ */
250
+ export async function* parseMhtml(stream, { decoderOverrides = new Map(), } = {}) {
251
+ // initial setup
252
+ const decoders = new Map([
253
+ ...defaultDecoders.entries(),
254
+ ...decoderOverrides.entries(),
255
+ ]);
256
+ const crlf = new Uint8Array([13, 10]);
257
+ const lines = splitStream(asIterable(stream), crlf);
258
+ let bound = null;
259
+ let cont = true;
260
+ while (cont) {
261
+ // parse out headers and get encoding for content
262
+ const headers = await parseHeaders(lines);
263
+ const [boundary, terminus] = bound ?? (bound = getBoundary(headers));
264
+ const encoding = headers.get("Content-Transfer-Encoding") ?? "7bit";
265
+ const decode = decoders.get(encoding);
266
+ if (decode === undefined) {
267
+ throw new Error(`unhandled encoding type: ${encoding}`);
268
+ }
269
+ // create line iterator that only iterates over content lines (checking for
270
+ // the boundaries), then pass into decoder
271
+ const content = await collect(decode({
272
+ [Symbol.asyncIterator]() {
273
+ return {
274
+ async next() {
275
+ const { done, value } = await lines.next();
276
+ if (done) {
277
+ throw new Error(`stream didn't end with the appropriate termination boundary: ${decoder.decode(terminus)}`);
278
+ }
279
+ else if (bytesEqual(value, boundary)) {
280
+ return { done: true, value: undefined };
281
+ }
282
+ else if (bytesEqual(value, terminus)) {
283
+ cont = false;
284
+ return { done: true, value: undefined };
285
+ }
286
+ else {
287
+ return { value };
288
+ }
289
+ },
290
+ };
291
+ },
292
+ }));
293
+ yield { headers, content };
294
+ }
295
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * create a Uint8Array from a buffer
3
+ *
4
+ * This method is necessary since creating a Uint8Array from a view will copy
5
+ * the elements instead of creating a view of the underling buffer.
6
+ */
7
+ export declare function toBytes(buff: ArrayBuffer): Uint8Array;
8
+ /**
9
+ * Compare two byte arrays for equality
10
+ */
11
+ export declare function bytesEqual(left: Uint8Array, right: Uint8Array): boolean;
12
+ /**
13
+ * convert readable stream to async iterator
14
+ */
15
+ export declare function asIterable<T extends ArrayBuffer>(stream: ReadableStream<T>): AsyncIterable<T>;
16
+ /**
17
+ * Find index of one byte array in another
18
+ */
19
+ export declare function indexOf(haystack: Uint8Array, needle: Uint8Array): number;
20
+ /**
21
+ * Split a stream of bytes
22
+ *
23
+ * Takes a stream of data modeled as an async iterator of ArrayBuffer for
24
+ * compatibility between node and web, and splits it into an async iterator
25
+ * where each value is delimited by the split sequence.
26
+ */
27
+ export declare function splitStream(iter: AsyncIterable<ArrayBuffer>, split: Uint8Array): AsyncIterableIterator<Uint8Array>;
28
+ /**
29
+ * collect an async iterable of buffers into one
30
+ */
31
+ export declare function collect(stream: AsyncIterable<Uint8Array>): Promise<Uint8Array>;
32
+ /**
33
+ * decoder for quoted printable
34
+ *
35
+ * If quoted printable "lines" aren't escaped with an "=" then a new line needs
36
+ * to be inserted. We use `newLine` which defaults to a single "\n".
37
+ */
38
+ export declare function decodeQuotedPrintable(lines: AsyncIterable<Uint8Array>, newLine?: Uint8Array): AsyncIterableIterator<Uint8Array>;
39
+ /**
40
+ * decoder for base64
41
+ */
42
+ export declare function decodeBase64(lines: AsyncIterable<Uint8Array>): AsyncIterableIterator<Uint8Array>;
43
+ /**
44
+ * decoder for 7bit and 8bit
45
+ */
46
+ export declare function decodeIdentity(lines: AsyncIterable<Uint8Array>): AsyncIterableIterator<Uint8Array>;
47
+ /**
48
+ * decoder for binary
49
+ *
50
+ * For implementation reasons, binary can't be supported, so we throw a special
51
+ * error.
52
+ */
53
+ export declare function decodeBinary(): never;
package/dist/utils.js ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Utilities
3
+ *
4
+ * Most of these use Uint8Array explicitely because we don't care about a raw
5
+ * buffer, but about views of bytes, and this makes sure that we handle the
6
+ * types appropriately.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { decodeBase64 as db64 } from "./base64";
11
+ /**
12
+ * create a Uint8Array from a buffer
13
+ *
14
+ * This method is necessary since creating a Uint8Array from a view will copy
15
+ * the elements instead of creating a view of the underling buffer.
16
+ */
17
+ export function toBytes(buff) {
18
+ if (ArrayBuffer.isView(buff)) {
19
+ return new Uint8Array(buff.buffer, buff.byteOffset, buff.byteLength);
20
+ }
21
+ else {
22
+ return new Uint8Array(buff);
23
+ }
24
+ }
25
+ /**
26
+ * Compare two byte arrays for equality
27
+ */
28
+ export function bytesEqual(left, right) {
29
+ if (left.length !== right.length) {
30
+ return false;
31
+ }
32
+ else if (left.byteOffset % 4 === right.byteOffset % 4) {
33
+ // words align, can use fast path
34
+ const begin = (4 - (left.byteOffset % 4)) % 4;
35
+ const fourLen = Math.floor((left.byteLength - begin) / 4);
36
+ const end = begin + fourLen * 4;
37
+ const left32 = new Uint32Array(left.buffer, left.byteOffset + begin, fourLen);
38
+ const right32 = new Uint32Array(right.buffer, right.byteOffset + begin, fourLen);
39
+ for (let i = 0; i < begin; ++i) {
40
+ if (left[i] !== right[i]) {
41
+ return false;
42
+ }
43
+ }
44
+ for (let i = 0; i < fourLen; ++i) {
45
+ if (left32[i] !== right32[i]) {
46
+ return false;
47
+ }
48
+ }
49
+ for (let i = end; i < left.length; ++i) {
50
+ if (left[i] !== right[i]) {
51
+ return false;
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+ else {
57
+ // slower byte path
58
+ for (const ind of left.keys()) {
59
+ if (left[ind] !== right[ind]) {
60
+ return false;
61
+ }
62
+ }
63
+ return true;
64
+ }
65
+ }
66
+ /**
67
+ * convert readable stream to async iterator
68
+ */
69
+ export function asIterable(stream) {
70
+ return {
71
+ [Symbol.asyncIterator]() {
72
+ const reader = stream.getReader();
73
+ return {
74
+ // NOTE this complexity is for typescript
75
+ async next() {
76
+ const res = await reader.read();
77
+ if (res.done) {
78
+ return { done: true, value: undefined };
79
+ }
80
+ else {
81
+ return { done: false, value: res.value };
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * Find index of one byte array in another
90
+ */
91
+ export function indexOf(haystack, needle) {
92
+ return haystack.findIndex((val, ind) => {
93
+ if (val !== needle[0] || ind + needle.length > haystack.length) {
94
+ return false;
95
+ }
96
+ else {
97
+ for (let i = 1; i < needle.length; ++i) {
98
+ if (haystack[ind + i] !== needle[i]) {
99
+ return false;
100
+ }
101
+ }
102
+ return true;
103
+ }
104
+ });
105
+ }
106
+ /**
107
+ * Split a stream of bytes
108
+ *
109
+ * Takes a stream of data modeled as an async iterator of ArrayBuffer for
110
+ * compatibility between node and web, and splits it into an async iterator
111
+ * where each value is delimited by the split sequence.
112
+ */
113
+ export async function* splitStream(iter, split) {
114
+ let current = new Uint8Array(0);
115
+ for await (const chunk of iter) {
116
+ const next = toBytes(chunk);
117
+ current = current.length ? concat([current, next]) : next;
118
+ let nextInd;
119
+ while ((nextInd = indexOf(current, split)) !== -1) {
120
+ yield current.subarray(0, nextInd);
121
+ current = current.subarray(nextInd + split.length);
122
+ }
123
+ }
124
+ yield current;
125
+ }
126
+ /**
127
+ * concatenate multiple buffers
128
+ */
129
+ function concat(chunks) {
130
+ const totalBytes = chunks.reduce((t, c) => t + c.length, 0);
131
+ const res = new Uint8Array(totalBytes);
132
+ let offset = 0;
133
+ for (const chunk of chunks) {
134
+ res.set(chunk, offset);
135
+ offset += chunk.length;
136
+ }
137
+ return res;
138
+ }
139
+ /**
140
+ * collect an async iterable of buffers into one
141
+ */
142
+ export async function collect(stream) {
143
+ const chunks = [];
144
+ for await (const chunk of stream) {
145
+ chunks.push(chunk);
146
+ }
147
+ return concat(chunks);
148
+ }
149
+ // default new line character for quoted printable decoding
150
+ const defaultNewLine = new Uint8Array([10]);
151
+ /**
152
+ * decoder for quoted printable
153
+ *
154
+ * If quoted printable "lines" aren't escaped with an "=" then a new line needs
155
+ * to be inserted. We use `newLine` which defaults to a single "\n".
156
+ */
157
+ export async function* decodeQuotedPrintable(lines, newLine = defaultNewLine) {
158
+ for await (const bytes of lines) {
159
+ const res = new Uint8Array(bytes.length + newLine.length);
160
+ let softLine = false; // if newline wasn't escaped so we need to add
161
+ let destInd = 0;
162
+ for (let ind = 0; ind < bytes.length; ++ind) {
163
+ const code = bytes[ind];
164
+ if (code >= 128) {
165
+ throw new Error(`got non-ascii character when decoding quoted printable: ${bytes}`);
166
+ }
167
+ if (code !== 61) {
168
+ res[destInd++] = code;
169
+ }
170
+ else {
171
+ // escaped char
172
+ const first = bytes[++ind];
173
+ if (first === undefined) {
174
+ // soft newline
175
+ softLine = true;
176
+ }
177
+ else {
178
+ const second = bytes[++ind];
179
+ if (second === undefined) {
180
+ throw new Error("quoted printable escape (=) was not followed by two bytes");
181
+ }
182
+ let val = parseInt(String.fromCharCode(first), 16);
183
+ val *= 16;
184
+ val += parseInt(String.fromCharCode(second), 16);
185
+ res[destInd++] = val;
186
+ }
187
+ }
188
+ }
189
+ if (!softLine) {
190
+ res.set(newLine, destInd);
191
+ destInd += newLine.length;
192
+ }
193
+ yield res.subarray(0, destInd);
194
+ }
195
+ }
196
+ const decoder = new TextDecoder();
197
+ /**
198
+ * decoder for base64
199
+ */
200
+ export async function* decodeBase64(lines) {
201
+ for await (const bytes of lines) {
202
+ yield db64(decoder.decode(bytes));
203
+ }
204
+ }
205
+ /**
206
+ * decoder for 7bit and 8bit
207
+ */
208
+ export async function* decodeIdentity(lines) {
209
+ for await (const bytes of lines) {
210
+ yield bytes;
211
+ }
212
+ }
213
+ /**
214
+ * decoder for binary
215
+ *
216
+ * For implementation reasons, binary can't be supported, so we throw a special
217
+ * error.
218
+ */
219
+ export function decodeBinary() {
220
+ throw new Error("binary transfer-encoding is explicitly not supported and trying to add an implementation will likely result in unexpected results, but if you want to ignore anyway, set binary to `decode8bit`");
221
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "mhtml-stream",
3
+ "version": "0.0.1",
4
+ "description": "Zero dependency stream MHTML parser",
5
+ "keywords": [
6
+ "mhtml",
7
+ "stream"
8
+ ],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/erikbrinkman/mhtml-stream.git"
12
+ },
13
+ "author": "Erik Brinkman <erik.brinkman@gmail.com>",
14
+ "license": "MIT",
15
+ "module": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "main": "dist/index.cjs.min.js",
18
+ "unpkg": "dist/index.iife.min.js",
19
+ "files": [
20
+ "/dist/**/*.js",
21
+ "/dist/**/*.d.ts"
22
+ ],
23
+ "type": "module",
24
+ "scripts": {
25
+ "fmt": "prettier --write 'src/*.ts' '*.json'",
26
+ "lint": "tsc && eslint 'src/*.ts'",
27
+ "test": "jest --coverage",
28
+ "doc": "typedoc",
29
+ "build": "tsc -p tsconfig.build.json && esbuild src/index.ts --bundle --minify --format=cjs --outfile=dist/index.cjs.min.js && esbuild src/index.ts --bundle --minify --global-name=mhtmlStream --outfile=dist/index.iife.min.js",
30
+ "prepare": "yarn lint && yarn test && yarn build"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^17.0.14",
34
+ "esbuild": "^0.14.17",
35
+ "@babel/core": "^7.16.12",
36
+ "@babel/preset-env": "^7.16.11",
37
+ "@babel/preset-typescript": "^7.16.7",
38
+ "@types/jest": "^27.4.0",
39
+ "@typescript-eslint/eslint-plugin": "^5.10.0",
40
+ "@typescript-eslint/parser": "^5.10.0",
41
+ "babel-jest": "^27.4.6",
42
+ "eslint": "^8.7.0",
43
+ "eslint-config-prettier": "^8.3.0",
44
+ "eslint-plugin-jest": "^26.0.0",
45
+ "eslint-plugin-spellcheck": "^0.0.19",
46
+ "jest": "^27.4.7",
47
+ "prettier": "^2.5.1",
48
+ "prettier-plugin-organize-imports": "^2.3.4",
49
+ "typedoc": "^0.22.11",
50
+ "typescript": "^4.5.5"
51
+ }
52
+ }