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 +21 -0
- package/README.md +27 -0
- package/dist/base64.d.ts +8 -0
- package/dist/base64.js +77 -0
- package/dist/index.cjs.min.js +1 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.iife.min.js +1 -0
- package/dist/index.js +295 -0
- package/dist/utils.d.ts +53 -0
- package/dist/utils.js +221 -0
- package/package.json +52 -0
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
|
+
[](https://github.com/erikbrinkman/mhtml-stream/actions/workflows/node.js.yml)
|
|
4
|
+
[](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
|
+
```
|
package/dist/base64.d.ts
ADDED
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);
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|