blob: 787770a4800fc47607df254b53959128edac28e2 [file] [log] [blame]
// Copyright 2014 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* A class that takes care of communication between NaCl and archive volume.
* Its job is to handle communication with the naclModule.
* @constructor
* @param {!Object} naclModule The nacl module with which the decompressor
* communicates.
* @param {!unpacker.types.FileSystemId} fileSystemId The file system id of for
* the archive volume to decompress.
* @param {!Blob} blob The correspondent file blob for fileSystemId.
* @param {!unpacker.PassphraseManager} passphraseManager Passphrase manager.
*/
unpacker.Decompressor = function(naclModule, fileSystemId, blob,
passphraseManager) {
/**
* @private {!Object}
* @const
*/
this.naclModule_ = naclModule;
/**
* @private {!unpacker.types.FileSystemId}
* @const
*/
this.fileSystemId_ = fileSystemId;
/**
* @private {!Blob}
* @const
*/
this.blob_ = blob;
/**
* @public {!unpacker.PassphraseManager}
* @const
*/
this.passphraseManager = passphraseManager;
/**
* Requests in progress. No need to save them onSuspend for now as metadata
* reads are restarted from start.
* @public {!Object<!unpacker.types.RequestId, !Object>}
* @const
*/
this.requestsInProgress = {};
};
/**
* @return {boolean} True if there is any request in progress.
*/
unpacker.Decompressor.prototype.hasRequestsInProgress = function() {
return Object.keys(this.requestsInProgress).length > 0;
};
/**
* Sends a request to NaCl and mark it as a request in progress. onSuccess and
* onError are the callbacks used when receiving an answer from NaCl.
* @param {!unpacker.types.RequestId} requestId The operation request id, which
* should be unique per every volume.
* @param {function(...)} onSuccess Callback to execute on success.
* @param {function(!ProviderError)} onError Callback to execute on error.
* @param {!Object} naclRequest A request that must be sent to NaCl using
* postMessage.
* @private
*/
unpacker.Decompressor.prototype.addRequest_ = function(requestId, onSuccess,
onError, naclRequest) {
console.assert(!this.requestsInProgress[requestId],
'There is already a request with the id ' + requestId + '.');
this.requestsInProgress[requestId] = {
onSuccess: onSuccess,
onError: onError
};
this.naclModule_.postMessage(naclRequest);
};
/**
* Creates a request for reading metadata.
* @param {!unpacker.types.RequestId} requestId
* @param {string} encoding Default encoding for the archive's headers.
* @param {function(!Object<string, !Object>)} onSuccess Callback to execute
* once the metadata is obtained from NaCl. It has one parameter, which is
* the metadata itself. The metadata has as key the full path to an entry
* and as value information about the entry.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Decompressor.prototype.readMetadata = function(requestId, encoding,
onSuccess, onError) {
this.addRequest_(
requestId, onSuccess, onError,
unpacker.request.createReadMetadataRequest(this.fileSystemId_, requestId,
encoding, this.blob_.size));
};
/**
* Sends an open file request to NaCl.
* @param {!unpacker.types.RequestId} requestId
* @param {number} index Index of the file in the header list.
* @param {string} encoding Default encoding for the archive's headers.
* @param {function()} onSuccess Callback to execute on successful open.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Decompressor.prototype.openFile = function(requestId, index, encoding,
onSuccess, onError) {
this.addRequest_(
requestId, onSuccess, onError,
unpacker.request.createOpenFileRequest(this.fileSystemId_, requestId,
index, encoding, this.blob_.size));
};
/**
* Sends a close file request to NaCl.
* @param {!unpacker.types.RequestId} requestId
* @param {!unpacker.types.RequestId} openRequestId The request id of the
* corresponding open file operation for the file to close.
* @param {function()} onSuccess Callback to execute on successful open.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Decompressor.prototype.closeFile = function(requestId, openRequestId,
onSuccess, onError) {
this.addRequest_(requestId, onSuccess, onError,
unpacker.request.createCloseFileRequest(
this.fileSystemId_, requestId, openRequestId));
};
/**
* Sends a read file request to NaCl.
* @param {!unpacker.types.RequestId} requestId
* @param {!unpacker.types.RequestId} openRequestId The request id of the
* corresponding open file operation for the file to read.
* @param {number} offset The offset from where read operation should start.
* @param {number} length The number of bytes to read.
* @param {function(!ArrayBuffer, boolean)} onSuccess Callback to execute on
* success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Decompressor.prototype.readFile = function(
requestId, openRequestId, offset, length, onSuccess, onError) {
this.addRequest_(
requestId, onSuccess, onError,
unpacker.request.createReadFileRequest(this.fileSystemId_, requestId,
openRequestId, offset, length));
};
/**
* Processes messages from NaCl module.
* @param {!Object} data The data contained in the message from NaCl. Its
* types depend on the operation of the request.
* @param {!unpacker.request.Operation} operation An operation from request.js.
* @param {number} requestId The request id, which should be unique per every
* volume.
*/
unpacker.Decompressor.prototype.processMessage = function(data, operation,
requestId) {
// Create a request reference for asynchronous calls as sometimes we delete
// some requestsInProgress from this.requestsInProgress.
var requestInProgress = this.requestsInProgress[requestId];
console.assert(requestInProgress, 'No request with id <' + requestId +
'> for: ' + this.fileSystemId_ + '.');
switch (operation) {
case unpacker.request.Operation.READ_METADATA_DONE:
var metadata = data[unpacker.request.Key.METADATA];
console.assert(metadata, 'No metadata.');
requestInProgress.onSuccess(metadata);
break;
case unpacker.request.Operation.READ_CHUNK:
this.readChunk_(data, requestId);
// this.requestsInProgress_[requestId] should be valid as long as NaCL
// can still make READ_CHUNK requests.
return;
case unpacker.request.Operation.READ_PASSPHRASE:
this.readPassphrase_(data, requestId);
// this.requestsInProgress_[requestId] should be valid as long as NaCL
// can still make READ_PASSPHRASE requests.
return;
case unpacker.request.Operation.OPEN_FILE_DONE:
requestInProgress.onSuccess();
// this.requestsInProgress_[requestId] should be valid until closing the
// file so NaCL can make READ_CHUNK requests.
return;
case unpacker.request.Operation.CLOSE_FILE_DONE:
var openRequestId = data[unpacker.request.Key.OPEN_REQUEST_ID];
console.assert(openRequestId, 'No open request id.');
openRequestId = Number(openRequestId); // Received as string.
delete this.requestsInProgress[openRequestId];
requestInProgress.onSuccess();
break;
case unpacker.request.Operation.READ_FILE_DONE:
var buffer = data[unpacker.request.Key.READ_FILE_DATA];
console.assert(buffer, 'No buffer for read file operation.');
var hasMoreData = data[unpacker.request.Key.HAS_MORE_DATA];
console.assert(buffer !== undefined,
'No HAS_MORE_DATA boolean value for file operation.');
requestInProgress.onSuccess(buffer, hasMoreData /* Last call. */);
if (hasMoreData)
return; // Do not delete requestInProgress.
break;
case unpacker.request.Operation.FILE_SYSTEM_ERROR:
console.error('File system error for <' + this.fileSystemId_ + '>: ' +
data[unpacker.request.Key.ERROR]); // The error contains
// the '.' at the end.
requestInProgress.onError('FAILED');
break;
case unpacker.request.Operation.CONSOLE_LOG:
case unpacker.request.Operation.CONSOLE_DEBUG:
var src_file = data[unpacker.request.Key.SRC_FILE];
var src_line = data[unpacker.request.Key.SRC_LINE];
var src_func = data[unpacker.request.Key.SRC_FUNC];
var msg = data[unpacker.request.Key.MESSAGE];
var log = operation == unpacker.request.Operation.CONSOLE_LOG ?
console.log : console.debug;
log(src_file + ':' + src_func + ':' + src_line + ': ' + msg);
break;
default:
console.error('Invalid NaCl operation: ' + operation + '.');
requestInProgress.onError('FAILED');
}
delete this.requestsInProgress[requestId];
};
/**
* Reads a chunk of data from this.blob_ for READ_CHUNK operation.
* @param {!Object} data The data received from the NaCl module.
* @param {number} requestId The request id, which should be unique per every
* volume.
* @private
*/
unpacker.Decompressor.prototype.readChunk_ = function(data, requestId) {
// Offset and length are received as strings. See request.js.
var offset_str = data[unpacker.request.Key.OFFSET];
var length_str = data[unpacker.request.Key.LENGTH];
// Explicit check if offset is undefined as it can be 0.
console.assert(offset_str !== undefined && !isNaN(offset_str) &&
Number(offset_str) >= 0 &&
Number(offset_str) < this.blob_.size,
'Invalid offset.');
console.assert(length_str && !isNaN(length_str) && Number(length_str) > 0,
'Invalid length.');
var offset = Number(offset_str);
var length = Math.min(this.blob_.size - offset, Number(length_str));
// Read a chunk from offset to offset + length.
var blob = this.blob_.slice(offset, offset + length);
var fileReader = new FileReader();
fileReader.onload = function(event) {
this.naclModule_.postMessage(unpacker.request.createReadChunkDoneResponse(
this.fileSystemId_, requestId, event.target.result, offset));
}.bind(this);
fileReader.onerror = function(event) {
console.error('Failed to read a chunk of data from the archive.');
this.naclModule_.postMessage(unpacker.request.createReadChunkErrorResponse(
this.fileSystemId_, requestId));
// Reading from the source file failed. Assume that the file is gone and
// unmount the archive.
// TODO(523195): Show a notification that the source file is gone.
unpacker.app.unmountVolume(this.fileSystemId_, true);
}.bind(this);
fileReader.readAsArrayBuffer(blob);
};
/**
* Reads a passphrase from user input for READ_PASSPHRASE operation.
* @param {!Object} data The data received from the NaCl module.
* @param {number} requestId The request id, which should be unique per every
* volume.
* @private
*/
unpacker.Decompressor.prototype.readPassphrase_ = function(data, requestId) {
this.passphraseManager.getPassphrase()
.then(function(passphrase) {
this.naclModule_.postMessage(
unpacker.request.createReadPassphraseDoneResponse(
this.fileSystemId_, requestId, passphrase));
}.bind(this))
.catch(function(error) {
console.error(error.stack || error);
this.naclModule_.postMessage(
unpacker.request.createReadPassphraseErrorResponse(
this.fileSystemId_, requestId));
// TODO(mtomasz): Instead of unmounting just let the current operation
// fail and ask for password for another files. This is however
// impossible for now due to a bug in libarchive.
unpacker.app.unmountVolume(this.fileSystemId_, true);
}.bind(this));
};