Add simple android client.
[odfkit:webodf.git] / android / assets / lib / core / Zip.js
1 /*global runtime core*/
2 /*jslint bitwise: false*/
3 /*
4 * @preserve
5 * OdfKit
6 * Copyright (c) 2010 Jos van den Oever 
7 * Licensed under the ... License:
8 *
9 * Project home: http://www.odfkit.org/
10 */
11
12 runtime.loadClass("core.RawInflate");
13 runtime.loadClass("core.ByteArray");
14
15 /**
16  * @constructor
17  * @param {!string} url path to zip file, should be readable by the runtime
18  * @param {?function(?string, !core.Zip):undefined} entriesReadCallback callback
19  *        indicating the zip
20  *        has loaded this list of entries, the arguments are a string that
21  *        indicates error if present and the created object
22  */
23 core.Zip = function Zip(url, entriesReadCallback) {
24     var entries, filesize, nEntries,
25         inflate = new core.RawInflate().inflate,
26         zip = this;
27
28     function crc32(str) {
29         // Calculate the crc32 polynomial of a string  
30         // 
31         // version: 1009.2513
32         // discuss at: http:\/\/phpjs.org\/functions\/crc32
33         // +   original by: Webtoolkit.info (http:\/\/www.webtoolkit.info\/)
34         // +   improved by: T0bsn
35         // -    depends on: utf8_encode
36         // *     example 1: crc32('Kevin van Zonneveld');
37         // *     returns 1: 1249991249
38         var 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],
39             crc = 0, i, iTop,
40             x = 0,
41             y = 0;
42     
43         crc = crc ^ (-1);
44         for (i = 0, iTop = str.length; i < iTop; i += 1) {
45             y = (crc ^ str.charCodeAt(i)) & 0xFF;
46             x = table[y];
47             crc = (crc >>> 8) ^ x;
48         }
49         return crc ^ (-1);
50     }
51
52     /**
53      * @param {!number} dostime
54      * @return {!Date}
55      */
56     function dosTime2Date(dostime) {
57         var year = ((dostime >> 25) & 0x7f) + 1980,
58             month = ((dostime >> 21) & 0x0f) - 1,
59             mday = (dostime >> 16) & 0x1f,
60             hour = (dostime >> 11) & 0x0f,
61             min = (dostime >> 5) & 0x3f,
62             sec = (dostime & 0x1f) << 1,
63             d = new Date(year, month, mday, hour, min, sec);
64         return d;
65     }
66     /**
67      * @param {!Date} date
68      * @return {!number}
69      */
70     function date2DosTime(date) {
71         var y = date.getFullYear();
72         return y < 1980 ? 0 :
73             ((y - 1980) << 25) | ((date.getMonth() + 1) << 21) |
74             (date.getDate() << 16) | (date.getHours() << 11) |
75             (date.getMinutes() << 5) | (date.getSeconds() >> 1);
76     }
77     /**
78      * @constructor
79      * @param {!string} url
80      * @param {!core.ByteArray} stream
81      */
82     function ZipEntry(url, stream) {
83         var sig = stream.readUInt32LE(),
84             namelen, extralen, commentlen,
85             compressionMethod,
86             compressedSize,
87             uncompressedSize,
88             offset, crc,
89             entry = this;
90
91         function handleEntryData(data, callback) {
92             var stream = new core.ByteArray(data),
93                 sig = stream.readUInt32LE(),
94                 filenamelen, extralen;
95             if (sig !== 0x04034b50) {
96                 callback('File entry signature is wrong.' + sig + ' ' +
97                         data.length, null);
98                 return;
99             }
100             stream.pos += 22;
101             filenamelen = stream.readUInt16LE();
102             extralen = stream.readUInt16LE();
103             stream.pos += filenamelen + extralen;
104             if (compressionMethod) {
105                 data = stream.data.substr(stream.pos, compressedSize);
106                 if (compressedSize !== data.length) {
107                     callback("The amount of compressed bytes read was " +
108                         data.length + " instead of " + compressedSize +
109                         " for " + entry.filename + " in " + url + ".");
110                     return;
111                 }
112                 data = inflate(data);
113             } else {
114                 data = stream.data.substr(stream.pos, uncompressedSize);
115             }
116             if (uncompressedSize !== data.length) {
117                 callback("The amount of bytes read was " + data.length +
118                         " instead of " + uncompressedSize + " for " +
119                         entry.filename + " in " + url + ".");
120                 return;
121             }
122 /*
123  * This check is disabled for performance reasons
124             if (crc !== crc32(data)) {
125                 runtime.log("Warning: CRC32 for " + entry.filename +
126                     " is wrong.");
127             }
128 */
129             entry.data = data;
130             callback(null, data);
131         }
132         /**
133          * @param {!function(?string, ?string)} callback with err and data
134          * @return {undefined}
135          */
136         function load(callback) {
137             // if data has already been downloaded, use that
138             if (entry.data !== undefined) {
139                 callback(null, entry.data);
140                 return;
141             }
142             // the 256 at the end is security for when local extra field is
143             // larger
144             var size = compressedSize + 34 + namelen + extralen + 256;
145             runtime.read(url, offset, size, function (err, data) {
146                 if (err) {
147                     callback(err, data);
148                 } else {
149                     handleEntryData(data, callback);
150                 }
151             });
152         }
153         this.load = load;
154         /**
155          * @type {?string}
156          */
157         this.error = null;
158         if (sig !== 0x02014b50) {
159             this.error =
160                 "Central directory entry has wrong signature at position " +
161                 (stream.pos - 4) + ' for file "' + url + '": ' +
162                 stream.data.length;
163             return;
164         }
165         // stream should be positioned at the start of the CDS entry for the
166         // file
167         stream.pos += 6;
168         compressionMethod = stream.readUInt16LE();
169         this.date = dosTime2Date(stream.readUInt32LE());
170         crc = stream.readUInt32LE();
171         compressedSize = stream.readUInt32LE();
172         uncompressedSize = stream.readUInt32LE();
173         namelen = stream.readUInt16LE();
174         extralen = stream.readUInt16LE();
175         commentlen = stream.readUInt16LE();
176         stream.pos += 8;
177         offset = stream.readUInt32LE();
178         this.filename = stream.data.substr(stream.pos, namelen);
179         stream.pos += namelen + extralen + commentlen;
180     }
181     /**
182      * @param {!string} data
183      * @param {!function(?string, !core.Zip)} callback
184      * @return {undefined}
185      */
186     function handleCentralDirectory(data, callback) {
187         // parse the central directory
188         var stream = new core.ByteArray(data), i, e;
189         entries = [];
190         for (i = 0; i < nEntries; i += 1) {
191             e = new ZipEntry(url, stream);
192             if (e.error) {
193                 callback(e.error, zip);
194                 return;
195             }
196             entries[entries.length] = e;
197         }
198         // report that entries are listed and no error occured
199         callback(null, zip);
200     }
201     /**
202      * @param {!string} data
203      * @param {!function(?string, !core.Zip)} callback
204      * @return {undefined}
205      */
206     function handleCentralDirectoryEnd(data, callback) {
207         if (data.length !== 22) {
208             callback("Central directory length should be 22.", zip);
209             return;
210         }
211         var stream = new core.ByteArray(data), sig, disk, cddisk, diskNEntries,
212             cdsSize, cdsOffset;
213         sig = stream.readUInt32LE();
214         if (sig !== 0x06054b50) {
215             callback('Central directory signature is wrong: ' + sig, zip);
216             return;
217         }
218         disk = stream.readUInt16LE();
219         if (disk !== 0) {
220             callback('Zip files with non-zero disk numbers are not supported.',
221                     zip);
222             return;
223         }
224         cddisk = stream.readUInt16LE();
225         if (cddisk !== 0) {
226             callback('Zip files with non-zero disk numbers are not supported.',
227                     zip);
228             return;
229         }
230         diskNEntries = stream.readUInt16LE();
231         nEntries = stream.readUInt16LE();
232         if (diskNEntries !== nEntries) {
233             callback('Number of entries is inconsistent.', zip);
234             return;
235         }
236         cdsSize = stream.readUInt32LE();
237         cdsOffset = stream.readUInt16LE();
238         cdsOffset = filesize - 22 - cdsSize;
239     
240         // for some reason cdsOffset is not always equal to offset calculated
241         // from the central directory size. The latter is reliable.
242         runtime.read(url, cdsOffset, filesize - cdsOffset,
243                 function (err, data) {
244             handleCentralDirectory(data, callback);
245         });
246     }
247     /**
248      * @param {!string} filename
249      * @param {!function(?string, ?string)} callback receiving err and data
250      * @return {undefined}
251      */
252     function load(filename, callback) {
253         var entry = null,
254             end = filesize,
255             e, i;
256         for (i = 0; i < entries.length; i += 1) {
257             e = entries[i];
258             if (e.filename === filename) {
259                 entry = e;
260                 break;
261             }
262         }
263         if (entry) {
264             if (entry.data) {
265                 callback(null, entry.data);
266             } else {
267                 entry.load(callback);
268             }
269         } else {
270             callback(filename + " not found.", null);
271         }
272     }
273     /**
274      * Add or replace an entry to the zip file.
275      * This data is not stored to disk yet, and therefore, no callback is
276      * necessary.
277      * @param {!string} filename
278      * @param {!string} data
279      * @param {!boolean} compressed
280      * @param {!Date} date
281      * @return {undefined}
282      */
283     function save(filename, data, compressed, date) {
284         var e = { filename: filename, data: data, compressed: compressed,
285                   date: date },
286             i, olde;
287         for (i = 0; i < entries.length; i += 1) {
288             olde = entries[i];
289             if (olde.filename === filename) {
290                 entries[i] = e;
291                 return;
292             }
293         }
294         entries.push(e);
295     }
296     function uint32LE(value) {
297         return String.fromCharCode(value & 0xff) +
298             String.fromCharCode((value>>8) & 0xff) +
299             String.fromCharCode((value>>16) & 0xff) +
300             String.fromCharCode((value>>24) & 0xff);
301     }
302     function uint16LE(value) {
303         return String.fromCharCode(value & 0xff) +
304             String.fromCharCode((value>>8) & 0xff);
305     }
306     /**
307      * @param {!ZipEntry} entry
308      * @return {!string}
309      */
310     function writeEntry(entry) {
311         // each entry is currently stored uncompressed
312         var data = "PK\x03\x04\x0a\x00\x00\x00\x00\x00";
313         data += uint32LE(date2DosTime(entry.date));
314         data += uint32LE(crc32(entry.data));
315         data += uint32LE(entry.data.length); // compressedSize
316         data += uint32LE(entry.data.length); // uncompressedSize
317         data += uint16LE(entry.filename.length); // namelen
318         data += uint16LE(0); // extralen
319         data += entry.filename;
320         data += entry.data;
321         return data;
322     }
323     /**
324      * @param {!ZipEntry} entry
325      * @param {!number} offset
326      * @return {!string}
327      */
328     function writeCODEntry(entry, offset) {
329         // each entry is currently stored uncompressed
330         var data = "PK\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00";
331         // mtime = mdate = 0 for now
332         data += uint32LE(date2DosTime(entry.date));
333         data += uint32LE(crc32(entry.data));
334         data += uint32LE(entry.data.length); // compressedSize
335         data += uint32LE(entry.data.length); // uncompressedSize
336         data += uint16LE(entry.filename.length); // namelen
337         // extralen, commalen, diskno, file attributes
338         data += "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
339         data += uint32LE(offset);
340         data += entry.filename;
341         return data;
342     }
343     /**
344      * Write the zipfile to the given path.
345      * @param {!function(?string):undefined} callback receiving possible err
346      * @return {undefined}
347      */
348     function write(callback) {
349         var data = "", i, e, codoffset, codsize,
350             offsets = [0];
351         // write entries
352         for (i = 0; i < entries.length; i += 1) {
353             data += writeEntry(entries[i]);
354             offsets.push(data.length);
355         }
356         // write central directory
357         codoffset = data.length;
358         for (i = 0; i < entries.length; i += 1) {
359             e = entries[i];
360             data += writeCODEntry(e, offsets[i]);
361         }
362         codsize = data.length - codoffset;
363         data += "PK\x05\x06\x00\x00\x00\x00";
364         data += uint16LE(entries.length);
365         data += uint16LE(entries.length);
366         data += uint32LE(codsize);
367         data += uint32LE(codoffset);
368         data += "\x00\x00";
369         runtime.writeFile(url, data, "binary", callback);
370     }
371
372     this.load = load;
373     this.save = save;
374     this.write = write;
375     this.getEntries = function () {
376         return entries.slice();
377     };
378
379     // determine the file size
380     filesize = -1;
381     // if no callback is defined, this is a new file
382     if (entriesReadCallback === null) {
383         entries = [];
384         return;
385     }
386     runtime.getFileSize(url, function (size) {
387         filesize = size;
388         if (filesize < 0) {
389             entriesReadCallback("File '" + url + "' cannot be read.", zip);
390         } else {
391             runtime.read(url, filesize - 22, 22, function (err, data) {
392                 // todo: refactor entire zip class
393                 if (err || entriesReadCallback === null) {
394                     entriesReadCallback(err, zip);
395                 } else {
396                     handleCentralDirectoryEnd(data, entriesReadCallback);
397                 }
398             });
399         }
400     });
401 };