| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 The Chromium OS Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * Converts a c/c++ time_t variable to Date. | |
| 9 * @param {number} timestamp A c/c++ time_t variable. | |
| 10 * @return {!Date} | |
| 11 */ | |
| 12 function DateFromTimeT(timestamp) { | |
| 13 return new Date(1000 * timestamp); | |
| 14 } | |
| 15 | |
| 16 /** | |
| 17 * Corrects metadata entries fields in order for them to be sent to Files.app. | |
| 18 * This function runs recursively for every entry in a directory. | |
| 19 * @param {!Object<string, !EntryMetadata>} entryMetadata The metadata to | |
| 20 * correct. | |
| 21 */ | |
| 22 function correctMetadata(entryMetadata) { | |
| 23 entryMetadata.index = parseInt(entryMetadata.index, 10); | |
| 24 entryMetadata.size = parseInt(entryMetadata.size, 10); | |
| 25 entryMetadata.modificationTime = | |
| 26 DateFromTimeT(entryMetadata.modificationTime); | |
| 27 if (entryMetadata.isDirectory) { | |
| 28 console.assert(entryMetadata.entries, | |
| 29 'The field "entries" is mandatory for dictionaries.'); | |
| 30 for (var entry in entryMetadata.entries) { | |
| 31 correctMetadata(entryMetadata.entries[entry]); | |
| 32 } | |
| 33 } | |
| 34 } | |
| 35 | |
| 36 /** | |
| 37 * Defines a volume object that contains information about archives' contents | |
| 38 * and performs operations on these contents. | |
| 39 * @constructor | |
| 40 * @param {!unpacker.Decompressor} decompressor The decompressor used to obtain | |
| 41 * data from archives. | |
| 42 * @param {!Entry} entry The entry corresponding to the volume's archive. | |
| 43 */ | |
| 44 unpacker.Volume = function(decompressor, entry) { | |
| 45 /** | |
| 46 * Used for restoring the opened file entry after resuming the event page. | |
| 47 * @type {!Entry} | |
| 48 */ | |
| 49 this.entry = entry; | |
| 50 | |
| 51 /** | |
| 52 * @type {!unpacker.Decompressor} | |
| 53 */ | |
| 54 this.decompressor = decompressor; | |
| 55 | |
| 56 /** | |
| 57 * The volume's metadata. The key is the full path to the file on this volume. | |
| 58 * For more details see | |
| 59 * https://developer.chrome.com/apps/fileSystemProvider#type-EntryMetadata | |
| 60 * @type {?Object<string, !EntryMetadata>} | |
| 61 */ | |
| 62 this.metadata = null; | |
| 63 | |
| 64 /** | |
| 65 * A map with currently opened files. The key is a requestId value from the | |
| 66 * openFileRequested event and the value is the open file options. | |
| 67 * @type {!Object<!unpacker.types.RequestId, | |
| 68 * !unpacker.types.OpenFileRequestedOptions>} | |
| 69 */ | |
| 70 this.openedFiles = {}; | |
| 71 | |
| 72 /** | |
| 73 * Default encoding set for this archive. If empty, then not known. | |
| 74 * @type {string} | |
| 75 */ | |
| 76 this.encoding = | |
| 77 unpacker.Volume.ENCODING_TABLE[chrome.i18n.getUILanguage()] || ''; | |
| 78 | |
| 79 /** | |
| 80 * The default read metadata request id. -1 is ok as the request ids used by | |
| 81 * flleSystemProvider are greater than 0. | |
| 82 * @type {number} | |
| 83 */ | |
| 84 this.DEFAULT_READ_METADATA_REQUEST_ID = -1; | |
| 85 }; | |
| 86 | |
| 87 /** | |
| 88 * The default read metadata request id. -1 is ok as the request ids used by | |
| 89 * flleSystemProvider are greater than 0. | |
| 90 * @const {number} | |
| 91 */ | |
| 92 unpacker.Volume.DEFAULT_READ_METADATA_REQUEST_ID = -1; | |
| 93 | |
| 94 /** | |
| 95 * Map from language codes to default charset encodings. | |
| 96 * @const {!Object<string, string>} | |
| 97 */ | |
| 98 unpacker.Volume.ENCODING_TABLE = { | |
| 99 ar: 'CP1256', | |
| 100 bg: 'CP1251', | |
| 101 ca: 'CP1252', | |
| 102 cs: 'CP1250', | |
| 103 da: 'CP1252', | |
| 104 de: 'CP1252', | |
| 105 el: 'CP1253', | |
| 106 en: 'CP1250', | |
| 107 en_GB: 'CP1250', | |
| 108 es: 'CP1252', | |
| 109 es_419: 'CP1252', | |
| 110 et: 'CP1257', | |
| 111 fa: 'CP1256', | |
| 112 fi: 'CP1252', | |
| 113 fil: 'CP1252', | |
| 114 fr: 'CP1252', | |
| 115 he: 'CP1255', | |
| 116 hi: 'UTF-8', // Another one may be better. | |
| 117 hr: 'CP1250', | |
| 118 hu: 'CP1250', | |
| 119 id: 'CP1252', | |
| 120 it: 'CP1252', | |
| 121 ja: 'CP932', // Alternatively SHIFT-JIS. | |
| 122 ko: 'CP949', // Alternatively EUC-KR. | |
| 123 lt: 'CP1257', | |
| 124 lv: 'CP1257', | |
| 125 ms: 'CP1252', | |
| 126 nl: 'CP1252', | |
| 127 no: 'CP1252', | |
| 128 pl: 'CP1250', | |
| 129 pt_BR: 'CP1252', | |
| 130 pt_PT: 'CP1252', | |
| 131 ro: 'CP1250', | |
| 132 ru: 'CP1251', | |
| 133 sk: 'CP1250', | |
| 134 sl: 'CP1250', | |
| 135 sr: 'CP1251', | |
| 136 sv: 'CP1252', | |
| 137 th: 'CP874', // Confirm! | |
| 138 tr: 'CP1254', | |
| 139 uk: 'CP1251', | |
| 140 vi: 'CP1258', | |
| 141 zh_CN: 'CP936', | |
| 142 zh_TW: 'CP950' | |
| 143 }; | |
| 144 | |
| 145 /** | |
| 146 * @return {boolean} True if volume is ready to be used. | |
| 147 */ | |
| 148 unpacker.Volume.prototype.isReady = function() { | |
| 149 return !!this.metadata; | |
| 150 }; | |
| 151 | |
| 152 /** | |
| 153 * @return {boolean} True if volume is in use. | |
| 154 */ | |
| 155 unpacker.Volume.prototype.inUse = function() { | |
| 156 return this.decompressor.hasRequestsInProgress() || | |
| 157 Object.keys(this.openedFiles).length > 0; | |
| 158 }; | |
| 159 | |
| 160 /** | |
| 161 * Initializes the volume by reading its metadata. | |
| 162 * @param {function()} onSuccess Callback to execute on success. | |
| 163 * @param {function(!ProviderError)} onError Callback to execute on error. | |
| 164 */ | |
| 165 unpacker.Volume.prototype.initialize = function(onSuccess, onError) { | |
| 166 var requestId = unpacker.Volume.DEFAULT_READ_METADATA_REQUEST_ID; | |
| 167 this.decompressor.readMetadata(requestId, this.encoding, function(metadata) { | |
| 168 // Make a deep copy of metadata. | |
| 169 this.metadata = /** @type {!Object<string, !EntryMetadata>} */ (JSON.parse( | |
| 170 JSON.stringify(metadata))); | |
| 171 correctMetadata(this.metadata); | |
| 172 | |
| 173 onSuccess(); | |
| 174 }.bind(this), onError); | |
| 175 }; | |
| 176 | |
| 177 /** | |
| 178 * Obtains the metadata for a single entry in the archive. Assumes metadata is | |
| 179 * loaded. | |
| 180 * @param {!unpacker.types.GetMetadataRequestedOptions} options Options for | |
| 181 * getting the metadata of an entry. | |
| 182 * @param {function(!EntryMetadata)} onSuccess Callback to execute on success. | |
| 183 * @param {function(!ProviderError)} onError Callback to execute on error. | |
| 184 */ | |
| 185 unpacker.Volume.prototype.onGetMetadataRequested = function(options, onSuccess, | |
| 186 onError) { | |
| 187 console.assert(this.isReady(), 'Metadata must be loaded.'); | |
| 188 var entryMetadata = this.getEntryMetadata_(options.entryPath); | |
| 189 if (!entryMetadata) | |
| 190 onError('NOT_FOUND'); | |
| 191 else | |
| 192 onSuccess(entryMetadata); | |
| 193 }; | |
| 194 | |
| 195 /** | |
| 196 * Reads a directory contents from metadata. Assumes metadata is loaded. | |
| 197 * @param {!unpacker.types.ReadDirectoryRequestedOptions} options Options | |
| 198 * for reading the contents of a directory. | |
| 199 * @param {function(!Array<!EntryMetadata>, boolean)} onSuccess Callback to | |
| 200 * execute on success. | |
| 201 * @param {function(!ProviderError)} onError Callback to execute on error. | |
| 202 */ | |
| 203 unpacker.Volume.prototype.onReadDirectoryRequested = function( | |
| 204 options, onSuccess, onError) { | |
| 205 console.assert(this.isReady(), 'Metadata must be loaded.'); | |
| 206 var directoryMetadata = this.getEntryMetadata_(options.directoryPath); | |
| 207 if (!directoryMetadata) { | |
| 208 onError('NOT_FOUND'); | |
| 209 return; | |
| 210 } | |
| 211 if (!directoryMetadata.isDirectory) { | |
| 212 onError('NOT_A_DIRECTORY'); | |
| 213 return; | |
| 214 } | |
| 215 | |
| 216 // Convert dictionary entries to an array. | |
| 217 var entries = []; | |
| 218 for (var entry in directoryMetadata.entries) { | |
| 219 entries.push(directoryMetadata.entries[entry]); | |
| 220 } | |
| 221 | |
| 222 onSuccess(entries, false /* Last call. */); | |
| 223 }; | |
| 224 | |
| 225 /** | |
| 226 * Opens a file for read or write. | |
| 227 * @param {!unpacker.types.OpenFileRequestedOptions} options Options for | |
| 228 * opening a file. | |
| 229 * @param {function()} onSuccess Callback to execute on success. | |
| 230 * @param {function(!ProviderError)} onError Callback to execute on error. | |
| 231 */ | |
| 232 unpacker.Volume.prototype.onOpenFileRequested = function(options, onSuccess, | |
| 233 onError) { | |
| 234 console.assert(this.isReady(), 'Metadata must be loaded.'); | |
| 235 if (options.mode != 'READ') { | |
| 236 onError('INVALID_OPERATION'); | |
| 237 return; | |
| 238 } | |
| 239 | |
| 240 var metadata = this.getEntryMetadata_(options.filePath); | |
| 241 if (!metadata) { | |
| 242 onError('NOT_FOUND'); | |
| 243 return; | |
| 244 } | |
| 245 | |
| 246 this.openedFiles[options.requestId] = options; | |
| 247 | |
| 248 this.decompressor.openFile( | |
| 249 options.requestId, metadata.index, this.encoding, function() { | |
| 250 onSuccess(); | |
| 251 }.bind(this), function(error) { | |
| 252 delete this.openedFiles[options.requestId]; | |
| 253 onError('FAILED'); | |
| 254 }.bind(this)); | |
| 255 }; | |
| 256 | |
| 257 /** | |
| 258 * Closes a file identified by options.openRequestId. | |
| 259 * @param {!unpacker.types.CloseFileRequestedOptions} options Options for | |
| 260 * closing a file. | |
| 261 * @param {function()} onSuccess Callback to execute on success. | |
| 262 * @param {function(!ProviderError)} onError Callback to execute on error. | |
| 263 */ | |
| 264 unpacker.Volume.prototype.onCloseFileRequested = function(options, onSuccess, | |
| 265 onError) { | |
| 266 console.assert(this.isReady(), 'Metadata must be loaded.'); | |
| 267 var openRequestId = options.openRequestId; | |
| 268 var openOptions = this.openedFiles[openRequestId]; | |
| 269 if (!openOptions) { | |
| 270 onError('INVALID_OPERATION'); | |
| 271 return; | |
| 272 } | |
| 273 | |
| 274 this.decompressor.closeFile(options.requestId, openRequestId, function() { | |
| 275 delete this.openedFiles[openRequestId]; | |
| 276 onSuccess(); | |
| 277 }.bind(this), onError); | |
| 278 }; | |
| 279 | |
| 280 /** | |
| 281 * Reads the contents of a file identified by options.openRequestId. | |
| 282 * @param {!unpacker.types.ReadFileRequestedOptions} options Options for | |
| 283 * reading a file's contents. | |
| 284 * @param {function(!ArrayBuffer, boolean)} onSuccess Callback to execute on | |
| 285 * success. | |
| 286 * @param {function(!ProviderError)} onError Callback to execute on error. | |
| 287 */ | |
| 288 unpacker.Volume.prototype.onReadFileRequested = function(options, onSuccess, | |
| 289 onError) { | |
| 290 console.assert(this.isReady(), 'Metadata must be loaded.'); | |
| 291 var openOptions = this.openedFiles[options.openRequestId]; | |
| 292 if (!openOptions) { | |
| 293 onError('INVALID_OPERATION'); | |
| 294 return; | |
| 295 } | |
| 296 | |
| 297 var offset = options.offset; | |
| 298 var length = options.length; | |
| 299 // Offset and length should be validated by the API. | |
| 300 console.assert(offset >= 0, 'Offset should be >= 0.'); | |
| 301 console.assert(length >= 0, 'Length should be >= 0.'); | |
| 302 | |
| 303 var fileSize = this.getEntryMetadata_(openOptions.filePath).size; | |
| 304 if (offset >= fileSize || length == 0) { // No more data. | |
| 305 onSuccess(new ArrayBuffer(0), false /* Last call. */); | |
| 306 return; | |
| 307 } | |
| 308 length = Math.min(length, fileSize - offset); | |
| 309 | |
| 310 this.decompressor.readFile(options.requestId, options.openRequestId, | |
| 311 offset, length, onSuccess, onError); | |
| 312 }; | |
| 313 | |
| 314 /** | |
| 315 * Gets the metadata for an entry based on its path. | |
| 316 * @param {string} entryPath The full path to the entry. | |
| 317 * @return {?Object} The correspondent metadata. | |
| 318 * @private | |
| 319 */ | |
| 320 unpacker.Volume.prototype.getEntryMetadata_ = function(entryPath) { | |
| 321 var pathArray = entryPath.split('/'); | |
| 322 | |
| 323 // Remove empty strings resulted after split. As paths start with '/' we will | |
| 324 // have an empty string at the beginning of pathArray and possible an | |
| 325 // empty string at the end for directories (e.g. /path/to/dir/). The code | |
| 326 // assumes entryPath cannot have consecutive '/'. | |
| 327 pathArray.splice(0, 1); | |
| 328 | |
| 329 if (pathArray.length > 0) { // In case of 0 this is root directory. | |
| 330 var lastIndex = pathArray.length - 1; | |
| 331 if (pathArray[lastIndex] == '') | |
| 332 pathArray.splice(lastIndex); | |
| 333 } | |
| 334 | |
| 335 // Get the actual metadata by iterating through every directory metadata | |
| 336 // on the path to the entry. | |
| 337 var entryMetadata = this.metadata; | |
| 338 for (var i = 0, limit = pathArray.length; i < limit; i++) { | |
| 339 if (!entryMetadata || | |
| 340 !entryMetadata.isDirectory && i != limit - 1 /* Parent directory. */) | |
| 341 return null; | |
| 342 entryMetadata = entryMetadata.entries[pathArray[i]]; | |
| 343 } | |
| 344 | |
| 345 return entryMetadata; | |
| 346 }; | |
| OLD | NEW |