Source: resources/untar.js

Object.assign(pc, function () {
    var Untar; // see below why we declare this here

    // The UntarScope function is going to be used
    // as the code that ends up in a Web Worker.
    // The Untar variable is declared outside the scope so that
    // we do not have to add a 'return' statement to the UntarScope function.
    // We also have to make sure that we do not mangle 'Untar' variable otherwise
    // the Worker will not work.
    function UntarScope(isWorker) {
        'use strict';

        var utfDecoder;
        var asciiDecoder;

        if (typeof TextDecoder !== 'undefined') {
            utfDecoder = new TextDecoder('utf-8');
            asciiDecoder = new TextDecoder('windows-1252');
        } else {
            console.warn('TextDecoder not supported - pc.Untar module will not work');
        }

        function PaxHeader(fields) {
            this._fields = fields;
        }

        PaxHeader.parse = function (buffer, start, length) {
            var paxArray = new Uint8Array(buffer, start, length);
            var bytesRead = 0;
            var fields = [];

            while (bytesRead < length) {
                var spaceIndex;
                for (spaceIndex = bytesRead; spaceIndex < length; spaceIndex++) {
                    if (paxArray[spaceIndex] == 0x20)
                        break;
                }

                if (spaceIndex >= length) {
                    throw new Error('Invalid PAX header data format.');
                }


                var fieldLength = parseInt(utfDecoder.decode(new Uint8Array(buffer, start + bytesRead, spaceIndex - bytesRead)), 10);
                var fieldText = utfDecoder.decode(new Uint8Array(buffer, start + spaceIndex + 1, fieldLength - (spaceIndex - bytesRead) - 2));
                var field = fieldText.split('=');

                if (field.length !== 2) {
                    throw new Error('Invalid PAX header data format.');
                }

                if (field[1].length === 0) {
                    field[1] = null;
                }

                fields.push({
                    name: field[0],
                    value: field[1]
                });

                bytesRead += fieldLength;
            }

            return new PaxHeader(fields);
        };

        PaxHeader.prototype.applyHeader = function (file) {
            for (var i = 0; i < this._fields.length; i++) {
                var fieldName = this._fields[i].name;
                var fieldValue = this._fields[i].value;

                if (fieldName === 'path') {
                    fieldName = 'name';
                }

                if (fieldValue === null) {
                    delete file[fieldName];
                } else {
                    file[fieldName] = fieldValue;
                }
            }
        };

        /**
         * @private
         * @name pc.Untar
         * @classdesc Untars a tar archive in the form of an array buffer
         * @param {ArrayBuffer} arrayBuffer The array buffer that holds the tar archive
         * @description Creates a new instance of pc.Untar
         */
        var UntarInternal = function (arrayBuffer) {
            this._arrayBuffer = arrayBuffer || new ArrayBuffer(0);
            this._bufferView = new DataView(this._arrayBuffer);
            this._globalPaxHeader = null;
            this._bytesRead = 0;
        };

        if (! isWorker) {
            Untar = UntarInternal;
        }

        /**
         * @private
         * @function
         * @name pc.Untar#_hasNext
         * @description Whether we have more files to untar
         * @returns {Boolean} Returns true or false
         */
        UntarInternal.prototype._hasNext = function () {
            return this._bytesRead + 4 < this._arrayBuffer.byteLength && this._bufferView.getUint32(this._bytesRead) !== 0;
        };

        /**
         * @private
         * @function
         * @name pc.Untar#_readNextFile
         * @description Untars the next file in the archive
         * @returns {Object} Returns a file descriptor in the following format:
         * {name, size, start, url}
         */
        UntarInternal.prototype._readNextFile = function () {
            var headersDataView = new DataView(this._arrayBuffer, this._bytesRead, 512);
            var headers = asciiDecoder.decode(headersDataView);
            this._bytesRead += 512;

            var name = headers.substr(0, 100).replace(/\0/g, '');
            var ustarFormat = headers.substr(257, 6);
            var size = parseInt(headers.substr(124, 12), 8);
            var type = headers.substr(156, 1);
            var start = this._bytesRead;
            var url = null;

            var paxHeader;
            var normalFile = false;
            switch (type) {
                case "0": case "": // Normal file
                    // do not create blob URL if we are in a worker
                    // because if the worker is destroyed it will also destroy the blob URLs
                    normalFile = true;
                    if (!isWorker) {
                        var blob = new Blob([this._arrayBuffer.slice(this._bytesRead, this._bytesRead + size)]);
                        url = URL.createObjectURL(blob);
                    }
                    break;
                case "g": // Global PAX header
                    this._globalPaxHeader = PaxHeader.parse(this._arrayBuffer, this._bytesRead, size);
                    break;
                case "x": // PAX header
                    paxHeader = PaxHeader.parse(this._arrayBuffer, this._bytesRead, size);
                    break;
                case "1": // Link to another file already archived
                case "2": // Symbolic link
                case "3": // Character special device
                case "4": // Block special device
                case "5": // Directory
                case "6": // FIFO special file
                case "7": // Reserved
                default: // Unknown file type
            }

            this._bytesRead += size;

            // File data is padded to reach a 512 byte boundary; skip the padded bytes too.
            var remainder = size % 512;
            if (remainder !== 0) {
                this._bytesRead += (512 - remainder);
            }

            if (! normalFile) {
                return null;
            }

            if (ustarFormat.indexOf("ustar") !== -1) {
                var namePrefix = headers.substr(345, 155).replace(/\0/g, '');

                if (namePrefix.length > 0) {
                    name = namePrefix.trim() + name.trim();
                }
            }

            var file = {
                name: name,
                start: start,
                size: size,
                url: url
            };

            if (this._globalPaxHeader) {
                this._globalPaxHeader.applyHeader(file);
            }

            if (paxHeader) {
                paxHeader.applyHeader(file);
            }

            return file;
        };

        /**
         * @private
         * @function
         * @name pc.Untar#untar
         * @description Untars the array buffer provided in the constructor.
         * @param {String} [filenamePrefix] The prefix for each filename in the tar archive. This is usually the {@link pc.AssetRegistry} prefix.
         * @returns {Object[]} An array of files in this format {name, start, size, url}
         */
        UntarInternal.prototype.untar = function (filenamePrefix) {
            if (! utfDecoder) {
                console.error('Cannot untar because TextDecoder interface is not available for this platform.');
                return [];
            }

            var files = [];
            while (this._hasNext()) {
                var file = this._readNextFile();
                if (! file) continue;
                if (filenamePrefix && file.name) {
                    file.name = filenamePrefix + file.name;
                }
                files.push(file);
            }

            return files;
        };

        // if we are in a worker then create the onmessage handler using worker.self
        if (isWorker) {
            self.onmessage = function (e) {
                var id = e.data.id;

                try {
                    var archive = new UntarInternal(e.data.arrayBuffer);
                    var files = archive.untar(e.data.prefix);
                    // The worker is done so send a message to the main thread.
                    // Notice we are sending the array buffer back as a Transferrable object
                    // so that the main thread can re-assume control of the array buffer.
                    postMessage({
                        id: id,
                        files: files,
                        arrayBuffer: e.data.arrayBuffer
                    }, [e.data.arrayBuffer]);
                } catch (err) {
                    postMessage({
                        id: id,
                        error: err.toString()
                    });
                }
            };
        }
    }

    // Convert the UntarScope function to a string and add
    // the onmessage handler for the worker to untar archives
    var scopeToUrl = function () {
        // execute UntarScope function in the worker
        var code = '(' + UntarScope.toString() + ')(true)\n\n';

        // create blob URL for the code above to be used for the worker
        var blob = new Blob([code], { type: 'application/javascript' });
        return URL.createObjectURL(blob);
    };

    // this is the URL that is going to be used for workers
    var WORKER_URL = scopeToUrl();

    /**
    * @private
    * @name pc.UntarWorker
    * @classdesc Wraps untar'ing a tar archive with a Web Worker.
    * @description Creates new instance of a pc.UntarWorker.
    * @param {String} [filenamePrefix] The prefix that should be added to each file name in the archive. This is usually the {@link pc.AssetRegistry} prefix.
    */
    var UntarWorker = function (filenamePrefix) {
        this._requestId = 0;
        this._pendingRequests = {};
        this._filenamePrefix = filenamePrefix;
        this._worker = new Worker(WORKER_URL);
        this._worker.addEventListener('message', this._onMessage.bind(this));
    };

    UntarWorker.prototype._onMessage = function (e) {
        var id = e.data.id;
        if (! this._pendingRequests[id]) return;

        var callback = this._pendingRequests[id];

        delete this._pendingRequests[id];

        if (e.data.error) {
            callback(e.data.error);
        } else {
            var arrayBuffer = e.data.arrayBuffer;

            // create blob URLs for each file. We are creating the URLs
            // here - outside of the worker - so that the main thread owns them
            for (var i = 0, len = e.data.files.length; i < len; i++) {
                var file = e.data.files[i];
                var blob = new Blob([arrayBuffer.slice(file.start, file.start + file.size)]);
                file.url = URL.createObjectURL(blob);
            }

            callback(null, e.data.files);
        }
    };

    /**
     * @private
     * @function
     * @name pc.UntarWorker#untar
     * @description Untars the specified array buffer using a Web Worker and returns the result in the callback.
     * @param {ArrayBuffer} arrayBuffer The array buffer that holds the tar archive.
     * @param {Function} callback The callback function called when the worker is finished or if there is an error. The
     * callback has the following arguments: {error, files}, where error is a string if any, and files is an array of file descriptors
     */
    UntarWorker.prototype.untar = function (arrayBuffer, callback) {
        var id = this._requestId++;
        this._pendingRequests[id] = callback;

        // send data to the worker - notice the last argument
        // converts the arrayBuffer to a Transferrable object
        // to avoid copying the array buffer which would cause a stall.
        // However this causes the worker to assume control of the array
        // buffer so we cannot access this buffer until the worker is done with it.
        this._worker.postMessage({
            id: id,
            prefix: this._filenamePrefix,
            arrayBuffer: arrayBuffer
        }, [arrayBuffer]);
    };

    /**
     * @private
     * @function
     * @name pc.UntarWorker#hasPendingRequests
     * @description Returns whether the worker has pending requests to untar array buffers
     * @returns {Boolean} Returns true of false
     */
    UntarWorker.prototype.hasPendingRequests = function () {
        for (var key in this._pendingRequests) {
            return true;
        }

        return false;
    };

    /**
     * @private
     * @function
     * @name pc.UntarWorker#destroy
     * @description Destroys the internal Web Worker
     */
    UntarWorker.prototype.destroy = function () {
        if (this._worker) {
            this._worker.terminate();
            this._worker = null;

            this._pendingRequests = null;
        }
    };

    // execute the UntarScope function in order to declare the Untar constructor
    UntarScope();

    // expose variables to the pc namespace
    return {
        Untar: Untar,
        UntarWorker: UntarWorker
    };
}());