1 /** 2 * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global runtime, core, DOMParser*/ 26 /*jslint bitwise: true*/ 27 28 /** 29 * @constructor 30 * @param {!string} url path to zip file, should be readable by the runtime 31 * @param {?function(?string, !core.Zip):undefined} entriesReadCallback callback 32 * indicating the zip 33 * has loaded this list of entries, the arguments are a string that 34 * indicates error if present and the created object 35 */ 36 core.Zip = function Zip(url, entriesReadCallback) { 37 "use strict"; 38 var /**@type{Array.<!ZipEntry>}*/ 39 entries, 40 /**@type{?Uint8Array}*/ 41 fileData, 42 /**@type{number}*/ 43 filesize, 44 /**@type{number}*/ 45 nEntries, 46 /**@type{!function(!Uint8Array,number):!Uint8Array}*/ 47 inflate = core.RawInflate.inflate, 48 /**@type{!core.Zip}*/ 49 zip = this, 50 base64 = new core.Base64(); 51 52 /** 53 * @param {!number} offset 54 * @param {!number} length 55 * @param {!function(?string,?Uint8Array):undefined} callback 56 * @return {undefined} 57 */ 58 function read(offset, length, callback) { 59 if (fileData) { 60 callback(null, fileData.subarray(offset, offset + length)); 61 } else { 62 callback("File data not loaded", null); 63 } 64 } 65 66 /** 67 * @param {!Uint8Array} data 68 * @return {!number} 69 */ 70 function crc32(data) { 71 // Calculate the crc32 polynomial of a string 72 // 73 // version: 1009.2513 74 // discuss at: http:\/\/phpjs.org\/functions\/crc32 75 // + original by: Webtoolkit.info (http:\/\/www.webtoolkit.info\/) 76 // + improved by: T0bsn 77 // - depends on: utf8_encode 78 // * example 1: crc32('Kevin van Zonneveld'); 79 // * returns 1: 1249991249 80 var /**@const 81 @type{!Array.<!number>}*/ 82 table = [0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D], 83 /**@type{!number}*/ 84 crc = 0, 85 /**@type{!number}*/ 86 i, 87 /**@const@type{!number}*/ 88 iTop = data.length, 89 /**@type{!number}*/ 90 x = 0, 91 /**@type{!number}*/ 92 y = 0; 93 crc = crc ^ (-1); 94 for (i = 0; i < iTop; i += 1) { 95 y = (crc ^ data[i]) & 0xFF; 96 x = table[y]; 97 crc = (crc >>> 8) ^ x; 98 } 99 return crc ^ (-1); 100 } 101 102 /** 103 * @param {!number} dostime 104 * @return {!Date} 105 */ 106 function dosTime2Date(dostime) { 107 var /**@const@type{!number}*/ 108 year = ((dostime >> 25) & 0x7f) + 1980, 109 /**@const@type{!number}*/ 110 month = ((dostime >> 21) & 0x0f) - 1, 111 /**@const@type{!number}*/ 112 mday = (dostime >> 16) & 0x1f, 113 /**@const@type{!number}*/ 114 hour = (dostime >> 11) & 0x0f, 115 /**@const@type{!number}*/ 116 min = (dostime >> 5) & 0x3f, 117 /**@const@type{!number}*/ 118 sec = (dostime & 0x1f) << 1, 119 /**@const@type{!Date}*/ 120 d = new Date(year, month, mday, hour, min, sec); 121 return d; 122 } 123 /** 124 * @param {!Date} date 125 * @return {!number} 126 */ 127 function date2DosTime(date) { 128 var /**@const@type{!number}*/ y = date.getFullYear(); 129 return y < 1980 ? 0 : 130 ((y - 1980) << 25) | ((date.getMonth() + 1) << 21) | 131 (date.getDate() << 16) | (date.getHours() << 11) | 132 (date.getMinutes() << 5) | (date.getSeconds() >> 1); 133 } 134 /** 135 * Create a new ZipEntry. 136 * If the stream is not provided, the object should be initialized 137 * with ZipEntry.set() 138 * @constructor 139 * @param {!string} url 140 * @param {!core.ByteArray=} stream 141 */ 142 function ZipEntry(url, stream) { 143 var /**@const 144 @type{!number}*/ 145 sig, 146 /**@const 147 @type{!number}*/ 148 namelen, 149 /**@const 150 @type{!number}*/ 151 extralen, 152 /**@const 153 @type{!number}*/ 154 commentlen, 155 /**@const 156 @type{!number}*/ 157 compressionMethod, 158 /**@const 159 @type{!number}*/ 160 compressedSize, 161 /**@const 162 @type{!number}*/ 163 uncompressedSize, 164 /**@const 165 @type{!number}*/ 166 offset, 167 /**@const 168 @type{!ZipEntry}*/ 169 entry = this; 170 171 /** 172 * @param {!Uint8Array} data 173 * @param {!function(?string, ?Uint8Array)} callback 174 * @return {undefined} 175 */ 176 function handleEntryData(data, callback) { 177 var /**@const 178 @type{!core.ByteArray}*/ 179 estream = new core.ByteArray(data), 180 /**@const 181 @type{!number}*/ 182 esig = estream.readUInt32LE(), 183 /**@const 184 @type{!number}*/ 185 filenamelen, 186 /**@const 187 @type{!number}*/ 188 eextralen; 189 if (esig !== 0x04034b50) { 190 callback('File entry signature is wrong.' + esig.toString() + 191 ' ' + data.length.toString(), null); 192 return; 193 } 194 estream.pos += 22; 195 filenamelen = estream.readUInt16LE(); 196 eextralen = estream.readUInt16LE(); 197 estream.pos += filenamelen + eextralen; 198 if (compressionMethod) { 199 data = data.subarray(estream.pos, estream.pos + compressedSize); 200 if (compressedSize !== data.length) { 201 callback("The amount of compressed bytes read was " + 202 data.length.toString() + " instead of " + 203 compressedSize.toString() + " for " + entry.filename + 204 " in " + url + ".", null); 205 return; 206 } 207 data = inflate(data, uncompressedSize); 208 } else { 209 data = data.subarray(estream.pos, estream.pos + uncompressedSize); 210 } 211 if (uncompressedSize !== data.length) { 212 callback("The amount of bytes read was " + 213 data.length.toString() + 214 " instead of " + uncompressedSize.toString() + " for " + 215 entry.filename + " in " + url + ".", null); 216 return; 217 } 218 /* 219 * This check is disabled for performance reasons 220 if (crc !== crc32(data)) { 221 runtime.log("Warning: CRC32 for " + entry.filename + 222 " is wrong."); 223 } 224 */ 225 entry.data = data; 226 callback(null, data); 227 } 228 /** 229 * @param {!function(?string, ?Uint8Array)} callback 230 * @return {undefined} 231 */ 232 function load(callback) { 233 // if data has already been downloaded, use that 234 if (entry.data !== null) { 235 callback(null, entry.data); 236 return; 237 } 238 // the 256 at the end is security for when local extra field is 239 // larger 240 var /**@type{!number}*/ size 241 = compressedSize + 34 + namelen + extralen + 256; 242 if (size + offset > filesize) { 243 size = filesize - offset; 244 } 245 read(offset, size, function (err, data) { 246 if (err || data === null) { 247 callback(err, data); 248 } else { 249 handleEntryData(data, callback); 250 } 251 }); 252 } 253 this.load = load; 254 /** 255 * @param {!string} filename 256 * @param {!Uint8Array} data 257 * @param {!boolean} compressed 258 * @param {!Date} date 259 * @return {undefined} 260 */ 261 function set(filename, data, compressed, date) { 262 entry.filename = filename; 263 entry.data = data; 264 entry.compressed = compressed; 265 entry.date = date; 266 } 267 this.set = set; 268 /** 269 * @type {?string} 270 */ 271 this.error = null; 272 273 if (!stream) { 274 return; 275 } 276 sig = stream.readUInt32LE(); 277 278 if (sig !== 0x02014b50) { 279 this.error = 280 "Central directory entry has wrong signature at position " + 281 (stream.pos - 4).toString() + ' for file "' + url + '": ' + 282 stream.data.length.toString(); 283 return; 284 } 285 // stream should be positioned at the start of the CDS entry for the 286 // file 287 stream.pos += 6; 288 compressionMethod = stream.readUInt16LE(); 289 this.date = dosTime2Date(stream.readUInt32LE()); 290 stream.readUInt32LE(); 291 compressedSize = stream.readUInt32LE(); 292 uncompressedSize = stream.readUInt32LE(); 293 namelen = stream.readUInt16LE(); 294 extralen = stream.readUInt16LE(); 295 commentlen = stream.readUInt16LE(); 296 stream.pos += 8; 297 offset = stream.readUInt32LE(); 298 this.filename = runtime.byteArrayToString( 299 stream.data.subarray(stream.pos, stream.pos + namelen), 300 "utf8" 301 ); 302 /**@type{?Uint8Array}*/ 303 this.data = null; 304 stream.pos += namelen + extralen + commentlen; 305 } 306 /** 307 * @param {!Uint8Array} data 308 * @param {!function(?string, !core.Zip)} callback 309 * @return {undefined} 310 */ 311 function handleCentralDirectory(data, callback) { 312 // parse the central directory 313 var stream = new core.ByteArray(data), i, e; 314 entries = []; 315 for (i = 0; i < nEntries; i += 1) { 316 e = new ZipEntry(url, stream); 317 if (e.error) { 318 callback(e.error, zip); 319 return; 320 } 321 entries[entries.length] = e; 322 } 323 // report that entries are listed and no error occured 324 callback(null, zip); 325 } 326 /** 327 * @param {!Uint8Array} data 328 * @param {!function(?string, !core.Zip)} callback 329 * @return {undefined} 330 */ 331 function handleCentralDirectoryEnd(data, callback) { 332 if (data.length !== 22) { 333 callback("Central directory length should be 22.", zip); 334 return; 335 } 336 var stream = new core.ByteArray(data), sig, disk, cddisk, diskNEntries, 337 cdsSize, cdsOffset; 338 sig = stream.readUInt32LE(); 339 if (sig !== 0x06054b50) { 340 callback('Central directory signature is wrong: ' + sig.toString(), 341 zip); 342 return; 343 } 344 disk = stream.readUInt16LE(); 345 if (disk !== 0) { 346 callback('Zip files with non-zero disk numbers are not supported.', 347 zip); 348 return; 349 } 350 cddisk = stream.readUInt16LE(); 351 if (cddisk !== 0) { 352 callback('Zip files with non-zero disk numbers are not supported.', 353 zip); 354 return; 355 } 356 diskNEntries = stream.readUInt16LE(); 357 nEntries = stream.readUInt16LE(); 358 if (diskNEntries !== nEntries) { 359 callback('Number of entries is inconsistent.', zip); 360 return; 361 } 362 cdsSize = stream.readUInt32LE(); 363 cdsOffset = stream.readUInt16LE(); 364 cdsOffset = filesize - 22 - cdsSize; 365 366 // for some reason cdsOffset is not always equal to offset calculated 367 // from the central directory size. The latter is reliable. 368 read(cdsOffset, filesize - cdsOffset, 369 function (err, data) { 370 if (err || data === null) { 371 callback(err, zip); 372 } else { 373 handleCentralDirectory(data, callback); 374 } 375 }); 376 } 377 /** 378 * @param {!string} filename 379 * @param {!function(?string, ?Uint8Array)} callback receiving err and data 380 * @return {undefined} 381 */ 382 function load(filename, callback) { 383 var entry = null, 384 e, 385 i; 386 for (i = 0; i < entries.length; i += 1) { 387 e = entries[i]; 388 if (e.filename === filename) { 389 entry = e; 390 break; 391 } 392 } 393 if (entry) { 394 if (entry.data) { 395 callback(null, entry.data); 396 } else { 397 entry.load(callback); 398 } 399 } else { 400 callback(filename + " not found.", null); 401 } 402 } 403 /** 404 * @param {!string} filename 405 * @param {!function(?string, ?string):undefined} callback receiving err and data 406 * @return {undefined} 407 */ 408 function loadAsString(filename, callback) { 409 // the javascript implementation simply reads the file and converts to 410 // string 411 load(filename, function (err, data) { 412 if (err || data === null) { 413 return callback(err, null); 414 } 415 var d = runtime.byteArrayToString(data, "utf8"); 416 callback(null, d); 417 }); 418 } 419 /** 420 * @param {!string} filename 421 * @param {!{rootElementReady: function(?string, ?string=, boolean=):undefined}} handler 422 * @return {undefined} 423 */ 424 function loadContentXmlAsFragments(filename, handler) { 425 // the javascript implementation simply reads the file 426 zip.loadAsString(filename, function (err, data) { 427 if (err) { 428 return handler.rootElementReady(err); 429 } 430 handler.rootElementReady(null, data, true); 431 }); 432 } 433 /** 434 * @param {!string} filename 435 * @param {!string} mimetype 436 * @param {!function(?string,?string):undefined} callback 437 */ 438 function loadAsDataURL(filename, mimetype, callback) { 439 load(filename, function (err, data) { 440 if (err || !data) { 441 return callback(err, null); 442 } 443 var /**@const@type{!Uint8Array}*/p = data, 444 chunksize = 45000, // must be multiple of 3 and less than 50000 445 i = 0, 446 dataurl; 447 if (!mimetype) { 448 if (p[1] === 0x50 && p[2] === 0x4E && p[3] === 0x47) { 449 mimetype = "image/png"; 450 } else if (p[0] === 0xFF && p[1] === 0xD8 && p[2] === 0xFF) { 451 mimetype = "image/jpeg"; 452 } else if (p[0] === 0x47 && p[1] === 0x49 && p[2] === 0x46) { 453 mimetype = "image/gif"; 454 } else { 455 mimetype = ""; 456 } 457 } 458 dataurl = 'data:' + mimetype + ';base64,'; 459 // to avoid exceptions, base64 encoding is done in chunks 460 // it would make sense to move this to base64.toBase64 461 while (i < data.length) { 462 dataurl += base64.convertUTF8ArrayToBase64( 463 p.subarray(i, Math.min(i + chunksize, p.length)) 464 ); 465 i += chunksize; 466 } 467 callback(null, dataurl); 468 }); 469 } 470 /** 471 * @param {!string} filename 472 * @param {function(?string,?Document):undefined} callback 473 * @return {undefined} 474 */ 475 function loadAsDOM(filename, callback) { 476 zip.loadAsString(filename, function (err, xmldata) { 477 if (err || xmldata === null) { 478 callback(err, null); 479 return; 480 } 481 var parser = new DOMParser(), 482 dom = parser.parseFromString(xmldata, "text/xml"); 483 callback(null, dom); 484 }); 485 } 486 /** 487 * Add or replace an entry to the zip file. 488 * This data is not stored to disk yet, and therefore, no callback is 489 * necessary. 490 * @param {!string} filename 491 * @param {!Uint8Array} data 492 * @param {!boolean} compressed 493 * @param {!Date} date 494 * @return {undefined} 495 */ 496 function save(filename, data, compressed, date) { 497 var i, 498 entry; 499 for (i = 0; i < entries.length; i += 1) { 500 entry = entries[i]; 501 if (entry.filename === filename) { 502 entry.set(filename, data, compressed, date); 503 return; 504 } 505 } 506 entry = new ZipEntry(url); 507 entry.set(filename, data, compressed, date); 508 entries.push(entry); 509 } 510 /** 511 * Removes entry from the zip. 512 * @param {!string} filename 513 * @return {!boolean} return false if entry is not found; otherwise true. 514 */ 515 function remove(filename) { 516 var i, entry; 517 for (i = 0; i < entries.length; i += 1) { 518 entry = entries[i]; 519 if (entry.filename === filename) { 520 entries.splice(i, 1); 521 return true; 522 } 523 } 524 return false; 525 } 526 /** 527 * @param {!ZipEntry} entry 528 * @return {!core.ByteArrayWriter} 529 */ 530 function writeEntry(entry) { 531 // each entry is currently stored uncompressed 532 var data = new core.ByteArrayWriter("utf8"), 533 length = 0; 534 data.appendArray([0x50, 0x4B, 0x03, 0x04, 0x14, 0, 0, 0, 0, 0]); 535 if (entry.data) { 536 length = entry.data.length; 537 } 538 data.appendUInt32LE(date2DosTime(entry.date)); 539 data.appendUInt32LE(entry.data ? crc32(entry.data) : 0); 540 data.appendUInt32LE(length); // compressedSize 541 data.appendUInt32LE(length); // uncompressedSize 542 data.appendUInt16LE(entry.filename.length); // namelen 543 data.appendUInt16LE(0); // extralen 544 data.appendString(entry.filename); 545 if (entry.data) { 546 data.appendByteArray(entry.data); 547 } 548 return data; 549 } 550 /** 551 * @param {!ZipEntry} entry 552 * @param {!number} offset 553 * @return {!core.ByteArrayWriter} 554 */ 555 function writeCODEntry(entry, offset) { 556 // each entry is currently stored uncompressed 557 var data = new core.ByteArrayWriter("utf8"), 558 length = 0; 559 data.appendArray([0x50, 0x4B, 0x01, 0x02, 0x14, 0x00, 0x14, 0x00, 560 0, 0, 0, 0]); 561 if (entry.data) { 562 length = entry.data.length; 563 } 564 data.appendUInt32LE(date2DosTime(entry.date)); 565 data.appendUInt32LE(entry.data ? crc32(entry.data) : 0); 566 data.appendUInt32LE(length); // compressedSize 567 data.appendUInt32LE(length); // uncompressedSize 568 data.appendUInt16LE(entry.filename.length); // namelen 569 // extralen, commalen, diskno, file attributes 570 data.appendArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); 571 data.appendUInt32LE(offset); 572 data.appendString(entry.filename); 573 return data; 574 } 575 /** 576 * @param {!number} position 577 * @param {!function(?string):undefined} callback 578 * @return {undefined} 579 */ 580 function loadAllEntries(position, callback) { 581 if (position === entries.length) { 582 callback(null); 583 return; 584 } 585 var entry = entries[position]; 586 if (entry.data !== null) { 587 loadAllEntries(position + 1, callback); 588 return; 589 } 590 entry.load(function (err) { 591 if (err) { 592 callback(err); 593 return; 594 } 595 loadAllEntries(position + 1, callback); 596 }); 597 } 598 /** 599 * Create a bytearray from the zipfile. 600 * @param {!function(!Uint8Array):undefined} successCallback receiving zip as bytearray 601 * @param {!function(?string):undefined} errorCallback receiving possible err 602 * @return {undefined} 603 */ 604 function createByteArray(successCallback, errorCallback) { 605 // make sure all data is in memory, for each entry that has data 606 // undefined, try to load the entry 607 loadAllEntries(0, function (err) { 608 if (err) { 609 errorCallback(err); 610 return; 611 } 612 var i, e, codoffset, codsize, 613 data = new core.ByteArrayWriter("utf8"), 614 offsets = [0]; 615 // write entries 616 for (i = 0; i < entries.length; i += 1) { 617 data.appendByteArrayWriter(writeEntry(entries[i])); 618 offsets.push(data.getLength()); 619 } 620 // write central directory 621 codoffset = data.getLength(); 622 for (i = 0; i < entries.length; i += 1) { 623 e = entries[i]; 624 data.appendByteArrayWriter(writeCODEntry(e, offsets[i])); 625 } 626 codsize = data.getLength() - codoffset; 627 data.appendArray([0x50, 0x4B, 0x05, 0x06, 0, 0, 0, 0]); 628 data.appendUInt16LE(entries.length); 629 data.appendUInt16LE(entries.length); 630 data.appendUInt32LE(codsize); 631 data.appendUInt32LE(codoffset); 632 data.appendArray([0, 0]); 633 634 successCallback(data.getByteArray()); 635 }); 636 } 637 /** 638 * Write the zipfile to the given path. 639 * @param {!string} newurl 640 * @param {!function(?string):undefined} callback receiving possible err 641 * @return {undefined} 642 */ 643 function writeAs(newurl, callback) { 644 // make sure all data is in memory, for each entry that has data 645 // undefined, try to load the entry 646 createByteArray(function (data) { 647 runtime.writeFile(newurl, data, function (err) { 648 if (!err) { 649 fileData = data; 650 filesize = fileData.length; 651 } 652 callback(err); 653 }); 654 }, callback); 655 } 656 /** 657 * Write the zipfile to the given path. 658 * @param {!function(?string):undefined} callback receiving possible err 659 * @return {undefined} 660 */ 661 function write(callback) { 662 writeAs(url, callback); 663 } 664 this.load = load; 665 this.save = save; 666 this.remove = remove; 667 this.write = write; 668 this.writeAs = writeAs; 669 this.createByteArray = createByteArray; 670 // a special function that makes faster odf loading possible 671 this.loadContentXmlAsFragments = loadContentXmlAsFragments; 672 this.loadAsString = loadAsString; 673 this.loadAsDOM = loadAsDOM; 674 this.loadAsDataURL = loadAsDataURL; 675 this.getEntries = function () { 676 return entries.slice(); 677 }; 678 679 // determine the file size 680 filesize = -1; 681 // if no callback is defined, this is a new file 682 if (entriesReadCallback === null) { 683 entries = []; 684 return; 685 } 686 runtime.readFile(url, "binary", function (err, result) { 687 if (typeof result === "string") { 688 err = "file was read as a string. Should be Uint8Array."; 689 } 690 if (err || !result || result.length === 0) { 691 entriesReadCallback("File '" + url + "' cannot be read. Err: " + (err || "[none]"), zip); 692 } else { 693 fileData = /**@type{!Uint8Array}*/(result); 694 filesize = fileData.length; 695 read(filesize - 22, 22, function (err, data) { 696 // todo: refactor entire zip class 697 if (err || data === null) { 698 entriesReadCallback(err, zip); 699 } else { 700 handleCentralDirectoryEnd(data, /**@type{!function(?string, !core.Zip):undefined}*/(entriesReadCallback)); 701 } 702 }); 703 } 704 }); 705 }; 706