"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const zlib_1 = require("zlib");
const multistream_1 = __importDefault(require("multistream"));
const assert_1 = __importDefault(require("assert"));
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const into_stream_1 = __importDefault(require("into-stream"));
const path_1 = __importDefault(require("path"));
const stream_meter_1 = __importDefault(require("stream-meter"));
const common_1 = require("./common");
const log_1 = require("./log");
const fabricator_1 = require("./fabricator");
const types_1 = require("./types");
const compress_type_1 = require("./compress_type");
function discoverPlaceholder(binaryBuffer, searchString, padder, searchOffset = 0) {
    const placeholder = Buffer.from(searchString);
    const position = binaryBuffer.indexOf(placeholder, searchOffset);
    if (position === -1) {
        return { notFound: true };
    }
    /**
     * the PAYLOAD/PRELUDE placeholders can occur twice in the binaries:
     *  - in source text as a string literal
     *  - in bytecode as a raw string
     * the ordering depends on the platform - we need to make sure that
     * the bytecode string is replaced, not the source literal.
     *
     * this rejects the source code literal if it occurs first in the binary
     * also see: https://github.com/yao-pkg/pkg/pull/86
     */
    if (binaryBuffer[position - 1] === 39 /* ascii for ' APOSTROPHE */) {
        const nextPlaceholder = discoverPlaceholder(binaryBuffer, searchString, padder, position + placeholder.length);
        if (!('notFound' in nextPlaceholder)) {
            return nextPlaceholder;
        }
    }
    return { position, size: placeholder.length, padder };
}
function injectPlaceholder(fd, placeholder, value, cb) {
    if ('notFound' in placeholder) {
        (0, assert_1.default)(false, 'Placeholder for not found');
    }
    const { position, size, padder } = placeholder;
    let stringValue = Buffer.from('');
    if (typeof value === 'number') {
        stringValue = Buffer.from(value.toString());
    }
    else if (typeof value === 'string') {
        stringValue = Buffer.from(value);
    }
    else {
        stringValue = value;
    }
    const padding = Buffer.from(padder.repeat(size - stringValue.length));
    stringValue = Buffer.concat([stringValue, padding]);
    fs_1.default.write(fd, stringValue, 0, stringValue.length, position, cb);
}
function discoverPlaceholders(binaryBuffer) {
    return {
        BAKERY: discoverPlaceholder(binaryBuffer, `\0${'// BAKERY '.repeat(20)}`, '\0'),
        PAYLOAD_POSITION: discoverPlaceholder(binaryBuffer, '// PAYLOAD_POSITION //', ' '),
        PAYLOAD_SIZE: discoverPlaceholder(binaryBuffer, '// PAYLOAD_SIZE //', ' '),
        PRELUDE_POSITION: discoverPlaceholder(binaryBuffer, '// PRELUDE_POSITION //', ' '),
        PRELUDE_SIZE: discoverPlaceholder(binaryBuffer, '// PRELUDE_SIZE //', ' '),
    };
}
function injectPlaceholders(fd, placeholders, values, cb) {
    injectPlaceholder(fd, placeholders.BAKERY, values.BAKERY, (error) => {
        if (error) {
            return cb(error);
        }
        injectPlaceholder(fd, placeholders.PAYLOAD_POSITION, values.PAYLOAD_POSITION, (error2) => {
            if (error2) {
                return cb(error2);
            }
            injectPlaceholder(fd, placeholders.PAYLOAD_SIZE, values.PAYLOAD_SIZE, (error3) => {
                if (error3) {
                    return cb(error3);
                }
                injectPlaceholder(fd, placeholders.PRELUDE_POSITION, values.PRELUDE_POSITION, (error4) => {
                    if (error4) {
                        return cb(error4);
                    }
                    injectPlaceholder(fd, placeholders.PRELUDE_SIZE, values.PRELUDE_SIZE, cb);
                });
            });
        });
    });
}
function makeBakeryValueFromBakes(bakes) {
    const parts = [];
    if (bakes.length) {
        for (let i = 0; i < bakes.length; i += 1) {
            parts.push(Buffer.from(bakes[i]));
            parts.push(Buffer.alloc(1));
        }
        parts.push(Buffer.alloc(1));
    }
    return Buffer.concat(parts);
}
function replaceDollarWise(s, sf, st) {
    return s.replace(sf, () => st);
}
function makePreludeBufferFromPrelude(prelude) {
    return Buffer.from(`(function(process, require, console, EXECPATH_FD, PAYLOAD_POSITION, PAYLOAD_SIZE) { ${prelude}\n})`);
}
function findPackageJson(nodeFile) {
    let dir = nodeFile;
    while (dir !== '/') {
        dir = path_1.default.dirname(dir);
        if (fs_1.default.existsSync(path_1.default.join(dir, 'package.json'))) {
            break;
        }
    }
    if (dir === '/') {
        throw new Error(`package.json not found for "${nodeFile}"`);
    }
    return dir;
}
function getPrebuildEnvPrefix(pkgName) {
    return `npm_config_${(pkgName || '')
        .replace(/[^a-zA-Z0-9]/g, '_')
        .replace(/^_/, '')}`;
}
function nativePrebuildInstall(target, nodeFile) {
    var _a, _b;
    const prebuildInstall = path_1.default.join(__dirname, '../node_modules/.bin/prebuild-install');
    const dir = findPackageJson(nodeFile);
    const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, 'package.json'), { encoding: 'utf-8' }));
    // only try prebuild-install for packages that actually use it or if
    // explicitly configured via environment variables
    const envPrefix = getPrebuildEnvPrefix(packageJson.name);
    if (((_a = packageJson.dependencies) === null || _a === void 0 ? void 0 : _a['prebuild-install']) == null &&
        ![
            `${envPrefix}_binary_host`,
            `${envPrefix}_binary_host_mirror`,
            `${envPrefix}_local_prebuilds`,
        ].some((i) => i in process.env)) {
        return;
    }
    // parse the target node version from the binaryPath
    const nodeVersion = path_1.default.basename(target.binaryPath).split('-')[1];
    if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(nodeVersion)) {
        throw new Error(`Couldn't find node version, instead got: ${nodeVersion}`);
    }
    const nativeFile = `${nodeFile}.${target.platform}.${target.arch}.${nodeVersion}`;
    if (fs_1.default.existsSync(nativeFile)) {
        return nativeFile;
    }
    // prebuild-install will overwrite the target .node file, so take a backup
    if (!fs_1.default.existsSync(`${nodeFile}.bak`)) {
        fs_1.default.copyFileSync(nodeFile, `${nodeFile}.bak`);
    }
    const napiVersions = (_b = packageJson === null || packageJson === void 0 ? void 0 : packageJson.binary) === null || _b === void 0 ? void 0 : _b.napi_versions;
    const options = [
        '--platform',
        types_1.platform[target.platform],
        '--arch',
        target.arch,
    ];
    if (napiVersions == null) {
        // TODO: consider target node version and supported n-api version
        options.push('--target', nodeVersion);
    }
    else {
        options.push('--runtime', 'napi');
    }
    // run prebuild
    (0, child_process_1.execFileSync)(prebuildInstall, options, { cwd: dir });
    // move the prebuild to a new name with a platform/version extension
    fs_1.default.copyFileSync(nodeFile, nativeFile);
    // put the backed up file back
    fs_1.default.renameSync(`${nodeFile}.bak`, nodeFile);
    return nativeFile;
}
/**
 * instead of creating a vfs dicionnary with actual path as key
 * we use a compression mechanism that can reduce significantly
 * the memory footprint of the vfs in the code.
 *
 * without vfs compression:
 *
 * vfs = {
 *   "/folder1/folder2/file1.js": {};
 *   "/folder1/folder2/folder3/file2.js": {};
 *   "/folder1/folder2/folder3/file3.js": {};
 * }
 *
 * with compression :
 *
 * fileDictionary = {
 *    "folder1": "1",
 *    "folder2": "2",
 *    "file1": "3",
 *    "folder3": "4",
 *    "file2": "5",
 *    "file3": "6",
 * }
 * vfs = {
 *   "/1/2/3": {};
 *   "/1/2/4/5": {};
 *   "/1/2/4/6": {};
 * }
 *
 * note: the key is computed in base36 for further compression.
 */
const fileDictionary = {};
let counter = 0;
function getOrCreateHash(fileOrFolderName) {
    let existingKey = fileDictionary[fileOrFolderName];
    if (!existingKey) {
        const newkey = counter;
        counter += 1;
        existingKey = newkey.toString(36);
        fileDictionary[fileOrFolderName] = existingKey;
    }
    return existingKey;
}
const separator = '/';
function makeKey(doCompression, fullpath, slash) {
    if (doCompression === compress_type_1.CompressType.None)
        return fullpath;
    return fullpath.split(slash).map(getOrCreateHash).join(separator);
}
function producer({ backpack, bakes, slash, target, symLinks, doCompress, nativeBuild, }) {
    return new Promise((resolve, reject) => {
        if (!Buffer.alloc) {
            throw (0, log_1.wasReported)('Your node.js does not have Buffer.alloc. Please upgrade!');
        }
        const { prelude } = backpack;
        let { entrypoint, stripes } = backpack;
        entrypoint = (0, common_1.snapshotify)(entrypoint, slash);
        stripes = stripes.slice();
        const vfs = {};
        for (const stripe of stripes) {
            let { snap } = stripe;
            snap = (0, common_1.snapshotify)(snap, slash);
            const vfsKey = makeKey(doCompress, snap, slash);
            if (!vfs[vfsKey])
                vfs[vfsKey] = {};
        }
        const snapshotSymLinks = {};
        for (const [key, value] of Object.entries(symLinks)) {
            const k = (0, common_1.snapshotify)(key, slash);
            const v = (0, common_1.snapshotify)(value, slash);
            const vfsKey = makeKey(doCompress, k, slash);
            snapshotSymLinks[vfsKey] = makeKey(doCompress, v, slash);
        }
        let meter;
        let count = 0;
        function pipeToNewMeter(s) {
            meter = (0, stream_meter_1.default)();
            return s.pipe(meter);
        }
        function pipeMayCompressToNewMeter(s) {
            if (doCompress === compress_type_1.CompressType.GZip) {
                return pipeToNewMeter(s.pipe((0, zlib_1.createGzip)()));
            }
            if (doCompress === compress_type_1.CompressType.Brotli) {
                return pipeToNewMeter(s.pipe((0, zlib_1.createBrotliCompress)()));
            }
            return pipeToNewMeter(s);
        }
        function next(s) {
            count += 1;
            return pipeToNewMeter(s);
        }
        const binaryBuffer = fs_1.default.readFileSync(target.binaryPath);
        const placeholders = discoverPlaceholders(binaryBuffer);
        let track = 0;
        let prevStripe;
        let payloadPosition;
        let payloadSize;
        let preludePosition;
        let preludeSize;
        new multistream_1.default((cb) => {
            if (count === 0) {
                return cb(null, next((0, into_stream_1.default)(binaryBuffer)));
            }
            if (count === 1) {
                payloadPosition = meter.bytes;
                return cb(null, next((0, into_stream_1.default)(Buffer.alloc(0))));
            }
            if (count === 2) {
                if (prevStripe && !prevStripe.skip) {
                    const { store } = prevStripe;
                    let { snap } = prevStripe;
                    snap = (0, common_1.snapshotify)(snap, slash);
                    const vfsKey = makeKey(doCompress, snap, slash);
                    vfs[vfsKey][store] = [track, meter.bytes];
                    track += meter.bytes;
                }
                if (stripes.length) {
                    // clone to prevent 'skip' propagate
                    // to other targets, since same stripe
                    // is used for several targets
                    const stripe = Object.assign({}, stripes.shift());
                    prevStripe = stripe;
                    if (stripe.buffer) {
                        if (stripe.store === common_1.STORE_BLOB) {
                            const snap = (0, common_1.snapshotify)(stripe.snap, slash);
                            return (0, fabricator_1.fabricateTwice)(bakes, target.fabricator, snap, stripe.buffer, (error, buffer) => {
                                if (error) {
                                    log_1.log.warn(error.message);
                                    stripe.skip = true;
                                    return cb(null, (0, into_stream_1.default)(Buffer.alloc(0)));
                                }
                                cb(null, pipeMayCompressToNewMeter((0, into_stream_1.default)(buffer || Buffer.from(''))));
                            });
                        }
                        return cb(null, pipeMayCompressToNewMeter((0, into_stream_1.default)(stripe.buffer)));
                    }
                    if (stripe.file) {
                        if (stripe.file === target.output) {
                            return cb((0, log_1.wasReported)('Trying to take executable into executable', stripe.file), null);
                        }
                        assert_1.default.strictEqual(stripe.store, common_1.STORE_CONTENT); // others must be buffers from walker
                        if ((0, common_1.isDotNODE)(stripe.file) && nativeBuild) {
                            try {
                                const platformFile = nativePrebuildInstall(target, stripe.file);
                                if (platformFile && fs_1.default.existsSync(platformFile)) {
                                    return cb(null, pipeMayCompressToNewMeter(fs_1.default.createReadStream(platformFile)));
                                }
                            }
                            catch (err) {
                                log_1.log.debug(`prebuild-install failed[${stripe.file}]:`, err.message);
                            }
                        }
                        return cb(null, pipeMayCompressToNewMeter(fs_1.default.createReadStream(stripe.file)));
                    }
                    (0, assert_1.default)(false, 'producer: bad stripe');
                }
                else {
                    payloadSize = track;
                    preludePosition = payloadPosition + payloadSize;
                    return cb(null, next((0, into_stream_1.default)(makePreludeBufferFromPrelude(replaceDollarWise(replaceDollarWise(replaceDollarWise(replaceDollarWise(replaceDollarWise(prelude, '%VIRTUAL_FILESYSTEM%', JSON.stringify(vfs)), '%DEFAULT_ENTRYPOINT%', JSON.stringify(entrypoint)), '%SYMLINKS%', JSON.stringify(snapshotSymLinks)), '%DICT%', JSON.stringify(fileDictionary)), '%DOCOMPRESS%', JSON.stringify(doCompress))))));
                }
            }
            else {
                return cb(null, null);
            }
        })
            .on('error', (error) => {
            reject(error);
        })
            .pipe(fs_1.default.createWriteStream(target.output))
            .on('error', (error) => {
            reject(error);
        })
            .on('close', () => {
            preludeSize = meter.bytes;
            fs_1.default.open(target.output, 'r+', (error, fd) => {
                if (error)
                    return reject(error);
                injectPlaceholders(fd, placeholders, {
                    BAKERY: makeBakeryValueFromBakes(bakes),
                    PAYLOAD_POSITION: payloadPosition,
                    PAYLOAD_SIZE: payloadSize,
                    PRELUDE_POSITION: preludePosition,
                    PRELUDE_SIZE: preludeSize,
                }, (error2) => {
                    if (error2)
                        return reject(error2);
                    fs_1.default.close(fd, (error3) => {
                        if (error3)
                            return reject(error3);
                        resolve();
                    });
                });
            });
        });
    });
}
exports.default = producer;
//# sourceMappingURL=producer.js.map