blob: 64fae35b99c899c0f811796be69d745fee9bc7bd [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';
/**
* Converts a c/c++ time_t variable to Date.
* @param {number} timestamp A c/c++ time_t variable.
* @return {!Date}
*/
function DateFromTimeT(timestamp) {
return new Date(1000 * timestamp);
}
/**
* Corrects metadata entries fields in order for them to be sent to Files.app.
* This function runs recursively for every entry in a directory.
* @param {!Object<string, !EntryMetadata>} entryMetadata The metadata to
* correct.
*/
function correctMetadata(entryMetadata) {
entryMetadata.index = parseInt(entryMetadata.index, 10);
entryMetadata.size = parseInt(entryMetadata.size, 10);
entryMetadata.modificationTime =
DateFromTimeT(entryMetadata.modificationTime);
if (entryMetadata.isDirectory) {
console.assert(entryMetadata.entries,
'The field "entries" is mandatory for dictionaries.');
for (var entry in entryMetadata.entries) {
correctMetadata(entryMetadata.entries[entry]);
}
}
}
/**
* Defines a volume object that contains information about archives' contents
* and performs operations on these contents.
* @constructor
* @param {!unpacker.Decompressor} decompressor The decompressor used to obtain
* data from archives.
* @param {!Entry} entry The entry corresponding to the volume's archive.
*/
unpacker.Volume = function(decompressor, entry) {
/**
* Used for restoring the opened file entry after resuming the event page.
* @type {!Entry}
*/
this.entry = entry;
/**
* @type {!unpacker.Decompressor}
*/
this.decompressor = decompressor;
/**
* The volume's metadata. The key is the full path to the file on this volume.
* For more details see
* https://developer.chrome.com/apps/fileSystemProvider#type-EntryMetadata
* @type {?Object<string, !EntryMetadata>}
*/
this.metadata = null;
/**
* A map with currently opened files. The key is a requestId value from the
* openFileRequested event and the value is the open file options.
* @type {!Object<!unpacker.types.RequestId,
* !unpacker.types.OpenFileRequestedOptions>}
*/
this.openedFiles = {};
/**
* Default encoding set for this archive. If empty, then not known.
* @type {string}
*/
this.encoding =
unpacker.Volume.ENCODING_TABLE[chrome.i18n.getUILanguage()] || '';
/**
* The default read metadata request id. -1 is ok as the request ids used by
* flleSystemProvider are greater than 0.
* @type {number}
*/
this.DEFAULT_READ_METADATA_REQUEST_ID = -1;
};
/**
* The default read metadata request id. -1 is ok as the request ids used by
* flleSystemProvider are greater than 0.
* @const {number}
*/
unpacker.Volume.DEFAULT_READ_METADATA_REQUEST_ID = -1;
/**
* Map from language codes to default charset encodings.
* @const {!Object<string, string>}
*/
unpacker.Volume.ENCODING_TABLE = {
ar: 'CP1256',
bg: 'CP1251',
ca: 'CP1252',
cs: 'CP1250',
da: 'CP1252',
de: 'CP1252',
el: 'CP1253',
en: 'CP1250',
en_GB: 'CP1250',
es: 'CP1252',
es_419: 'CP1252',
et: 'CP1257',
fa: 'CP1256',
fi: 'CP1252',
fil: 'CP1252',
fr: 'CP1252',
he: 'CP1255',
hi: 'UTF-8', // Another one may be better.
hr: 'CP1250',
hu: 'CP1250',
id: 'CP1252',
it: 'CP1252',
ja: 'CP932', // Alternatively SHIFT-JIS.
ko: 'CP949', // Alternatively EUC-KR.
lt: 'CP1257',
lv: 'CP1257',
ms: 'CP1252',
nl: 'CP1252',
no: 'CP1252',
pl: 'CP1250',
pt_BR: 'CP1252',
pt_PT: 'CP1252',
ro: 'CP1250',
ru: 'CP1251',
sk: 'CP1250',
sl: 'CP1250',
sr: 'CP1251',
sv: 'CP1252',
th: 'CP874', // Confirm!
tr: 'CP1254',
uk: 'CP1251',
vi: 'CP1258',
zh_CN: 'CP936',
zh_TW: 'CP950'
};
/**
* @return {boolean} True if volume is ready to be used.
*/
unpacker.Volume.prototype.isReady = function() {
return !!this.metadata;
};
/**
* @return {boolean} True if volume is in use.
*/
unpacker.Volume.prototype.inUse = function() {
return this.decompressor.hasRequestsInProgress() ||
Object.keys(this.openedFiles).length > 0;
};
/**
* Initializes the volume by reading its metadata.
* @param {function()} onSuccess Callback to execute on success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Volume.prototype.initialize = function(onSuccess, onError) {
var requestId = unpacker.Volume.DEFAULT_READ_METADATA_REQUEST_ID;
this.decompressor.readMetadata(requestId, this.encoding, function(metadata) {
// Make a deep copy of metadata.
this.metadata = /** @type {!Object<string, !EntryMetadata>} */ (JSON.parse(
JSON.stringify(metadata)));
correctMetadata(this.metadata);
onSuccess();
}.bind(this), onError);
};
/**
* Obtains the metadata for a single entry in the archive. Assumes metadata is
* loaded.
* @param {!unpacker.types.GetMetadataRequestedOptions} options Options for
* getting the metadata of an entry.
* @param {function(!EntryMetadata)} onSuccess Callback to execute on success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Volume.prototype.onGetMetadataRequested = function(options, onSuccess,
onError) {
console.assert(this.isReady(), 'Metadata must be loaded.');
var entryMetadata = this.getEntryMetadata_(options.entryPath);
if (!entryMetadata)
onError('NOT_FOUND');
else
onSuccess(entryMetadata);
};
/**
* Reads a directory contents from metadata. Assumes metadata is loaded.
* @param {!unpacker.types.ReadDirectoryRequestedOptions} options Options
* for reading the contents of a directory.
* @param {function(!Array<!EntryMetadata>, boolean)} onSuccess Callback to
* execute on success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Volume.prototype.onReadDirectoryRequested = function(
options, onSuccess, onError) {
console.assert(this.isReady(), 'Metadata must be loaded.');
var directoryMetadata = this.getEntryMetadata_(options.directoryPath);
if (!directoryMetadata) {
onError('NOT_FOUND');
return;
}
if (!directoryMetadata.isDirectory) {
onError('NOT_A_DIRECTORY');
return;
}
// Convert dictionary entries to an array.
var entries = [];
for (var entry in directoryMetadata.entries) {
entries.push(directoryMetadata.entries[entry]);
}
onSuccess(entries, false /* Last call. */);
};
/**
* Opens a file for read or write.
* @param {!unpacker.types.OpenFileRequestedOptions} options Options for
* opening a file.
* @param {function()} onSuccess Callback to execute on success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Volume.prototype.onOpenFileRequested = function(options, onSuccess,
onError) {
console.assert(this.isReady(), 'Metadata must be loaded.');
if (options.mode != 'READ') {
onError('INVALID_OPERATION');
return;
}
var metadata = this.getEntryMetadata_(options.filePath);
if (!metadata) {
onError('NOT_FOUND');
return;
}
this.openedFiles[options.requestId] = options;
this.decompressor.openFile(
options.requestId, metadata.index, this.encoding, function() {
onSuccess();
}.bind(this), function(error) {
delete this.openedFiles[options.requestId];
onError('FAILED');
}.bind(this));
};
/**
* Closes a file identified by options.openRequestId.
* @param {!unpacker.types.CloseFileRequestedOptions} options Options for
* closing a file.
* @param {function()} onSuccess Callback to execute on success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Volume.prototype.onCloseFileRequested = function(options, onSuccess,
onError) {
console.assert(this.isReady(), 'Metadata must be loaded.');
var openRequestId = options.openRequestId;
var openOptions = this.openedFiles[openRequestId];
if (!openOptions) {
onError('INVALID_OPERATION');
return;
}
this.decompressor.closeFile(options.requestId, openRequestId, function() {
delete this.openedFiles[openRequestId];
onSuccess();
}.bind(this), onError);
};
/**
* Reads the contents of a file identified by options.openRequestId.
* @param {!unpacker.types.ReadFileRequestedOptions} options Options for
* reading a file's contents.
* @param {function(!ArrayBuffer, boolean)} onSuccess Callback to execute on
* success.
* @param {function(!ProviderError)} onError Callback to execute on error.
*/
unpacker.Volume.prototype.onReadFileRequested = function(options, onSuccess,
onError) {
console.assert(this.isReady(), 'Metadata must be loaded.');
var openOptions = this.openedFiles[options.openRequestId];
if (!openOptions) {
onError('INVALID_OPERATION');
return;
}
var offset = options.offset;
var length = options.length;
// Offset and length should be validated by the API.
console.assert(offset >= 0, 'Offset should be >= 0.');
console.assert(length >= 0, 'Length should be >= 0.');
var fileSize = this.getEntryMetadata_(openOptions.filePath).size;
if (offset >= fileSize || length == 0) { // No more data.
onSuccess(new ArrayBuffer(0), false /* Last call. */);
return;
}
length = Math.min(length, fileSize - offset);
this.decompressor.readFile(options.requestId, options.openRequestId,
offset, length, onSuccess, onError);
};
/**
* Gets the metadata for an entry based on its path.
* @param {string} entryPath The full path to the entry.
* @return {?Object} The correspondent metadata.
* @private
*/
unpacker.Volume.prototype.getEntryMetadata_ = function(entryPath) {
var pathArray = entryPath.split('/');
// Remove empty strings resulted after split. As paths start with '/' we will
// have an empty string at the beginning of pathArray and possible an
// empty string at the end for directories (e.g. /path/to/dir/). The code
// assumes entryPath cannot have consecutive '/'.
pathArray.splice(0, 1);
if (pathArray.length > 0) { // In case of 0 this is root directory.
var lastIndex = pathArray.length - 1;
if (pathArray[lastIndex] == '')
pathArray.splice(lastIndex);
}
// Get the actual metadata by iterating through every directory metadata
// on the path to the entry.
var entryMetadata = this.metadata;
for (var i = 0, limit = pathArray.length; i < limit; i++) {
if (!entryMetadata ||
!entryMetadata.isDirectory && i != limit - 1 /* Parent directory. */)
return null;
entryMetadata = entryMetadata.entries[pathArray[i]];
}
return entryMetadata;
};