code:
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.jsonExt = factory());
})(this, (function () { 'use strict';

    var version = "0.5.7";

    const PrimitiveType = 1;
    const ObjectType = 2;
    const ArrayType = 3;
    const PromiseType = 4;
    const ReadableStringType = 5;
    const ReadableObjectType = 6;
    // https://tc39.es/ecma262/#table-json-single-character-escapes
    const escapableCharCodeSubstitution$1 = { // JSON Single Character Escape Sequences
        0x08: '\\b',
        0x09: '\\t',
        0x0a: '\\n',
        0x0c: '\\f',
        0x0d: '\\r',
        0x22: '\\\"',
        0x5c: '\\\\'
    };

    function isLeadingSurrogate$1(code) {
        return code >= 0xD800 && code <= 0xDBFF;
    }

    function isTrailingSurrogate$1(code) {
        return code >= 0xDC00 && code <= 0xDFFF;
    }

    function isReadableStream$1(value) {
        return (
            typeof value.pipe === 'function' &&
            typeof value._read === 'function' &&
            typeof value._readableState === 'object' && value._readableState !== null
        );
    }

    function replaceValue$1(holder, key, value, replacer) {
        if (value && typeof value.toJSON === 'function') {
            value = value.toJSON();
        }

        if (replacer !== null) {
            value = replacer.call(holder, String(key), value);
        }

        switch (typeof value) {
            case 'function':
            case 'symbol':
                value = undefined;
                break;

            case 'object':
                if (value !== null) {
                    const cls = value.constructor;
                    if (cls === String || cls === Number || cls === Boolean) {
                        value = value.valueOf();
                    }
                }
                break;
        }

        return value;
    }

    function getTypeNative$1(value) {
        if (value === null || typeof value !== 'object') {
            return PrimitiveType;
        }

        if (Array.isArray(value)) {
            return ArrayType;
        }

        return ObjectType;
    }

    function getTypeAsync$1(value) {
        if (value === null || typeof value !== 'object') {
            return PrimitiveType;
        }

        if (typeof value.then === 'function') {
            return PromiseType;
        }

        if (isReadableStream$1(value)) {
            return value._readableState.objectMode ? ReadableObjectType : ReadableStringType;
        }

        if (Array.isArray(value)) {
            return ArrayType;
        }

        return ObjectType;
    }

    function normalizeReplacer$1(replacer) {
        if (typeof replacer === 'function') {
            return replacer;
        }

        if (Array.isArray(replacer)) {
            const allowlist = new Set(replacer
                .map(item => {
                    const cls = item && item.constructor;
                    return cls === String || cls === Number ? String(item) : null;
                })
                .filter(item => typeof item === 'string')
            );

            return [...allowlist];
        }

        return null;
    }

    function normalizeSpace$1(space) {
        if (typeof space === 'number') {
            if (!Number.isFinite(space) || space < 1) {
                return false;
            }

            return ' '.repeat(Math.min(space, 10));
        }

        if (typeof space === 'string') {
            return space.slice(0, 10) || false;
        }

        return false;
    }

    var utils = {
        escapableCharCodeSubstitution: escapableCharCodeSubstitution$1,
        isLeadingSurrogate: isLeadingSurrogate$1,
        isTrailingSurrogate: isTrailingSurrogate$1,
        type: {
            PRIMITIVE: PrimitiveType,
            PROMISE: PromiseType,
            ARRAY: ArrayType,
            OBJECT: ObjectType,
            STRING_STREAM: ReadableStringType,
            OBJECT_STREAM: ReadableObjectType
        },

        isReadableStream: isReadableStream$1,
        replaceValue: replaceValue$1,
        getTypeNative: getTypeNative$1,
        getTypeAsync: getTypeAsync$1,
        normalizeReplacer: normalizeReplacer$1,
        normalizeSpace: normalizeSpace$1
    };

    const {
        normalizeReplacer,
        normalizeSpace,
        replaceValue,
        getTypeNative,
        getTypeAsync,
        isLeadingSurrogate,
        isTrailingSurrogate,
        escapableCharCodeSubstitution,
        type: {
            PRIMITIVE,
            OBJECT,
            ARRAY,
            PROMISE,
            STRING_STREAM,
            OBJECT_STREAM
        }
    } = utils;
    const charLength2048 = Array.from({ length: 2048 }).map((_, code) => {
        if (escapableCharCodeSubstitution.hasOwnProperty(code)) {
            return 2; // \X
        }

        if (code < 0x20) {
            return 6; // \uXXXX
        }

        return code < 128 ? 1 : 2; // UTF8 bytes
    });

    function stringLength(str) {
        let len = 0;
        let prevLeadingSurrogate = false;

        for (let i = 0; i < str.length; i++) {
            const code = str.charCodeAt(i);

            if (code < 2048) {
                len += charLength2048[code];
            } else if (isLeadingSurrogate(code)) {
                len += 6; // \uXXXX since no pair with trailing surrogate yet
                prevLeadingSurrogate = true;
                continue;
            } else if (isTrailingSurrogate(code)) {
                len = prevLeadingSurrogate
                    ? len - 2  // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes
                    : len + 6; // \uXXXX
            } else {
                len += 3; // code >= 2048 is 3 bytes length for UTF8
            }

            prevLeadingSurrogate = false;
        }

        return len + 2; // +2 for quotes
    }

    function primitiveLength(value) {
        switch (typeof value) {
            case 'string':
                return stringLength(value);

            case 'number':
                return Number.isFinite(value) ? String(value).length : 4 /* null */;

            case 'boolean':
                return value ? 4 /* true */ : 5 /* false */;

            case 'undefined':
            case 'object':
                return 4; /* null */

            default:
                return 0;
        }
    }

    function spaceLength(space) {
        space = normalizeSpace(space);
        return typeof space === 'string' ? space.length : 0;
    }

    var stringifyInfo = function jsonStringifyInfo(value, replacer, space, options) {
        function walk(holder, key, value) {
            if (stop) {
                return;
            }

            value = replaceValue(holder, key, value, replacer);

            let type = getType(value);

            // check for circular structure
            if (type !== PRIMITIVE && stack.has(value)) {
                circular.add(value);
                length += 4; // treat as null

                if (!options.continueOnCircular) {
                    stop = true;
                }

                return;
            }

            switch (type) {
                case PRIMITIVE:
                    if (value !== undefined || Array.isArray(holder)) {
                        length += primitiveLength(value);
                    } else if (holder === root) {
                        length += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null?
                    }
                    break;

                case OBJECT: {
                    if (visited.has(value)) {
                        duplicate.add(value);
                        length += visited.get(value);
                        break;
                    }

                    const valueLength = length;
                    let entries = 0;

                    length += 2; // {}

                    stack.add(value);

                    for (const key in value) {
                        if (hasOwnProperty.call(value, key) && (allowlist === null || allowlist.has(key))) {
                            const prevLength = length;
                            walk(value, key, value[key]);

                            if (prevLength !== length) {
                                // value is printed
                                length += stringLength(key) + 1; // "key":
                                entries++;
                            }
                        }
                    }

                    if (entries > 1) {
                        length += entries - 1; // commas
                    }

                    stack.delete(value);

                    if (space > 0 && entries > 0) {
                        length += (1 + (stack.size + 1) * space + 1) * entries; // for each key-value: \n{space}
                        length += 1 + stack.size * space; // for }
                    }

                    visited.set(value, length - valueLength);

                    break;
                }

                case ARRAY: {
                    if (visited.has(value)) {
                        duplicate.add(value);
                        length += visited.get(value);
                        break;
                    }

                    const valueLength = length;

                    length += 2; // []

                    stack.add(value);

                    for (let i = 0; i < value.length; i++) {
                        walk(value, i, value[i]);
                    }

                    if (value.length > 1) {
                        length += value.length - 1; // commas
                    }

                    stack.delete(value);

                    if (space > 0 && value.length > 0) {
                        length += (1 + (stack.size + 1) * space) * value.length; // for each element: \n{space}
                        length += 1 + stack.size * space; // for ]
                    }

                    visited.set(value, length - valueLength);

                    break;
                }

                case PROMISE:
                case STRING_STREAM:
                    async.add(value);
                    break;

                case OBJECT_STREAM:
                    length += 2; // []
                    async.add(value);
                    break;
            }
        }

        let allowlist = null;
        replacer = normalizeReplacer(replacer);

        if (Array.isArray(replacer)) {
            allowlist = new Set(replacer);
            replacer = null;
        }

        space = spaceLength(space);
        options = options || {};

        const visited = new Map();
        const stack = new Set();
        const duplicate = new Set();
        const circular = new Set();
        const async = new Set();
        const getType = options.async ? getTypeAsync : getTypeNative;
        const root = { '': value };
        let stop = false;
        let length = 0;

        walk(root, '', value);

        return {
            minLength: isNaN(length) ? Infinity : length,
            circular: [...circular],
            duplicate: [...duplicate],
            async: [...async]
        };
    };

    var stringifyStreamBrowser = () => {
        throw new Error('Method is not supported');
    };

    var textDecoderBrowser = TextDecoder;

    const { isReadableStream } = utils;

    const STACK_OBJECT = 1;
    const STACK_ARRAY = 2;
    const decoder = new textDecoderBrowser();

    function isObject(value) {
        return value !== null && typeof value === 'object';
    }

    function adjustPosition(error, parser) {
        if (error.name === 'SyntaxError' && parser.jsonParseOffset) {
            error.message = error.message.replace(/at position (\d+)/, (_, pos) =>
                'at position ' + (Number(pos) + parser.jsonParseOffset)
            );
        }

        return error;
    }

    function append(array, elements) {
        // Note: Avoid to use array.push(...elements) since it may lead to
        // "RangeError: Maximum call stack size exceeded" for a long arrays
        const initialLength = array.length;
        array.length += elements.length;

        for (let i = 0; i < elements.length; i++) {
            array[initialLength + i] = elements[i];
        }
    }

    var parseChunked = function(chunkEmitter) {
        let parser = new ChunkParser();

        if (isObject(chunkEmitter) && isReadableStream(chunkEmitter)) {
            return new Promise((resolve, reject) => {
                chunkEmitter
                    .on('data', chunk => {
                        try {
                            parser.push(chunk);
                        } catch (e) {
                            reject(adjustPosition(e, parser));
                            parser = null;
                        }
                    })
                    .on('error', (e) => {
                        parser = null;
                        reject(e);
                    })
                    .on('end', () => {
                        try {
                            resolve(parser.finish());
                        } catch (e) {
                            reject(adjustPosition(e, parser));
                        } finally {
                            parser = null;
                        }
                    });
            });
        }

        if (typeof chunkEmitter === 'function') {
            const iterator = chunkEmitter();

            if (isObject(iterator) && (Symbol.iterator in iterator || Symbol.asyncIterator in iterator)) {
                return new Promise(async (resolve, reject) => {
                    try {
                        for await (const chunk of iterator) {
                            parser.push(chunk);
                        }

                        resolve(parser.finish());
                    } catch (e) {
                        reject(adjustPosition(e, parser));
                    } finally {
                        parser = null;
                    }
                });
            }
        }

        throw new Error(
            'Chunk emitter should be readable stream, generator, ' +
            'async generator or function returning an iterable object'
        );
    };

    class ChunkParser {
        constructor() {
            this.value = undefined;
            this.valueStack = null;

            this.stack = new Array(100);
            this.lastFlushDepth = 0;
            this.flushDepth = 0;
            this.stateString = false;
            this.stateStringEscape = false;
            this.pendingByteSeq = null;
            this.pendingChunk = null;
            this.chunkOffset = 0;
            this.jsonParseOffset = 0;
        }

        parseAndAppend(fragment, wrap) {
            // Append new entries or elements
            if (this.stack[this.lastFlushDepth - 1] === STACK_OBJECT) {
                if (wrap) {
                    this.jsonParseOffset--;
                    fragment = '{' + fragment + '}';
                }

                Object.assign(this.valueStack.value, JSON.parse(fragment));
            } else {
                if (wrap) {
                    this.jsonParseOffset--;
                    fragment = '[' + fragment + ']';
                }

                append(this.valueStack.value, JSON.parse(fragment));
            }
        }

        prepareAddition(fragment) {
            const { value } = this.valueStack;
            const expectComma = Array.isArray(value)
                ? value.length !== 0
                : Object.keys(value).length !== 0;

            if (expectComma) {
                // Skip a comma at the beginning of fragment, otherwise it would
                // fail to parse
                if (fragment[0] === ',') {
                    this.jsonParseOffset++;
                    return fragment.slice(1);
                }

                // When value (an object or array) is not empty and a fragment
                // doesn't start with a comma, a single valid fragment starting
                // is a closing bracket. If it's not, a prefix is adding to fail
                // parsing. Otherwise, the sequence of chunks can be successfully
                // parsed, although it should not, e.g. ["[{}", "{}]"]
                if (fragment[0] !== '}' && fragment[0] !== ']') {
                    this.jsonParseOffset -= 3;
                    return '[[]' + fragment;
                }
            }

            return fragment;
        }

        flush(chunk, start, end) {
            let fragment = chunk.slice(start, end);

            // Save position correction an error in JSON.parse() if any
            this.jsonParseOffset = this.chunkOffset + start;

            // Prepend pending chunk if any
            if (this.pendingChunk !== null) {
                fragment = this.pendingChunk + fragment;
                this.jsonParseOffset -= this.pendingChunk.length;
                this.pendingChunk = null;
            }

            if (this.flushDepth === this.lastFlushDepth) {
                // Depth didn't changed, so it's a root value or entry/element set
                if (this.flushDepth > 0) {
                    this.parseAndAppend(this.prepareAddition(fragment), true);
                } else {
                    // That's an entire value on a top level
                    this.value = JSON.parse(fragment);
                    this.valueStack = {
                        value: this.value,
                        prev: null
                    };
                }
            } else if (this.flushDepth > this.lastFlushDepth) {
                // Add missed closing brackets/parentheses
                for (let i = this.flushDepth - 1; i >= this.lastFlushDepth; i--) {
                    fragment += this.stack[i] === STACK_OBJECT ? '}' : ']';
                }

                if (this.lastFlushDepth === 0) {
                    // That's a root value
                    this.value = JSON.parse(fragment);
                    this.valueStack = {
                        value: this.value,
                        prev: null
                    };
                } else {
                    this.parseAndAppend(this.prepareAddition(fragment), true);
                }

                // Move down to the depths to the last object/array, which is current now
                for (let i = this.lastFlushDepth || 1; i < this.flushDepth; i++) {
                    let value = this.valueStack.value;

                    if (this.stack[i - 1] === STACK_OBJECT) {
                        // find last entry
                        let key;
                        // eslint-disable-next-line curly
                        for (key in value);
                        value = value[key];
                    } else {
                        // last element
                        value = value[value.length - 1];
                    }

                    this.valueStack = {
                        value,
                        prev: this.valueStack
                    };
                }
            } else /* this.flushDepth < this.lastFlushDepth */ {
                fragment = this.prepareAddition(fragment);

                // Add missed opening brackets/parentheses
                for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
                    this.jsonParseOffset--;
                    fragment = (this.stack[i] === STACK_OBJECT ? '{' : '[') + fragment;
                }

                this.parseAndAppend(fragment, false);

                for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
                    this.valueStack = this.valueStack.prev;
                }
            }

            this.lastFlushDepth = this.flushDepth;
        }

        push(chunk) {
            if (typeof chunk !== 'string') {
                // Suppose chunk is Buffer or Uint8Array

                // Prepend uncompleted byte sequence if any
                if (this.pendingByteSeq !== null) {
                    const origRawChunk = chunk;
                    chunk = new Uint8Array(this.pendingByteSeq.length + origRawChunk.length);
                    chunk.set(this.pendingByteSeq);
                    chunk.set(origRawChunk, this.pendingByteSeq.length);
                    this.pendingByteSeq = null;
                }

                // In case Buffer/Uint8Array, an input is encoded in UTF8
                // Seek for parts of uncompleted UTF8 symbol on the ending
                // This makes sense only if we expect more chunks and last char is not multi-bytes
                if (chunk[chunk.length - 1] > 127) {
                    for (let seqLength = 0; seqLength < chunk.length; seqLength++) {
                        const byte = chunk[chunk.length - 1 - seqLength];

                        // 10xxxxxx - 2nd, 3rd or 4th byte
                        // 110xxxxx – first byte of 2-byte sequence
                        // 1110xxxx - first byte of 3-byte sequence
                        // 11110xxx - first byte of 4-byte sequence
                        if (byte >> 6 === 3) {
                            seqLength++;

                            // If the sequence is really incomplete, then preserve it
                            // for the future chunk and cut off it from the current chunk
                            if ((seqLength !== 4 && byte >> 3 === 0b11110) ||
                                (seqLength !== 3 && byte >> 4 === 0b1110) ||
                                (seqLength !== 2 && byte >> 5 === 0b110)) {
                                this.pendingByteSeq = chunk.slice(chunk.length - seqLength);
                                chunk = chunk.slice(0, -seqLength);
                            }

                            break;
                        }
                    }
                }

                // Convert chunk to a string, since single decode per chunk
                // is much effective than decode multiple small substrings
                chunk = decoder.decode(chunk);
            }

            const chunkLength = chunk.length;
            let lastFlushPoint = 0;
            let flushPoint = 0;

            // Main scan loop
            scan: for (let i = 0; i < chunkLength; i++) {
                if (this.stateString) {
                    for (; i < chunkLength; i++) {
                        if (this.stateStringEscape) {
                            this.stateStringEscape = false;
                        } else {
                            switch (chunk.charCodeAt(i)) {
                                case 0x22: /* " */
                                    this.stateString = false;
                                    continue scan;

                                case 0x5C: /* \ */
                                    this.stateStringEscape = true;
                            }
                        }
                    }

                    break;
                }

                switch (chunk.charCodeAt(i)) {
                    case 0x22: /* " */
                        this.stateString = true;
                        this.stateStringEscape = false;
                        break;

                    case 0x2C: /* , */
                        flushPoint = i;
                        break;

                    case 0x7B: /* { */
                        // Open an object
                        flushPoint = i + 1;
                        this.stack[this.flushDepth++] = STACK_OBJECT;
                        break;

                    case 0x5B: /* [ */
                        // Open an array
                        flushPoint = i + 1;
                        this.stack[this.flushDepth++] = STACK_ARRAY;
                        break;

                    case 0x5D: /* ] */
                    case 0x7D: /* } */
                        // Close an object or array
                        flushPoint = i + 1;
                        this.flushDepth--;

                        if (this.flushDepth < this.lastFlushDepth) {
                            this.flush(chunk, lastFlushPoint, flushPoint);
                            lastFlushPoint = flushPoint;
                        }

                        break;

                    case 0x09: /* \t */
                    case 0x0A: /* \n */
                    case 0x0D: /* \r */
                    case 0x20: /* space */
                        // Move points forward when they points on current position and it's a whitespace
                        if (lastFlushPoint === i) {
                            lastFlushPoint++;
                        }

                        if (flushPoint === i) {
                            flushPoint++;
                        }

                        break;
                }
            }

            if (flushPoint > lastFlushPoint) {
                this.flush(chunk, lastFlushPoint, flushPoint);
            }

            // Produce pendingChunk if something left
            if (flushPoint < chunkLength) {
                if (this.pendingChunk !== null) {
                    // When there is already a pending chunk then no flush happened,
                    // appending entire chunk to pending one
                    this.pendingChunk += chunk;
                } else {
                    // Create a pending chunk, it will start with non-whitespace since
                    // flushPoint was moved forward away from whitespaces on scan
                    this.pendingChunk = chunk.slice(flushPoint, chunkLength);
                }
            }

            this.chunkOffset += chunkLength;
        }

        finish() {
            if (this.pendingChunk !== null) {
                this.flush('', 0, 0);
                this.pendingChunk = null;
            }

            return this.value;
        }
    }

    var src = {
        version: version,
        stringifyInfo: stringifyInfo,
        stringifyStream: stringifyStreamBrowser,
        parseChunked: parseChunked
    };

    return src;

}));


code:
declare module '@discoveryjs/json-ext' {
    import { Readable } from 'stream';

    type TReplacer =
        | ((this: any, key: string, value: any) => any)
        | string[]
        | number[]
        | null;
    type TSpace = string | number | null;
    type TChunk = string | Buffer | Uint8Array;

    export function parseChunked(input: Readable): Promise<any>;
    export function parseChunked(input: () => (Iterable<TChunk> | AsyncIterable<TChunk>)): Promise<any>;

    export function stringifyStream(value: any, replacer?: TReplacer, space?: TSpace): Readable;

    export function stringifyInfo(
        value: any,
        replacer?: TReplacer,
        space?: TSpace,
        options?: {
            async?: boolean;
            continueOnCircular?: boolean;
        }
    ): {
        minLength: number;
        circular: any[];
        duplicate: any[];
        async: any[];
    };
}

license: mit


@discoveryjs/json-ext

description: A set of utilities that extend the use of JSON

keywords: json utils stream async promise stringify info

author: Roman Dvornov <rdvornov@gmail.com> (https://github.com/lahmatiy)

license: MIT

repository: discoveryjs/json-ext

test: mocha --reporter progress

lint: eslint src test

lint-and-test: npm run lint && npm test

build: rollup --config

test:all: npm run test:src && npm run test:dist

test:src: npm test

test:dist: cross-env MODE=dist npm test && cross-env MODE=dist-min npm test

build-and-test: npm run build && npm run test:dist

coverage: c8 --reporter=lcovonly npm test

prepublishOnly: npm run lint && npm test && npm run build-and-test

files: dist src index.d.ts


code:
module.exports = {
    version: require('./version'),
    stringifyInfo: require('./stringify-info'),
    stringifyStream: require('./stringify-stream'),
    parseChunked: require('./parse-chunked')
};


code:
const { isReadableStream } = require('./utils');
const TextDecoder = require('./text-decoder');

const STACK_OBJECT = 1;
const STACK_ARRAY = 2;
const decoder = new TextDecoder();

function isObject(value) {
    return value !== null && typeof value === 'object';
}

function adjustPosition(error, parser) {
    if (error.name === 'SyntaxError' && parser.jsonParseOffset) {
        error.message = error.message.replace(/at position (\d+)/, (_, pos) =>
            'at position ' + (Number(pos) + parser.jsonParseOffset)
        );
    }

    return error;
}

function append(array, elements) {
    // Note: Avoid to use array.push(...elements) since it may lead to
    // "RangeError: Maximum call stack size exceeded" for a long arrays
    const initialLength = array.length;
    array.length += elements.length;

    for (let i = 0; i < elements.length; i++) {
        array[initialLength + i] = elements[i];
    }
}

module.exports = function(chunkEmitter) {
    let parser = new ChunkParser();

    if (isObject(chunkEmitter) && isReadableStream(chunkEmitter)) {
        return new Promise((resolve, reject) => {
            chunkEmitter
                .on('data', chunk => {
                    try {
                        parser.push(chunk);
                    } catch (e) {
                        reject(adjustPosition(e, parser));
                        parser = null;
                    }
                })
                .on('error', (e) => {
                    parser = null;
                    reject(e);
                })
                .on('end', () => {
                    try {
                        resolve(parser.finish());
                    } catch (e) {
                        reject(adjustPosition(e, parser));
                    } finally {
                        parser = null;
                    }
                });
        });
    }

    if (typeof chunkEmitter === 'function') {
        const iterator = chunkEmitter();

        if (isObject(iterator) && (Symbol.iterator in iterator || Symbol.asyncIterator in iterator)) {
            return new Promise(async (resolve, reject) => {
                try {
                    for await (const chunk of iterator) {
                        parser.push(chunk);
                    }

                    resolve(parser.finish());
                } catch (e) {
                    reject(adjustPosition(e, parser));
                } finally {
                    parser = null;
                }
            });
        }
    }

    throw new Error(
        'Chunk emitter should be readable stream, generator, ' +
        'async generator or function returning an iterable object'
    );
};

class ChunkParser {
    constructor() {
        this.value = undefined;
        this.valueStack = null;

        this.stack = new Array(100);
        this.lastFlushDepth = 0;
        this.flushDepth = 0;
        this.stateString = false;
        this.stateStringEscape = false;
        this.pendingByteSeq = null;
        this.pendingChunk = null;
        this.chunkOffset = 0;
        this.jsonParseOffset = 0;
    }

    parseAndAppend(fragment, wrap) {
        // Append new entries or elements
        if (this.stack[this.lastFlushDepth - 1] === STACK_OBJECT) {
            if (wrap) {
                this.jsonParseOffset--;
                fragment = '{' + fragment + '}';
            }

            Object.assign(this.valueStack.value, JSON.parse(fragment));
        } else {
            if (wrap) {
                this.jsonParseOffset--;
                fragment = '[' + fragment + ']';
            }

            append(this.valueStack.value, JSON.parse(fragment));
        }
    }

    prepareAddition(fragment) {
        const { value } = this.valueStack;
        const expectComma = Array.isArray(value)
            ? value.length !== 0
            : Object.keys(value).length !== 0;

        if (expectComma) {
            // Skip a comma at the beginning of fragment, otherwise it would
            // fail to parse
            if (fragment[0] === ',') {
                this.jsonParseOffset++;
                return fragment.slice(1);
            }

            // When value (an object or array) is not empty and a fragment
            // doesn't start with a comma, a single valid fragment starting
            // is a closing bracket. If it's not, a prefix is adding to fail
            // parsing. Otherwise, the sequence of chunks can be successfully
            // parsed, although it should not, e.g. ["[{}", "{}]"]
            if (fragment[0] !== '}' && fragment[0] !== ']') {
                this.jsonParseOffset -= 3;
                return '[[]' + fragment;
            }
        }

        return fragment;
    }

    flush(chunk, start, end) {
        let fragment = chunk.slice(start, end);

        // Save position correction an error in JSON.parse() if any
        this.jsonParseOffset = this.chunkOffset + start;

        // Prepend pending chunk if any
        if (this.pendingChunk !== null) {
            fragment = this.pendingChunk + fragment;
            this.jsonParseOffset -= this.pendingChunk.length;
            this.pendingChunk = null;
        }

        if (this.flushDepth === this.lastFlushDepth) {
            // Depth didn't changed, so it's a root value or entry/element set
            if (this.flushDepth > 0) {
                this.parseAndAppend(this.prepareAddition(fragment), true);
            } else {
                // That's an entire value on a top level
                this.value = JSON.parse(fragment);
                this.valueStack = {
                    value: this.value,
                    prev: null
                };
            }
        } else if (this.flushDepth > this.lastFlushDepth) {
            // Add missed closing brackets/parentheses
            for (let i = this.flushDepth - 1; i >= this.lastFlushDepth; i--) {
                fragment += this.stack[i] === STACK_OBJECT ? '}' : ']';
            }

            if (this.lastFlushDepth === 0) {
                // That's a root value
                this.value = JSON.parse(fragment);
                this.valueStack = {
                    value: this.value,
                    prev: null
                };
            } else {
                this.parseAndAppend(this.prepareAddition(fragment), true);
            }

            // Move down to the depths to the last object/array, which is current now
            for (let i = this.lastFlushDepth || 1; i < this.flushDepth; i++) {
                let value = this.valueStack.value;

                if (this.stack[i - 1] === STACK_OBJECT) {
                    // find last entry
                    let key;
                    // eslint-disable-next-line curly
                    for (key in value);
                    value = value[key];
                } else {
                    // last element
                    value = value[value.length - 1];
                }

                this.valueStack = {
                    value,
                    prev: this.valueStack
                };
            }
        } else /* this.flushDepth < this.lastFlushDepth */ {
            fragment = this.prepareAddition(fragment);

            // Add missed opening brackets/parentheses
            for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
                this.jsonParseOffset--;
                fragment = (this.stack[i] === STACK_OBJECT ? '{' : '[') + fragment;
            }

            this.parseAndAppend(fragment, false);

            for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
                this.valueStack = this.valueStack.prev;
            }
        }

        this.lastFlushDepth = this.flushDepth;
    }

    push(chunk) {
        if (typeof chunk !== 'string') {
            // Suppose chunk is Buffer or Uint8Array

            // Prepend uncompleted byte sequence if any
            if (this.pendingByteSeq !== null) {
                const origRawChunk = chunk;
                chunk = new Uint8Array(this.pendingByteSeq.length + origRawChunk.length);
                chunk.set(this.pendingByteSeq);
                chunk.set(origRawChunk, this.pendingByteSeq.length);
                this.pendingByteSeq = null;
            }

            // In case Buffer/Uint8Array, an input is encoded in UTF8
            // Seek for parts of uncompleted UTF8 symbol on the ending
            // This makes sense only if we expect more chunks and last char is not multi-bytes
            if (chunk[chunk.length - 1] > 127) {
                for (let seqLength = 0; seqLength < chunk.length; seqLength++) {
                    const byte = chunk[chunk.length - 1 - seqLength];

                    // 10xxxxxx - 2nd, 3rd or 4th byte
                    // 110xxxxx – first byte of 2-byte sequence
                    // 1110xxxx - first byte of 3-byte sequence
                    // 11110xxx - first byte of 4-byte sequence
                    if (byte >> 6 === 3) {
                        seqLength++;

                        // If the sequence is really incomplete, then preserve it
                        // for the future chunk and cut off it from the current chunk
                        if ((seqLength !== 4 && byte >> 3 === 0b11110) ||
                            (seqLength !== 3 && byte >> 4 === 0b1110) ||
                            (seqLength !== 2 && byte >> 5 === 0b110)) {
                            this.pendingByteSeq = chunk.slice(chunk.length - seqLength);
                            chunk = chunk.slice(0, -seqLength);
                        }

                        break;
                    }
                }
            }

            // Convert chunk to a string, since single decode per chunk
            // is much effective than decode multiple small substrings
            chunk = decoder.decode(chunk);
        }

        const chunkLength = chunk.length;
        let lastFlushPoint = 0;
        let flushPoint = 0;

        // Main scan loop
        scan: for (let i = 0; i < chunkLength; i++) {
            if (this.stateString) {
                for (; i < chunkLength; i++) {
                    if (this.stateStringEscape) {
                        this.stateStringEscape = false;
                    } else {
                        switch (chunk.charCodeAt(i)) {
                            case 0x22: /* " */
                                this.stateString = false;
                                continue scan;

                            case 0x5C: /* \ */
                                this.stateStringEscape = true;
                        }
                    }
                }

                break;
            }

            switch (chunk.charCodeAt(i)) {
                case 0x22: /* " */
                    this.stateString = true;
                    this.stateStringEscape = false;
                    break;

                case 0x2C: /* , */
                    flushPoint = i;
                    break;

                case 0x7B: /* { */
                    // Open an object
                    flushPoint = i + 1;
                    this.stack[this.flushDepth++] = STACK_OBJECT;
                    break;

                case 0x5B: /* [ */
                    // Open an array
                    flushPoint = i + 1;
                    this.stack[this.flushDepth++] = STACK_ARRAY;
                    break;

                case 0x5D: /* ] */
                case 0x7D: /* } */
                    // Close an object or array
                    flushPoint = i + 1;
                    this.flushDepth--;

                    if (this.flushDepth < this.lastFlushDepth) {
                        this.flush(chunk, lastFlushPoint, flushPoint);
                        lastFlushPoint = flushPoint;
                    }

                    break;

                case 0x09: /* \t */
                case 0x0A: /* \n */
                case 0x0D: /* \r */
                case 0x20: /* space */
                    // Move points forward when they points on current position and it's a whitespace
                    if (lastFlushPoint === i) {
                        lastFlushPoint++;
                    }

                    if (flushPoint === i) {
                        flushPoint++;
                    }

                    break;
            }
        }

        if (flushPoint > lastFlushPoint) {
            this.flush(chunk, lastFlushPoint, flushPoint);
        }

        // Produce pendingChunk if something left
        if (flushPoint < chunkLength) {
            if (this.pendingChunk !== null) {
                // When there is already a pending chunk then no flush happened,
                // appending entire chunk to pending one
                this.pendingChunk += chunk;
            } else {
                // Create a pending chunk, it will start with non-whitespace since
                // flushPoint was moved forward away from whitespaces on scan
                this.pendingChunk = chunk.slice(flushPoint, chunkLength);
            }
        }

        this.chunkOffset += chunkLength;
    }

    finish() {
        if (this.pendingChunk !== null) {
            this.flush('', 0, 0);
            this.pendingChunk = null;
        }

        return this.value;
    }
};


code:
const {
    normalizeReplacer,
    normalizeSpace,
    replaceValue,
    getTypeNative,
    getTypeAsync,
    isLeadingSurrogate,
    isTrailingSurrogate,
    escapableCharCodeSubstitution,
    type: {
        PRIMITIVE,
        OBJECT,
        ARRAY,
        PROMISE,
        STRING_STREAM,
        OBJECT_STREAM
    }
} = require('./utils');
const charLength2048 = Array.from({ length: 2048 }).map((_, code) => {
    if (escapableCharCodeSubstitution.hasOwnProperty(code)) {
        return 2; // \X
    }

    if (code < 0x20) {
        return 6; // \uXXXX
    }

    return code < 128 ? 1 : 2; // UTF8 bytes
});

function stringLength(str) {
    let len = 0;
    let prevLeadingSurrogate = false;

    for (let i = 0; i < str.length; i++) {
        const code = str.charCodeAt(i);

        if (code < 2048) {
            len += charLength2048[code];
        } else if (isLeadingSurrogate(code)) {
            len += 6; // \uXXXX since no pair with trailing surrogate yet
            prevLeadingSurrogate = true;
            continue;
        } else if (isTrailingSurrogate(code)) {
            len = prevLeadingSurrogate
                ? len - 2  // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes
                : len + 6; // \uXXXX
        } else {
            len += 3; // code >= 2048 is 3 bytes length for UTF8
        }

        prevLeadingSurrogate = false;
    }

    return len + 2; // +2 for quotes
}

function primitiveLength(value) {
    switch (typeof value) {
        case 'string':
            return stringLength(value);

        case 'number':
            return Number.isFinite(value) ? String(value).length : 4 /* null */;

        case 'boolean':
            return value ? 4 /* true */ : 5 /* false */;

        case 'undefined':
        case 'object':
            return 4; /* null */

        default:
            return 0;
    }
}

function spaceLength(space) {
    space = normalizeSpace(space);
    return typeof space === 'string' ? space.length : 0;
}

module.exports = function jsonStringifyInfo(value, replacer, space, options) {
    function walk(holder, key, value) {
        if (stop) {
            return;
        }

        value = replaceValue(holder, key, value, replacer);

        let type = getType(value);

        // check for circular structure
        if (type !== PRIMITIVE && stack.has(value)) {
            circular.add(value);
            length += 4; // treat as null

            if (!options.continueOnCircular) {
                stop = true;
            }

            return;
        }

        switch (type) {
            case PRIMITIVE:
                if (value !== undefined || Array.isArray(holder)) {
                    length += primitiveLength(value);
                } else if (holder === root) {
                    length += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null?
                }
                break;

            case OBJECT: {
                if (visited.has(value)) {
                    duplicate.add(value);
                    length += visited.get(value);
                    break;
                }

                const valueLength = length;
                let entries = 0;

                length += 2; // {}

                stack.add(value);

                for (const key in value) {
                    if (hasOwnProperty.call(value, key) && (allowlist === null || allowlist.has(key))) {
                        const prevLength = length;
                        walk(value, key, value[key]);

                        if (prevLength !== length) {
                            // value is printed
                            length += stringLength(key) + 1; // "key":
                            entries++;
                        }
                    }
                }

                if (entries > 1) {
                    length += entries - 1; // commas
                }

                stack.delete(value);

                if (space > 0 && entries > 0) {
                    length += (1 + (stack.size + 1) * space + 1) * entries; // for each key-value: \n{space}
                    length += 1 + stack.size * space; // for }
                }

                visited.set(value, length - valueLength);

                break;
            }

            case ARRAY: {
                if (visited.has(value)) {
                    duplicate.add(value);
                    length += visited.get(value);
                    break;
                }

                const valueLength = length;

                length += 2; // []

                stack.add(value);

                for (let i = 0; i < value.length; i++) {
                    walk(value, i, value[i]);
                }

                if (value.length > 1) {
                    length += value.length - 1; // commas
                }

                stack.delete(value);

                if (space > 0 && value.length > 0) {
                    length += (1 + (stack.size + 1) * space) * value.length; // for each element: \n{space}
                    length += 1 + stack.size * space; // for ]
                }

                visited.set(value, length - valueLength);

                break;
            }

            case PROMISE:
            case STRING_STREAM:
                async.add(value);
                break;

            case OBJECT_STREAM:
                length += 2; // []
                async.add(value);
                break;
        }
    }

    let allowlist = null;
    replacer = normalizeReplacer(replacer);

    if (Array.isArray(replacer)) {
        allowlist = new Set(replacer);
        replacer = null;
    }

    space = spaceLength(space);
    options = options || {};

    const visited = new Map();
    const stack = new Set();
    const duplicate = new Set();
    const circular = new Set();
    const async = new Set();
    const getType = options.async ? getTypeAsync : getTypeNative;
    const root = { '': value };
    let stop = false;
    let length = 0;

    walk(root, '', value);

    return {
        minLength: isNaN(length) ? Infinity : length,
        circular: [...circular],
        duplicate: [...duplicate],
        async: [...async]
    };
};


code:
module.exports = () => {
    throw new Error('Method is not supported');
};


code:
const { Readable } = require('stream');
const {
    normalizeReplacer,
    normalizeSpace,
    replaceValue,
    getTypeAsync,
    type: {
        PRIMITIVE,
        OBJECT,
        ARRAY,
        PROMISE,
        STRING_STREAM,
        OBJECT_STREAM
    }
} = require('./utils');
const noop = () => {};
const hasOwnProperty = Object.prototype.hasOwnProperty;

// TODO: Remove when drop support for Node.js 10
// Node.js 10 has no well-formed JSON.stringify()
// https://github.com/tc39/proposal-well-formed-stringify
// Adopted code from https://bugs.chromium.org/p/v8/issues/detail?id=7782#c12
const wellformedStringStringify = JSON.stringify('\ud800') === '"\\ud800"'
    ? JSON.stringify
    : s => JSON.stringify(s).replace(
        /\p{Surrogate}/gu,
        m => `\\u${m.charCodeAt(0).toString(16)}`
    );

function push() {
    this.push(this._stack.value);
    this.popStack();
}

function pushPrimitive(value) {
    switch (typeof value) {
        case 'string':
            this.push(this.encodeString(value));
            break;

        case 'number':
            this.push(Number.isFinite(value) ? this.encodeNumber(value) : 'null');
            break;

        case 'boolean':
            this.push(value ? 'true' : 'false');
            break;

        case 'undefined':
        case 'object': // typeof null === 'object'
            this.push('null');
            break;

        default:
            this.destroy(new TypeError(`Do not know how to serialize a ${value.constructor && value.constructor.name || typeof value}`));
    }
}

function processObjectEntry(key) {
    const current = this._stack;

    if (!current.first) {
        current.first = true;
    } else {
        this.push(',');
    }

    if (this.space) {
        this.push(`\n${this.space.repeat(this._depth)}${this.encodeString(key)}: `);
    } else {
        this.push(this.encodeString(key) + ':');
    }
}

function processObject() {
    const current = this._stack;

    // when no keys left, remove obj from stack
    if (current.index === current.keys.length) {
        if (this.space && current.first) {
            this.push(`\n${this.space.repeat(this._depth - 1)}}`);
        } else {
            this.push('}');
        }

        this.popStack();
        return;
    }

    const key = current.keys[current.index];

    this.processValue(current.value, key, current.value[key], processObjectEntry);
    current.index++;
}

function processArrayItem(index) {
    if (index !== 0) {
        this.push(',');
    }

    if (this.space) {
        this.push(`\n${this.space.repeat(this._depth)}`);
    }
}

function processArray() {
    const current = this._stack;

    if (current.index === current.value.length) {
        if (this.space && current.index > 0) {
            this.push(`\n${this.space.repeat(this._depth - 1)}]`);
        } else {
            this.push(']');
        }

        this.popStack();
        return;
    }

    this.processValue(current.value, current.index, current.value[current.index], processArrayItem);
    current.index++;
}

function createStreamReader(fn) {
    return function() {
        const current = this._stack;
        const data = current.value.read(this._readSize);

        if (data !== null) {
            current.first = false;
            fn.call(this, data, current);
        } else {
            if ((current.first && !current.value._readableState.reading) || current.ended) {
                this.popStack();
            } else {
                current.first = true;
                current.awaiting = true;
            }
        }
    };
}

const processReadableObject = createStreamReader(function(data, current) {
    this.processValue(current.value, current.index, data, processArrayItem);
    current.index++;
});

const processReadableString = createStreamReader(function(data) {
    this.push(data);
});

class JsonStringifyStream extends Readable {
    constructor(value, replacer, space) {
        super({
            autoDestroy: true
        });

        this.getKeys = Object.keys;
        this.replacer = normalizeReplacer(replacer);

        if (Array.isArray(this.replacer)) {
            const allowlist = this.replacer;

            this.getKeys = (value) => allowlist.filter(key => hasOwnProperty.call(value, key));
            this.replacer = null;
        }

        this.space = normalizeSpace(space);
        this._depth = 0;

        this.error = null;
        this._processing = false;
        this._ended = false;

        this._readSize = 0;
        this._buffer = '';

        this._stack = null;
        this._visited = new WeakSet();

        this.pushStack({
            handler: () => {
                this.popStack();
                this.processValue({ '': value }, '', value, noop);
            }
        });
    }

    encodeString(value) {
        if (/[^\x20-\uD799]|[\x22\x5c]/.test(value)) {
            return wellformedStringStringify(value);
        }

        return '"' + value + '"';
    }

    encodeNumber(value) {
        return value;
    }

    processValue(holder, key, value, callback) {
        value = replaceValue(holder, key, value, this.replacer);

        let type = getTypeAsync(value);

        switch (type) {
            case PRIMITIVE:
                if (callback !== processObjectEntry || value !== undefined) {
                    callback.call(this, key);
                    pushPrimitive.call(this, value);
                }
                break;

            case OBJECT:
                callback.call(this, key);

                // check for circular structure
                if (this._visited.has(value)) {
                    return this.destroy(new TypeError('Converting circular structure to JSON'));
                }

                this._visited.add(value);
                this._depth++;
                this.push('{');
                this.pushStack({
                    handler: processObject,
                    value,
                    index: 0,
                    first: false,
                    keys: this.getKeys(value)
                });
                break;

            case ARRAY:
                callback.call(this, key);

                // check for circular structure
                if (this._visited.has(value)) {
                    return this.destroy(new TypeError('Converting circular structure to JSON'));
                }

                this._visited.add(value);

                this.push('[');
                this.pushStack({
                    handler: processArray,
                    value,
                    index: 0
                });
                this._depth++;
                break;

            case PROMISE:
                this.pushStack({
                    handler: noop,
                    awaiting: true
                });

                Promise.resolve(value)
                    .then(resolved => {
                        this.popStack();
                        this.processValue(holder, key, resolved, callback);
                        this.processStack();
                    })
                    .catch(error => {
                        this.destroy(error);
                    });
                break;

            case STRING_STREAM:
            case OBJECT_STREAM:
                callback.call(this, key);

                // TODO: Remove when drop support for Node.js 10
                // Used `_readableState.endEmitted` as fallback, since Node.js 10 has no `readableEnded` getter
                if (value.readableEnded || value._readableState.endEmitted) {
                    return this.destroy(new Error('Readable Stream has ended before it was serialized. All stream data have been lost'));
                }

                if (value.readableFlowing) {
                    return this.destroy(new Error('Readable Stream is in flowing mode, data may have been lost. Trying to pause stream.'));
                }

                if (type === OBJECT_STREAM) {
                    this.push('[');
                    this.pushStack({
                        handler: push,
                        value: this.space ? '\n' + this.space.repeat(this._depth) + ']' : ']'
                    });
                    this._depth++;
                }

                const self = this.pushStack({
                    handler: type === OBJECT_STREAM ? processReadableObject : processReadableString,
                    value,
                    index: 0,
                    first: false,
                    ended: false,
                    awaiting: !value.readable || value.readableLength === 0
                });
                const continueProcessing = () => {
                    if (self.awaiting) {
                        self.awaiting = false;
                        this.processStack();
                    }
                };

                value.once('error', error => this.destroy(error));
                value.once('end', () => {
                    self.ended = true;
                    continueProcessing();
                });
                value.on('readable', continueProcessing);
                break;
        }
    }

    pushStack(node) {
        node.prev = this._stack;
        return this._stack = node;
    }

    popStack() {
        const { handler, value } = this._stack;

        if (handler === processObject || handler === processArray || handler === processReadableObject) {
            this._visited.delete(value);
            this._depth--;
        }

        this._stack = this._stack.prev;
    }

    processStack() {
        if (this._processing || this._ended) {
            return;
        }

        try {
            this._processing = true;

            while (this._stack !== null && !this._stack.awaiting) {
                this._stack.handler.call(this);

                if (!this._processing) {
                    return;
                }
            }

            this._processing = false;
        } catch (error) {
            this.destroy(error);
            return;
        }

        if (this._stack === null && !this._ended) {
            this._finish();
            this.push(null);
        }
    }

    push(data) {
        if (data !== null) {
            this._buffer += data;

            // check buffer overflow
            if (this._buffer.length < this._readSize) {
                return;
            }

            // flush buffer
            data = this._buffer;
            this._buffer = '';
            this._processing = false;
        }

        super.push(data);
    }

    _read(size) {
        // start processing
        this._readSize = size || this.readableHighWaterMark;
        this.processStack();
    }

    _finish() {
        this._ended = true;
        this._processing = false;
        this._stack = null;
        this._visited = null;

        if (this._buffer && this._buffer.length) {
            super.push(this._buffer); // flush buffer
        }

        this._buffer = '';
    }

    _destroy(error, cb) {
        this.error = this.error || error;
        this._finish();
        cb(error);
    }
}

module.exports = function createJsonStringifyStream(value, replacer, space) {
    return new JsonStringifyStream(value, replacer, space);
};


code:
const PrimitiveType = 1;
const ObjectType = 2;
const ArrayType = 3;
const PromiseType = 4;
const ReadableStringType = 5;
const ReadableObjectType = 6;
// https://tc39.es/ecma262/#table-json-single-character-escapes
const escapableCharCodeSubstitution = { // JSON Single Character Escape Sequences
    0x08: '\\b',
    0x09: '\\t',
    0x0a: '\\n',
    0x0c: '\\f',
    0x0d: '\\r',
    0x22: '\\\"',
    0x5c: '\\\\'
};

function isLeadingSurrogate(code) {
    return code >= 0xD800 && code <= 0xDBFF;
}

function isTrailingSurrogate(code) {
    return code >= 0xDC00 && code <= 0xDFFF;
}

function isReadableStream(value) {
    return (
        typeof value.pipe === 'function' &&
        typeof value._read === 'function' &&
        typeof value._readableState === 'object' && value._readableState !== null
    );
}

function replaceValue(holder, key, value, replacer) {
    if (value && typeof value.toJSON === 'function') {
        value = value.toJSON();
    }

    if (replacer !== null) {
        value = replacer.call(holder, String(key), value);
    }

    switch (typeof value) {
        case 'function':
        case 'symbol':
            value = undefined;
            break;

        case 'object':
            if (value !== null) {
                const cls = value.constructor;
                if (cls === String || cls === Number || cls === Boolean) {
                    value = value.valueOf();
                }
            }
            break;
    }

    return value;
}

function getTypeNative(value) {
    if (value === null || typeof value !== 'object') {
        return PrimitiveType;
    }

    if (Array.isArray(value)) {
        return ArrayType;
    }

    return ObjectType;
}

function getTypeAsync(value) {
    if (value === null || typeof value !== 'object') {
        return PrimitiveType;
    }

    if (typeof value.then === 'function') {
        return PromiseType;
    }

    if (isReadableStream(value)) {
        return value._readableState.objectMode ? ReadableObjectType : ReadableStringType;
    }

    if (Array.isArray(value)) {
        return ArrayType;
    }

    return ObjectType;
}

function normalizeReplacer(replacer) {
    if (typeof replacer === 'function') {
        return replacer;
    }

    if (Array.isArray(replacer)) {
        const allowlist = new Set(replacer
            .map(item => {
                const cls = item && item.constructor;
                return cls === String || cls === Number ? String(item) : null;
            })
            .filter(item => typeof item === 'string')
        );

        return [...allowlist];
    }

    return null;
}

function normalizeSpace(space) {
    if (typeof space === 'number') {
        if (!Number.isFinite(space) || space < 1) {
            return false;
        }

        return ' '.repeat(Math.min(space, 10));
    }

    if (typeof space === 'string') {
        return space.slice(0, 10) || false;
    }

    return false;
}

module.exports = {
    escapableCharCodeSubstitution,
    isLeadingSurrogate,
    isTrailingSurrogate,
    type: {
        PRIMITIVE: PrimitiveType,
        PROMISE: PromiseType,
        ARRAY: ArrayType,
        OBJECT: ObjectType,
        STRING_STREAM: ReadableStringType,
        OBJECT_STREAM: ReadableObjectType
    },

    isReadableStream,
    replaceValue,
    getTypeNative,
    getTypeAsync,
    normalizeReplacer,
    normalizeSpace
};


code:
# yauzl

[![Build Status](https://travis-ci.org/thejoshwolfe/yauzl.svg?branch=master)](https://travis-ci.org/thejoshwolfe/yauzl)
[![Coverage Status](https://img.shields.io/coveralls/thejoshwolfe/yauzl.svg)](https://coveralls.io/r/thejoshwolfe/yauzl)

yet another unzip library for node. For zipping, see
[yazl](https://github.com/thejoshwolfe/yazl).

Design principles:

 * Follow the spec.
   Don't scan for local file headers.
   Read the central directory for file metadata.
   (see [No Streaming Unzip API](#no-streaming-unzip-api)).
 * Don't block the JavaScript thread.
   Use and provide async APIs.
 * Keep memory usage under control.
   Don't attempt to buffer entire files in RAM at once.
 * Never crash (if used properly).
   Don't let malformed zip files bring down client applications who are trying to catch errors.
 * Catch unsafe file names.
   See `validateFileName()`.

## Usage

```js
var yauzl = require("yauzl");

yauzl.open("path/to/file.zip", {lazyEntries: true}, function(err, zipfile) {
  if (err) throw err;
  zipfile.readEntry();
  zipfile.on("entry", function(entry) {
    if (/\/$/.test(entry.fileName)) {
      // Directory file names end with '/'.
      // Note that entries for directories themselves are optional.
      // An entry's fileName implicitly requires its parent directories to exist.
      zipfile.readEntry();
    } else {
      // file entry
      zipfile.openReadStream(entry, function(err, readStream) {
        if (err) throw err;
        readStream.on("end", function() {
          zipfile.readEntry();
        });
        readStream.pipe(somewhere);
      });
    }
  });
});
```

See also `examples/` for more usage examples.

## API

The default for every optional `callback` parameter is:

```js
function defaultCallback(err) {
  if (err) throw err;
}
```

### open(path, [options], [callback])

Calls `fs.open(path, "r")` and reads the `fd` effectively the same as `fromFd()` would.

`options` may be omitted or `null`. The defaults are `{autoClose: true, lazyEntries: false, decodeStrings: true, validateEntrySizes: true, strictFileNames: false}`.

`autoClose` is effectively equivalent to:

```js
zipfile.once("end", function() {
  zipfile.close();
});
```

`lazyEntries` indicates that entries should be read only when `readEntry()` is called.
If `lazyEntries` is `false`, `entry` events will be emitted as fast as possible to allow `pipe()`ing
file data from all entries in parallel.
This is not recommended, as it can lead to out of control memory usage for zip files with many entries.
See [issue #22](https://github.com/thejoshwolfe/yauzl/issues/22).
If `lazyEntries` is `true`, an `entry` or `end` event will be emitted in response to each call to `readEntry()`.
This allows processing of one entry at a time, and will keep memory usage under control for zip files with many entries.

`decodeStrings` is the default and causes yauzl to decode strings with `CP437` or `UTF-8` as required by the spec.
The exact effects of turning this option off are:

* `zipfile.comment`, `entry.fileName`, and `entry.fileComment` will be `Buffer` objects instead of `String`s.
* Any Info-ZIP Unicode Path Extra Field will be ignored. See `extraFields`.
* Automatic file name validation will not be performed. See `validateFileName()`.

`validateEntrySizes` is the default and ensures that an entry's reported uncompressed size matches its actual uncompressed size.
This check happens as early as possible, which is either before emitting each `"entry"` event (for entries with no compression),
or during the `readStream` piping after calling `openReadStream()`.
See `openReadStream()` for more information on defending against zip bomb attacks.

When `strictFileNames` is `false` (the default) and `decodeStrings` is `true`,
all backslash (`\`) characters in each `entry.fileName` are replaced with forward slashes (`/`).
The spec forbids file names with backslashes,
but Microsoft's `System.IO.Compression.ZipFile` class in .NET versions 4.5.0 until 4.6.1
creates non-conformant zipfiles with backslashes in file names.
`strictFileNames` is `false` by default so that clients can read these
non-conformant zipfiles without knowing about this Microsoft-specific bug.
When `strictFileNames` is `true` and `decodeStrings` is `true`,
entries with backslashes in their file names will result in an error. See `validateFileName()`.
When `decodeStrings` is `false`, `strictFileNames` has no effect.

The `callback` is given the arguments `(err, zipfile)`.
An `err` is provided if the End of Central Directory Record cannot be found, or if its metadata appears malformed.
This kind of error usually indicates that this is not a zip file.
Otherwise, `zipfile` is an instance of `ZipFile`.

### fromFd(fd, [options], [callback])

Reads from the fd, which is presumed to be an open .zip file.
Note that random access is required by the zip file specification,
so the fd cannot be an open socket or any other fd that does not support random access.

`options` may be omitted or `null`. The defaults are `{autoClose: false, lazyEntries: false, decodeStrings: true, validateEntrySizes: true, strictFileNames: false}`.

See `open()` for the meaning of the options and callback.

### fromBuffer(buffer, [options], [callback])

Like `fromFd()`, but reads from a RAM buffer instead of an open file.
`buffer` is a `Buffer`.

If a `ZipFile` is acquired from this method,
it will never emit the `close` event,
and calling `close()` is not necessary.

`options` may be omitted or `null`. The defaults are `{lazyEntries: false, decodeStrings: true, validateEntrySizes: true, strictFileNames: false}`.

See `open()` for the meaning of the options and callback.
The `autoClose` option is ignored for this method.

### fromRandomAccessReader(reader, totalSize, [options], [callback])

This method of reading a zip file allows clients to implement their own back-end file system.
For example, a client might translate read calls into network requests.

The `reader` parameter must be of a type that is a subclass of
[RandomAccessReader](#class-randomaccessreader) that implements the required methods.
The `totalSize` is a Number and indicates the total file size of the zip file.

`options` may be omitted or `null`. The defaults are `{autoClose: true, lazyEntries: false, decodeStrings: true, validateEntrySizes: true, strictFileNames: false}`.

See `open()` for the meaning of the options and callback.

### dosDateTimeToDate(date, time)

Converts MS-DOS `date` and `time` data into a JavaScript `Date` object.
Each parameter is a `Number` treated as an unsigned 16-bit integer.
Note that this format does not support timezones,
so the returned object will use the local timezone.

### validateFileName(fileName)

Returns `null` or a `String` error message depending on the validity of `fileName`.
If `fileName` starts with `"/"` or `/[A-Za-z]:\//` or if it contains `".."` path segments or `"\\"`,
this function returns an error message appropriate for use like this:

```js
var errorMessage = yauzl.validateFileName(fileName);
if (errorMessage != null) throw new Error(errorMessage);
```

This function is automatically run for each entry, as long as `decodeStrings` is `true`.
See `open()`, `strictFileNames`, and `Event: "entry"` for more information.

### Class: ZipFile

The constructor for the class is not part of the public API.
Use `open()`, `fromFd()`, `fromBuffer()`, or `fromRandomAccessReader()` instead.

#### Event: "entry"

Callback gets `(entry)`, which is an `Entry`.
See `open()` and `readEntry()` for when this event is emitted.

If `decodeStrings` is `true`, entries emitted via this event have already passed file name validation.
See `validateFileName()` and `open()` for more information.

If `validateEntrySizes` is `true` and this entry's `compressionMethod` is `0` (stored without compression),
this entry has already passed entry size validation.
See `open()` for more information.

#### Event: "end"

Emitted after the last `entry` event has been emitted.
See `open()` and `readEntry()` for more info on when this event is emitted.

#### Event: "close"

Emitted after the fd is actually closed.
This is after calling `close()` (or after the `end` event when `autoClose` is `true`),
and after all stream pipelines created from `openReadStream()` have finished reading data from the fd.

If this `ZipFile` was acquired from `fromRandomAccessReader()`,
the "fd" in the previous paragraph refers to the `RandomAccessReader` implemented by the client.

If this `ZipFile` was acquired from `fromBuffer()`, this event is never emitted.

#### Event: "error"

Emitted in the case of errors with reading the zip file.
(Note that other errors can be emitted from the streams created from `openReadStream()` as well.)
After this event has been emitted, no further `entry`, `end`, or `error` events will be emitted,
but the `close` event may still be emitted.

#### readEntry()

Causes this `ZipFile` to emit an `entry` or `end` event (or an `error` event).
This method must only be called when this `ZipFile` was created with the `lazyEntries` option set to `true` (see `open()`).
When this `ZipFile` was created with the `lazyEntries` option set to `true`,
`entry` and `end` events are only ever emitted in response to this method call.

The event that is emitted in response to this method will not be emitted until after this method has returned,
so it is safe to call this method before attaching event listeners.

After calling this method, calling this method again before the response event has been emitted will cause undefined behavior.
Calling this method after the `end` event has been emitted will cause undefined behavior.
Calling this method after calling `close()` will cause undefined behavior.

#### openReadStream(entry, [options], callback)

`entry` must be an `Entry` object from this `ZipFile`.
`callback` gets `(err, readStream)`, where `readStream` is a `Readable Stream` that provides the file data for this entry.
If this zipfile is already closed (see `close()`), the `callback` will receive an `err`.

`options` may be omitted or `null`, and has the following defaults:

```js
{
  decompress: entry.isCompressed() ? true : null,
  decrypt: null,
  start: 0,                  // actually the default is null, see below
  end: entry.compressedSize, // actually the default is null, see below
}
```

If the entry is compressed (with a supported compression method),
and the `decompress` option is `true` (or omitted),
the read stream provides the decompressed data.
Omitting the `decompress` option is what most clients should do.

The `decompress` option must be `null` (or omitted) when the entry is not compressed (see `isCompressed()`),
and either `true` (or omitted) or `false` when the entry is compressed.
Specifying `decompress: false` for a compressed entry causes the read stream
to provide the raw compressed file data without going through a zlib inflate transform.

If the entry is encrypted (see `isEncrypted()`), clients may want to avoid calling `openReadStream()` on the entry entirely.
Alternatively, clients may call `openReadStream()` for encrypted entries and specify `decrypt: false`.
If the entry is also compressed, clients must *also* specify `decompress: false`.
Specifying `decrypt: false` for an encrypted entry causes the read stream to provide the raw, still-encrypted file data.
(This data includes the 12-byte header described in the spec.)

The `decrypt` option must be `null` (or omitted) for non-encrypted entries, and `false` for encrypted entries.
Omitting the `decrypt` option (or specifying it as `null`) for an encrypted entry
will result in the `callback` receiving an `err`.
This default behavior is so that clients not accounting for encrypted files aren't surprised by bogus file data.

The `start` (inclusive) and `end` (exclusive) options are byte offsets into this entry's file data,
and can be used to obtain part of an entry's file data rather than the whole thing.
If either of these options are specified and non-`null`,
then the above options must be used to obain the file's raw data.
Specifying `{start: 0, end: entry.compressedSize}` will result in the complete file,
which is effectively the default values for these options,
but note that unlike omitting the options, when you specify `start` or `end` as any non-`null` value,
the above requirement is still enforced that you must also pass the appropriate options to get the file's raw data.

It's possible for the `readStream` provided to the `callback` to emit errors for several reasons.
For example, if zlib cannot decompress the data, the zlib error will be emitted from the `readStream`.
Two more error cases (when `validateEntrySizes` is `true`) are if the decompressed data has too many
or too few actual bytes compared to the reported byte count from the entry's `uncompressedSize` field.
yauzl notices this false information and emits an error from the `readStream`
after some number of bytes have already been piped through the stream.

This check allows clients to trust the `uncompressedSize` field in `Entry` objects.
Guarding against [zip bomb](http://en.wikipedia.org/wiki/Zip_bomb) attacks can be accomplished by
doing some heuristic checks on the size metadata and then watching out for the above errors.
Such heuristics are outside the scope of this library,
but enforcing the `uncompressedSize` is implemented here as a security feature.

It is possible to destroy the `readStream` before it has piped all of its data.
To do this, call `readStream.destroy()`.
You must `unpipe()` the `readStream` from any destination before calling `readStream.destroy()`.
If this zipfile was created using `fromRandomAccessReader()`, the `RandomAccessReader` implementation
must provide readable streams that implement a `.destroy()` method (see `randomAccessReader._readStreamForRange()`)
in order for calls to `readStream.destroy()` to work in this context.

#### close()

Causes all future calls to `openReadStream()` to fail,
and closes the fd, if any, after all streams created by `openReadStream()` have emitted their `end` events.

If the `autoClose` option is set to `true` (see `open()`),
this function will be called automatically effectively in response to this object's `end` event.

If the `lazyEntries` option is set to `false` (see `open()`) and this object's `end` event has not been emitted yet,
this function causes undefined behavior.
If the `lazyEntries` option is set to `true`,
you can call this function instead of calling `readEntry()` to abort reading the entries of a zipfile.

It is safe to call this function multiple times; after the first call, successive calls have no effect.
This includes situations where the `autoClose` option effectively calls this function for you.

If `close()` is never called, then the zipfile is "kept open".
For zipfiles created with `fromFd()`, this will leave the `fd` open, which may be desirable.
For zipfiles created with `open()`, this will leave the underlying `fd` open, thereby "leaking" it, which is probably undesirable.
For zipfiles created with `fromRandomAccessReader()`, the reader's `close()` method will never be called.
For zipfiles created with `fromBuffer()`, the `close()` function has no effect whether called or not.

Regardless of how this `ZipFile` was created, there are no resources other than those listed above that require cleanup from this function.
This means it may be desirable to never call `close()` in some usecases.

#### isOpen

`Boolean`. `true` until `close()` is called; then it's `false`.

#### entryCount

`Number`. Total number of central directory records.

#### comment

`String`. Always decoded with `CP437` per the spec.

If `decodeStrings` is `false` (see `open()`), this field is the undecoded `Buffer` instead of a decoded `String`.

### Class: Entry

Objects of this class represent Central Directory Records.
Refer to the zipfile specification for more details about these fields.

These fields are of type `Number`:

 * `versionMadeBy`
 * `versionNeededToExtract`
 * `generalPurposeBitFlag`
 * `compressionMethod`
 * `lastModFileTime` (MS-DOS format, see `getLastModDateTime`)
 * `lastModFileDate` (MS-DOS format, see `getLastModDateTime`)
 * `crc32`
 * `compressedSize`
 * `uncompressedSize`
 * `fileNameLength` (bytes)
 * `extraFieldLength` (bytes)
 * `fileCommentLength` (bytes)
 * `internalFileAttributes`
 * `externalFileAttributes`
 * `relativeOffsetOfLocalHeader`

#### fileName

`String`.
Following the spec, the bytes for the file name are decoded with
`UTF-8` if `generalPurposeBitFlag & 0x800`, otherwise with `CP437`.
Alternatively, this field may be populated from the Info-ZIP Unicode Path Extra Field
(see `extraFields`).

This field is automatically validated by `validateFileName()` before yauzl emits an "entry" event.
If this field would contain unsafe characters, yauzl emits an error instead of an entry.

If `decodeStrings` is `false` (see `open()`), this field is the undecoded `Buffer` instead of a decoded `String`.
Therefore, `generalPurposeBitFlag` and any Info-ZIP Unicode Path Extra Field are ignored.
Furthermore, no automatic file name validation is performed for this file name.

#### extraFields

`Array` with each entry in the form `{id: id, data: data}`,
where `id` is a `Number` and `data` is a `Buffer`.

This library looks for and reads the ZIP64 Extended Information Extra Field (0x0001)
in order to support ZIP64 format zip files.

This library also looks for and reads the Info-ZIP Unicode Path Extra Field (0x7075)
in order to support some zipfiles that use it instead of General Purpose Bit 11
to convey `UTF-8` file names.
When the field is identified and verified to be reliable (see the zipfile spec),
the the file name in this field is stored in the `fileName` property,
and the file name in the central directory record for this entry is ignored.
Note that when `decodeStrings` is false, all Info-ZIP Unicode Path Extra Fields are ignored.

None of the other fields are considered significant by this library.
Fields that this library reads are left unalterned in the `extraFields` array.

#### fileComment

`String` decoded with the charset indicated by `generalPurposeBitFlag & 0x800` as with the `fileName`.
(The Info-ZIP Unicode Path Extra Field has no effect on the charset used for this field.)

If `decodeStrings` is `false` (see `open()`), this field is the undecoded `Buffer` instead of a decoded `String`.

Prior to yauzl version 2.7.0, this field was erroneously documented as `comment` instead of `fileComment`.
For compatibility with any code that uses the field name `comment`,
yauzl creates an alias field named `comment` which is identical to `fileComment`.

#### getLastModDate()

Effectively implemented as:

```js
return dosDateTimeToDate(this.lastModFileDate, this.lastModFileTime);
```

#### isEncrypted()

Returns is this entry encrypted with "Traditional Encryption".
Effectively implemented as:

```js
return (this.generalPurposeBitFlag & 0x1) !== 0;
```

See `openReadStream()` for the implications of this value.

Note that "Strong Encryption" is not supported, and will result in an `"error"` event emitted from the `ZipFile`.

#### isCompressed()

Effectively implemented as:

```js
return this.compressionMethod === 8;
```

See `openReadStream()` for the implications of this value.

### Class: RandomAccessReader

This class is meant to be subclassed by clients and instantiated for the `fromRandomAccessReader()` function.

An example implementation can be found in `test/test.js`.

#### randomAccessReader._readStreamForRange(start, end)

Subclasses *must* implement this method.

`start` and `end` are Numbers and indicate byte offsets from the start of the file.
`end` is exclusive, so `_readStreamForRange(0x1000, 0x2000)` would indicate to read `0x1000` bytes.
`end - start` will always be at least `1`.

This method should return a readable stream which will be `pipe()`ed into another stream.
It is expected that the readable stream will provide data in several chunks if necessary.
If the readable stream provides too many or too few bytes, an error will be emitted.
(Note that `validateEntrySizes` has no effect on this check,
because this is a low-level API that should behave correctly regardless of the contents of the file.)
Any errors emitted on the readable stream will be handled and re-emitted on the client-visible stream
(returned from `zipfile.openReadStream()`) or provided as the `err` argument to the appropriate callback
(for example, for `fromRandomAccessReader()`).

The returned stream *must* implement a method `.destroy()`
if you call `readStream.destroy()` on streams you get from `openReadStream()`.
If you never call `readStream.destroy()`, then streams returned from this method do not need to implement a method `.destroy()`.
`.destroy()` should abort any streaming that is in progress and clean up any associated resources.
`.destroy()` will only be called after the stream has been `unpipe()`d from its destination.

Note that the stream returned from this method might not be the same object that is provided by `openReadStream()`.
The stream returned from this method might be `pipe()`d through one or more filter streams (for example, a zlib inflate stream).

#### randomAccessReader.read(buffer, offset, length, position, callback)

Subclasses may implement this method.
The default implementation uses `createReadStream()` to fill the `buffer`.

This method should behave like `fs.read()`.

#### randomAccessReader.close(callback)

Subclasses may implement this method.
The default implementation is effectively `setImmediate(callback);`.

`callback` takes parameters `(err)`.

This method is called once the all streams returned from `_readStreamForRange()` have ended,
and no more `_readStreamForRange()` or `read()` requests will be issued to this object.

## How to Avoid Crashing

When a malformed zipfile is encountered, the default behavior is to crash (throw an exception).
If you want to handle errors more gracefully than this,
be sure to do the following:

 * Provide `callback` parameters where they are allowed, and check the `err` parameter.
 * Attach a listener for the `error` event on any `ZipFile` object you get from `open()`, `fromFd()`, `fromBuffer()`, or `fromRandomAccessReader()`.
 * Attach a listener for the `error` event on any stream you get from `openReadStream()`.

Minor version updates to yauzl will not add any additional requirements to this list.

## Limitations

### No Streaming Unzip API

Due to the design of the .zip file format, it's impossible to interpret a .zip file from start to finish
(such as from a readable stream) without sacrificing correctness.
The Central Directory, which is the authority on the contents of the .zip file, is at the end of a .zip file, not the beginning.
A streaming API would need to either buffer the entire .zip file to get to the Central Directory before interpreting anything
(defeating the purpose of a streaming interface), or rely on the Local File Headers which are interspersed through the .zip file.
However, the Local File Headers are explicitly denounced in the spec as being unreliable copies of the Central Directory,
so trusting them would be a violation of the spec.

Any library that offers a streaming unzip API must make one of the above two compromises,
which makes the library either dishonest or nonconformant (usually the latter).
This library insists on correctness and adherence to the spec, and so does not offer a streaming API.

Here is a way to create a spec-conformant .zip file using the `zip` command line program (Info-ZIP)
available in most unix-like environments, that is (nearly) impossible to parse correctly with a streaming parser:

```
$ echo -ne '\x50\x4b\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' > file.txt
$ zip -q0 - file.txt | cat > out.zip
```

This .zip file contains a single file entry that uses General Purpose Bit 3,
which means the Local File Header doesn't know the size of the file.
Any streaming parser that encounters this situation will either immediately fail,
or attempt to search for the Data Descriptor after the file's contents.
The file's contents is a sequence of 16-bytes crafted to exactly mimic a valid Data Descriptor for an empty file,
which will fool any parser that gets this far into thinking that the file is empty rather than containing 16-bytes.
What follows the file's real contents is the file's real Data Descriptor,
which will likely cause some kind of signature mismatch error for a streaming parser (if one hasn't occurred already).

By using General Purpose Bit 3 (and compression method 0),
it's possible to create arbitrarily ambiguous .zip files that
distract parsers with file contents that contain apparently valid .zip file metadata.

### Limitted ZIP64 Support

For ZIP64, only zip files smaller than `8PiB` are supported,
not the full `16EiB` range that a 64-bit integer should be able to index.
This is due to the JavaScript Number type being an IEEE 754 double precision float.

The Node.js `fs` module probably has this same limitation.

### ZIP64 Extensible Data Sector Is Ignored

The spec does not allow zip file creators to put arbitrary data here,
but rather reserves its use for PKWARE and mentions something about Z390.
This doesn't seem useful to expose in this library, so it is ignored.

### No Multi-Disk Archive Support

This library does not support multi-disk zip files.
The multi-disk fields in the zipfile spec were intended for a zip file to span multiple floppy disks,
which probably never happens now.
If the "number of this disk" field in the End of Central Directory Record is not `0`,
the `open()`, `fromFd()`, `fromBuffer()`, or `fromRandomAccessReader()` `callback` will receive an `err`.
By extension the following zip file fields are ignored by this library and not provided to clients:

 * Disk where central directory starts
 * Number of central directory records on this disk
 * Disk number where file starts

### Limited Encryption Handling

You can detect when a file entry is encrypted with "Traditional Encryption" via `isEncrypted()`,
but yauzl will not help you decrypt it.
See `openReadStream()`.

If a zip file contains file entries encrypted with "Strong Encryption", yauzl emits an error.

If the central directory is encrypted or compressed, yauzl emits an error.

### Local File Headers Are Ignored

Many unzip libraries mistakenly read the Local File Header data in zip files.
This data is officially defined to be redundant with the Central Directory information,
and is not to be trusted.
Aside from checking the signature, yauzl ignores the content of the Local File Header.

### No CRC-32 Checking

This library provides the `crc32` field of `Entry` objects read from the Central Directory.
However, this field is not used for anything in this library.

### versionNeededToExtract Is Ignored

The field `versionNeededToExtract` is ignored,
because this library doesn't support the complete zip file spec at any version,

### No Support For Obscure Compression Methods

Regarding the `compressionMethod` field of `Entry` objects,
only method `0` (stored with no compression)
and method `8` (deflated) are supported.
Any of the other 15 official methods will cause the `openReadStream()` `callback` to receive an `err`.

### Data Descriptors Are Ignored

There may or may not be Data Descriptor sections in a zip file.
This library provides no support for finding or interpreting them.

### Archive Extra Data Record Is Ignored

There may or may not be an Archive Extra Data Record section in a zip file.
This library provides no support for finding or interpreting it.

### No Language Encoding Flag Support

Zip files officially support charset encodings other than CP437 and UTF-8,
but the zip file spec does not specify how it works.
This library makes no attempt to interpret the Language Encoding Flag.

## Change History

 * 2.10.0
   * Added support for non-conformant zipfiles created by Microsoft, and added option `strictFileNames` to disable the workaround. [issue #66](https://github.com/thejoshwolfe/yauzl/issues/66), [issue #88](https://github.com/thejoshwolfe/yauzl/issues/88)
 * 2.9.2
   * Removed `tools/hexdump-zip.js` and `tools/hex2bin.js`. Those tools are now located here: [thejoshwolfe/hexdump-zip](https://github.com/thejoshwolfe/hexdump-zip) and [thejoshwolfe/hex2bin](https://github.com/thejoshwolfe/hex2bin)
   * Worked around performance problem with zlib when using `fromBuffer()` and `readStream.destroy()` for large compressed files. [issue #87](https://github.com/thejoshwolfe/yauzl/issues/87)
 * 2.9.1
   * Removed `console.log()` accidentally introduced in 2.9.0. [issue #64](https://github.com/thejoshwolfe/yauzl/issues/64)
 * 2.9.0
   * Throw an exception if `readEntry()` is called without `lazyEntries:true`. Previously this caused undefined behavior. [issue #63](https://github.com/thejoshwolfe/yauzl/issues/63)
 * 2.8.0
   * Added option `validateEntrySizes`. [issue #53](https://github.com/thejoshwolfe/yauzl/issues/53)
   * Added `examples/promises.js`
   * Added ability to read raw file data via `decompress` and `decrypt` options. [issue #11](https://github.com/thejoshwolfe/yauzl/issues/11), [issue #38](https://github.com/thejoshwolfe/yauzl/issues/38), [pull #39](https://github.com/thejoshwolfe/yauzl/pull/39)
   * Added `start` and `end` options to `openReadStream()`. [issue #38](https://github.com/thejoshwolfe/yauzl/issues/38)
 * 2.7.0
   * Added option `decodeStrings`. [issue #42](https://github.com/thejoshwolfe/yauzl/issues/42)
   * Fixed documentation for `entry.fileComment` and added compatibility alias. [issue #47](https://github.com/thejoshwolfe/yauzl/issues/47)
 * 2.6.0
   * Support Info-ZIP Unicode Path Extra Field, used by WinRAR for Chinese file names. [issue #33](https://github.com/thejoshwolfe/yauzl/issues/33)
 * 2.5.0
   * Ignore malformed Extra Field that is common in Android .apk files. [issue #31](https://github.com/thejoshwolfe/yauzl/issues/31)
 * 2.4.3
   * Fix crash when parsing malformed Extra Field buffers. [issue #31](https://github.com/thejoshwolfe/yauzl/issues/31)
 * 2.4.2
   * Remove .npmignore and .travis.yml from npm package.
 * 2.4.1
   * Fix error handling.
 * 2.4.0
   * Add ZIP64 support. [issue #6](https://github.com/thejoshwolfe/yauzl/issues/6)
   * Add `lazyEntries` option. [issue #22](https://github.com/thejoshwolfe/yauzl/issues/22)
   * Add `readStream.destroy()` method. [issue #26](https://github.com/thejoshwolfe/yauzl/issues/26)
   * Add `fromRandomAccessReader()`. [issue #14](https://github.com/thejoshwolfe/yauzl/issues/14)
   * Add `examples/unzip.js`.
 * 2.3.1
   * Documentation updates.
 * 2.3.0
   * Check that `uncompressedSize` is correct, or else emit an error. [issue #13](https://github.com/thejoshwolfe/yauzl/issues/13)
 * 2.2.1
   * Update dependencies.
 * 2.2.0
   * Update dependencies.
 * 2.1.0
   * Remove dependency on `iconv`.
 * 2.0.3
   * Fix crash when trying to read a 0-byte file.
 * 2.0.2
   * Fix event behavior after errors.
 * 2.0.1
   * Fix bug with using `iconv`.
 * 2.0.0
   * Initial release.