mhtml-stream
Version:
Zero dependency stream MHTML parser
185 lines (184 loc) • 5.75 kB
JavaScript
/**
* Utilities
*
* Most of these use Uint8Array explicitely because we don't care about a raw
* buffer, but about views of bytes, and this makes sure that we handle the
* types appropriately.
*
* @packageDocumentation
*/
import { toByteArray } from "base64-js";
/**
* Compare two byte arrays for equality
*/
export function bytesEqual(left, right) {
if (left.length !== right.length) {
return false;
}
else if (left.byteOffset % 4 === right.byteOffset % 4) {
// words align, can use fast path
const begin = (4 - (left.byteOffset % 4)) % 4;
const fourLen = Math.floor((left.byteLength - begin) / 4);
const end = begin + fourLen * 4;
const left32 = new Uint32Array(left.buffer, left.byteOffset + begin, fourLen);
const right32 = new Uint32Array(right.buffer, right.byteOffset + begin, fourLen);
for (let i = 0; i < begin; ++i) {
if (left[i] !== right[i]) {
return false;
}
}
for (let i = 0; i < fourLen; ++i) {
if (left32[i] !== right32[i]) {
return false;
}
}
for (let i = end; i < left.length; ++i) {
if (left[i] !== right[i]) {
return false;
}
}
return true;
}
else {
// slower byte path
for (const ind of left.keys()) {
if (left[ind] !== right[ind]) {
return false;
}
}
return true;
}
}
/**
* Find index of one byte array in another
*/
export function indexOf(haystack, needle) {
return haystack.findIndex((val, ind) => {
if (val !== needle[0] || ind + needle.length > haystack.length) {
return false;
}
else {
for (let i = 1; i < needle.length; ++i) {
if (haystack[ind + i] !== needle[i]) {
return false;
}
}
return true;
}
});
}
/**
* Split a stream of bytes
*
* Takes a stream of data modeled as an async iterator of ArrayBuffer for
* compatibility between node and web, and splits it into an async iterator
* where each value is delimited by the split sequence.
*/
export async function* splitStream(iter, split) {
let current = new Uint8Array(0);
for await (const chunk of iter) {
current = current.length ? concat([current, chunk]) : chunk;
let nextInd;
while ((nextInd = indexOf(current, split)) !== -1) {
yield current.subarray(0, nextInd);
current = current.subarray(nextInd + split.length);
}
}
yield current;
}
/**
* concatenate multiple buffers
*/
function concat(chunks) {
const totalBytes = chunks.reduce((t, c) => t + c.length, 0);
const res = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
res.set(chunk, offset);
offset += chunk.length;
}
return res;
}
/**
* collect an async iterable of buffers into one
*/
export async function collect(stream) {
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
return concat(chunks);
}
// default new line character for quoted printable decoding
const defaultNewLine = new Uint8Array([10]);
/**
* decoder for quoted printable
*
* If quoted printable "lines" aren't escaped with an "=" then a new line needs
* to be inserted. We use `newLine` which defaults to a single "\n".
*/
export async function* decodeQuotedPrintable(lines, newLine = defaultNewLine) {
for await (const bytes of lines) {
const res = new Uint8Array(bytes.length + newLine.length);
let softLine = false; // if newline wasn't escaped so we need to add
let destInd = 0;
for (let ind = 0; ind < bytes.length; ++ind) {
const code = bytes[ind];
if (code >= 128) {
throw new Error(`got non-ascii character when decoding quoted printable: ${code}`);
}
if (code !== 61) {
res[destInd++] = code;
}
else {
// escaped char
const first = bytes[++ind];
if (first === undefined) {
// soft newline
softLine = true;
}
else {
const second = bytes[++ind];
if (second === undefined) {
throw new Error("quoted printable escape (=) was not followed by two bytes");
}
let val = parseInt(String.fromCharCode(first), 16);
val *= 16;
val += parseInt(String.fromCharCode(second), 16);
res[destInd++] = val;
}
}
}
if (!softLine) {
res.set(newLine, destInd);
destInd += newLine.length;
}
yield res.subarray(0, destInd);
}
}
const decoder = new TextDecoder();
/**
* decoder for base64
*/
export async function* decodeBase64(lines) {
for await (const bytes of lines) {
yield toByteArray(decoder.decode(bytes));
}
}
/**
* decoder for 7bit and 8bit
*/
export async function* decodeIdentity(lines) {
for await (const bytes of lines) {
yield bytes;
}
}
/**
* decoder for binary
*
* For implementation reasons, binary can't be supported, so we throw a special
* error.
*/
export function decodeBinary() {
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`");
}