"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Xpi = void 0;
const yauzl_1 = __importDefault(require("yauzl"));
const first_chunk_stream_1 = __importDefault(require("first-chunk-stream"));
const strip_bom_stream_1 = __importDefault(require("strip-bom-stream"));
const common_tags_1 = require("common-tags");
const base_1 = require("./base");
const errors_1 = require("../errors");
/*
 * Simple Promise wrapper for the Yauzl unzipping lib to unpack add-on .xpis.
 *
 * Note: We're using the autoclose feature of yauzl as a result every operation
 * will open the zip, do something and then close it implicitly. This makes the
 * API easy to use and the consumer doesn't need to remember to close the
 * zipfile. That being said, we sometimes need to control the autoclose
 * feature, so we can disable it if needed.
 */
class Xpi extends base_1.IOBase {
    constructor({ autoClose = true, filePath, stderr, zipLib = yauzl_1.default, }) {
        super({ filePath, stderr });
        this.autoClose = autoClose;
        this.files = {};
        this.processed = false;
        this.zipLib = zipLib;
    }
    open() {
        return new Promise((resolve, reject) => {
            // When we disable the autoclose feature, we can reuse the same file
            // descriptor instead of creating new ones, but only if we have opened
            // the file once and the descriptor is still open.
            if (!this.autoClose && this.zipfile && this.zipfile.isOpen) {
                resolve(this.zipfile);
                return;
            }
            this.zipLib.open(this.path, {
                autoClose: this.autoClose,
                // Enable checks on invalid chars in zip entries filenames.
                strictFileNames: true,
                // Decode automatically filenames and zip entries content from buffer into strings
                // and autodetects their encoding.
                //
                // NOTE: this is also mandatory because without this option set to true
                // strictFileNames option is ignored.
                decodeStrings: true,
            }, (err, zipfile) => {
                if (err) {
                    return reject(err);
                }
                this.zipfile = zipfile;
                return resolve(zipfile);
            });
        });
    }
    handleEntry(entry, reject) {
        if (/\/$/.test(entry.fileName)) {
            return;
        }
        if (!this.shouldScanFile(entry.fileName, false)) {
            this.stderr.debug(`skipping file: ${entry.fileName}`);
            return;
        }
        if (this.entries.includes(entry.fileName)) {
            this.stderr.info((0, common_tags_1.oneLine) `found duplicate file entry: "${entry.fileName}"
        in package`);
            reject(new errors_1.DuplicateZipEntryError((0, common_tags_1.oneLine) `Entry "${entry.fileName}" has already
          been seen`));
            return;
        }
        this.entries.push(entry.fileName);
        this.files[entry.fileName] = entry;
    }
    getFiles(_onEventsSubscribed) {
        return __awaiter(this, void 0, void 0, function* () {
            // If we have already processed the file and have data on this instance
            // return that.
            if (this.processed) {
                const wantedFiles = {};
                Object.keys(this.files).forEach((fileName) => {
                    if (this.shouldScanFile(fileName, false)) {
                        wantedFiles[fileName] = this.files[fileName];
                    }
                    else {
                        this.stderr.debug(`skipping cached file: ${fileName}`);
                    }
                });
                return wantedFiles;
            }
            const zipfile = yield this.open();
            return new Promise((resolve, reject) => {
                zipfile.on('error', (err) => {
                    reject(new errors_1.InvalidZipFileError(err.message));
                });
                zipfile.on('entry', (entry) => {
                    this.handleEntry(entry, reject);
                });
                // When the last entry has been processed, resolve the promise.
                //
                // Note: we were using 'close' before because of a potential race
                // condition but we are not able to reproduce it and the `yauzl` code has
                // changed a bit. We are using 'end' again now so that this function
                // continues to work with `autoClose: false`.
                //
                // See: https://github.com/mozilla/addons-linter/pull/43
                zipfile.on('end', () => {
                    this.processed = true;
                    resolve(this.files);
                });
                if (_onEventsSubscribed) {
                    // Run optional callback when we know the event handlers
                    // have been inited. Useful for testing.
                    if (typeof _onEventsSubscribed === 'function') {
                        Promise.resolve().then(() => _onEventsSubscribed());
                    }
                }
            });
        });
    }
    checkPath(path) {
        if (!Object.prototype.hasOwnProperty.call(this.files, path)) {
            throw new Error(`Path "${path}" does not exist in this XPI`);
        }
        if (this.files[path].uncompressedSize > this.maxSizeBytes) {
            throw new Error(`File "${path}" is too large. Aborting.`);
        }
    }
    getFileAsStream(path) {
        return __awaiter(this, void 0, void 0, function* () {
            this.checkPath(path);
            const zipfile = yield this.open();
            return new Promise((resolve, reject) => {
                zipfile.openReadStream(this.files[path], (err, readStream) => {
                    if (err) {
                        return reject(err);
                    }
                    if (!readStream) {
                        return reject(new Error('readStream is falsey'));
                    }
                    return resolve(readStream.pipe((0, strip_bom_stream_1.default)()));
                });
            });
        });
    }
    getFileAsString(path) {
        return __awaiter(this, void 0, void 0, function* () {
            const fileStream = yield this.getFileAsStream(path);
            return new Promise((resolve, reject) => {
                let buf = Buffer.from('');
                fileStream.on('data', (chunk) => {
                    buf = Buffer.concat([buf, chunk]);
                });
                // Once the file is assembled, resolve the promise.
                fileStream.on('end', () => {
                    const fileString = buf.toString('utf8');
                    resolve(fileString);
                });
                fileStream.on('error', reject);
            });
        });
    }
    getChunkAsBuffer(path, chunkLength) {
        return __awaiter(this, void 0, void 0, function* () {
            this.checkPath(path);
            const zipfile = yield this.open();
            return new Promise((resolve, reject) => {
                zipfile.openReadStream(this.files[path], (err, readStream) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    if (!readStream) {
                        reject(new Error('readStream is falsey'));
                        return;
                    }
                    readStream.pipe(new first_chunk_stream_1.default({ chunkLength }, (_, enc) => {
                        resolve(enc);
                    }));
                });
            });
        });
    }
    close() {
        if (this.autoClose) {
            return;
        }
        if (this.zipfile) {
            // According to the yauzl docs, it is safe to call `close()` multiple
            // times so we don't check `isOpen` here.
            this.zipfile.close();
        }
    }
}
exports.Xpi = Xpi;
