Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

jszip-load : zip64 support (enhance issue #7)

Zip64 are generated for big files (> 4GiB, not really useful in a
browser) and when a stream is compressed (the zip utility doesn't know
the size, and uses zip64 for the worst case scenario).

Two other changes :
- The parser now read things backward (end of central dir, central dir,
  local files). Reading the file backward seems odd, but the zip format
  was designed to be read like that (and I have less troubles doing so).
- I also enforced the Stuk's coding style (to be coherent with the other
  files).
  • Loading branch information...
commit 482b3a92bb7a490af0f67d1d2b0802b8386fbbaa 1 parent d71158b
David Duponchel dduponchel authored
6 index.html
View
@@ -144,7 +144,7 @@
<div id="header" class="grid_12">
<div class="alpha grid_5">
<h1>JS<span style="font-weight:100">Zip</span></h1>
- Create .zip files with Javascript
+ Create and read .zip files with Javascript
</div>
</div>
@@ -450,11 +450,11 @@ <h4 id="doc_load_data_options">load(data, options)</h4>
<ul>
<li>Compression (<code>DEFLATE</code> with jszip-deflate.js)</li>
<li>zip with data descriptor</li>
+ <li>ZIP64</li>
</ul>
<h5>Zip features not (yet) supported</h5>
<ul>
<li>password protected zip</li>
- <li>ZIP64</li>
<li>multi-volume zip</li>
</ul>
@@ -485,7 +485,7 @@ <h4 id="doc_filter_predicate">filter(predicate)</h4>
<h3 id="zip_load_limits">Loading zip files, limitations</h3>
<p>
The first limitation comes from the limited subset of supported zip features.
- Classic zip files will work but ZIP64, encrypted zip, etc are not supported
+ Classic zip files will work but encrypted zip, multi-volume, etc are not supported
and the load() method will throw an <code>Error</code>.
</p>
<p>
948 jszip-load.js
View
@@ -9,422 +9,548 @@ Licenced under the GPLv3 and the MIT licences
**/
(function () {
- /**
- * Prettify a string read as binary.
- * @param str the string to prettify.
- * @return a pretty string.
- */
- var pretty = function (str) {
- var res = '', code, i;
- for (i = 0; i < str.length; i++)
- {
- code = str.charCodeAt(i);
- res += '\\x' + (code < 10 ? "0" : "") + code.toString(16);
- }
- return res;
- };
-
- /**
- * Find a compression registered in JSZip.
- * @param compressionMethod the method magic to find.
- * @return the JSZip compression object, null if none found.
- */
- var findCompression = function (compressionMethod)
- {
- for (var method in JSZip.compressions)
- {
- if (JSZip.compressions[method].magic === compressionMethod)
+ /**
+ * Prettify a string read as binary.
+ * @param {String} str the string to prettify.
+ * @return {String} a pretty string.
+ */
+ var pretty = function (str)
+ {
+ var res = '', code, i;
+ for (i = 0; i < str.length; i++)
{
- return JSZip.compressions[method];
+ code = str.charCodeAt(i);
+ res += '\\x' + (code < 10 ? "0" : "") + code.toString(16);
}
- }
- return null;
- };
-
- /**
- * @Class StreamReader
- * Read bytes from a stream.
- * @param stream the stream to read.
- */
- function StreamReader(stream) {
- this.stream = stream;
- this.index = 0;
- }
- /**
- * Check that the offset will not go too far.
- * @param offset the additional offset to check.
- * @throws an Error if the offset is out of bounds.
- */
- StreamReader.prototype.checkRange = function (offset)
- {
- if (this.index + offset > this.stream.length)
- {
- throw new Error("End of stream reached (stream length = " + this.stream.length + ", asked index = " + (this.index + offset) + ")");
- }
- };
- /**
- * Check if the end of the file has been reached.
- * @return true if it is the case, false otherwise.
- */
- StreamReader.prototype.eof = function ()
- {
- return this.index >= this.stream.length;
- };
- /**
- * Get the byte at the specified index.
- * @param i the index to use.
- * @return a byte.
- */
- StreamReader.prototype.byteAt = function(i)
- {
- return this.stream.charCodeAt(i) & 0xff;
- };
- /**
- * Get the next byte of this stream.
- * @return the next byte.
- */
- StreamReader.prototype.byte = function ()
- {
- this.checkRange(1);
- return this.byteAt(this.index++ + 1);
- };
- /**
- * Get the next number with a given byte size.
- * @param size the number of bytes to read.
- * @return the corresponding number.
- */
- StreamReader.prototype.int = function (size)
- {
- var result = 0, i;
- this.checkRange(size);
- for(i = size - 1; i >= 0; i--)
- {
- result = (result << 8) + this.byteAt(this.index + i);
- }
- this.index += size;
- return result;
- };
- /**
- * Get the next string with a given byte size.
- * @param size the number of bytes to read.
- * @return the corresponding string.
- */
- StreamReader.prototype.string = function (size)
- {
- var result = "", i, code;
- this.checkRange(size);
- for(i = 0; i < size; i++)
- {
- code = this.byteAt(this.index + i);
- result += String.fromCharCode(code);
- }
- this.index += size;
- return result;
- };
- /**
- * Get the next date.
- * @return the date.
- */
- StreamReader.prototype.date = function ()
- {
- var dostime = this.int(4);
- return new Date(
- ((dostime >> 25) & 0x7f) + 1980, // year
- ((dostime >> 21) & 0x0f) - 1, // month
- (dostime >> 16) & 0x1f, // day
- (dostime >> 11) & 0x1f, // hour
- (dostime >> 5) & 0x3f, // minute
- (dostime & 0x1f) << 1); // second
- };
-
- /**
- * @Class ZipEntry
- * An entry in the zip file.
- */
- function ZipEntry()
- {
- }
- /**
- * say if the file is encrypted.
- * @return true if the file is encrypted, false otherwise.
- */
- ZipEntry.prototype.isEncrypted = function ()
- {
- return (this.bitFlag & 0x0001) === 0x0001;
- },
- /**
- * say if the file has a data decriptor.
- * @return true if the file has a data descriptor, false otherwise.
- */
- ZipEntry.prototype.hasDataDescriptor = function ()
- {
- return (this.bitFlag & 0x0008) === 0x0008;
- },
- /**
- * Read the local part header of a zip file and add the info in this object.
- * @param reader the reader to use.
- */
- ZipEntry.prototype.readLocalPartHeader = function(reader)
- {
- // the signature has already been consumed
- this.versionNeeded = reader.int(2);
- this.bitFlag = reader.int(2);
- this.compressionMethod = reader.string(2);
- this.date = reader.date();
- this.crc32 = reader.int(4);
- this.compressedSize = reader.int(4);
- this.uncompressedSize = reader.int(4);
- this.fileNameLength = reader.int(2);
- this.extraFieldsLength = reader.int(2);
-
- if (this.isEncrypted())
- {
- throw new Error("Encrypted zip are not supported");
- }
- };
- /**
- * Read the local part of a zip file and add the info in this object.
- * @param reader the reader to use.
- */
- ZipEntry.prototype.readLocalPart = function(reader)
- {
- var compression;
-
- this.readLocalPartHeader(reader);
-
- this.fileName = reader.string(this.fileNameLength);
- this.readExtraFields(reader, this.extraFieldsLength);
- if (!this.hasDataDescriptor())
- {
- // easy : we know the file length
- this.compressedFileData = reader.string(this.compressedSize);
- }
- else
- {
- // hard way : find the data descriptor manually
- this.compressedFileData = this.findDataUntilDataDescriptor(reader);
- this.crc32 = reader.int(4);
- this.compressedSize = reader.int(4);
- this.uncompressedSize = reader.int(4);
-
- if (this.compressedFileData.length !== this.compressedSize)
+ return res;
+ };
+
+ /**
+ * Find a compression registered in JSZip.
+ * @param {String} compressionMethod the method magic to find.
+ * @return {String} the JSZip compression object, null if none found.
+ */
+ var findCompression = function (compressionMethod)
+ {
+ for (var method in JSZip.compressions)
+ {
+ if (JSZip.compressions[method].magic === compressionMethod)
+ {
+ return JSZip.compressions[method];
+ }
+ }
+ return null;
+ };
+
+ // StreamReader {{{
+ /**
+ * @Class StreamReader
+ * Read bytes from a stream.
+ * @param {String} stream the stream to read.
+ */
+ function StreamReader(stream) {
+ this.stream = stream;
+ this.index = 0;
+ }
+ /**
+ * Check that the offset will not go too far.
+ * @param {String} offset the additional offset to check.
+ * @throws {Error} an Error if the offset is out of bounds.
+ */
+ StreamReader.prototype.checkRange = function (offset)
+ {
+ if (this.index + offset > this.stream.length)
+ {
+ throw new Error("End of stream reached (stream length = " +
+ this.stream.length + ", asked index = " +
+ (this.index + offset) + ")");
+ }
+ };
+ /**
+ * Change the index.
+ * @param {integer} newIndex The new index.
+ * @throws {Error} if the new index is out of the stream.
+ */
+ StreamReader.prototype.setIndex = function (newIndex)
+ {
+ if (this.stream.length < newIndex || newIndex < 0)
+ {
+ throw new Error("Corrupted zip : " +
+ "new index (" + newIndex + ") is out of range " +
+ "(stream length = " + this.stream.length + ")");
+ }
+ this.index = newIndex;
+ };
+ /**
+ * Check if the end of the file has been reached.
+ * @return {boolean} true if it is the case, false otherwise.
+ */
+ StreamReader.prototype.eof = function ()
+ {
+ return this.index >= this.stream.length;
+ };
+ /**
+ * Get the byte at the specified index.
+ * @param {integer} i the index to use.
+ * @return {character} a byte.
+ */
+ StreamReader.prototype.byteAt = function(i)
+ {
+ return this.stream.charCodeAt(i) & 0xff;
+ };
+ /**
+ * Get the next byte of this stream.
+ * @return {character} the next byte.
+ */
+ StreamReader.prototype.byte = function ()
+ {
+ this.checkRange(1);
+ return this.byteAt(this.index++ + 1);
+ };
+ /**
+ * Get the next number with a given byte size.
+ * @param {integer} size the number of bytes to read.
+ * @return {integer} the corresponding number.
+ */
+ StreamReader.prototype.int = function (size)
+ {
+ var result = 0, i;
+ this.checkRange(size);
+ for(i = size - 1; i >= 0; i--)
+ {
+ result = (result << 8) + this.byteAt(this.index + i);
+ }
+ this.index += size;
+ return result;
+ };
+ /**
+ * Get the next string with a given byte size.
+ * @param {integer} size the number of bytes to read.
+ * @return {String} the corresponding string.
+ */
+ StreamReader.prototype.string = function (size)
+ {
+ var result = "", i, code;
+ this.checkRange(size);
+ for(i = 0; i < size; i++)
+ {
+ code = this.byteAt(this.index + i);
+ result += String.fromCharCode(code);
+ }
+ this.index += size;
+ return result;
+ };
+ /**
+ * Get the next date.
+ * @return {Date} the date.
+ */
+ StreamReader.prototype.date = function ()
+ {
+ var dostime = this.int(4);
+ return new Date(
+ ((dostime >> 25) & 0x7f) + 1980, // year
+ ((dostime >> 21) & 0x0f) - 1, // month
+ (dostime >> 16) & 0x1f, // day
+ (dostime >> 11) & 0x1f, // hour
+ (dostime >> 5) & 0x3f, // minute
+ (dostime & 0x1f) << 1); // second
+ };
+ // }}} end of StreamReader
+
+ // ZipEntry {{{
+ /**
+ * @Class ZipEntry
+ * An entry in the zip file.
+ */
+ function ZipEntry(options)
+ {
+ this.options = options;
+ }
+ /**
+ * say if the file is encrypted.
+ * @return {boolean} true if the file is encrypted, false otherwise.
+ */
+ ZipEntry.prototype.isEncrypted = function ()
+ {
+ return (this.bitFlag & 0x0001) === 0x0001;
+ };
+ /**
+ * say if the file has a data decriptor.
+ * @return {boolean} true if the file has a data descriptor, false otherwise.
+ */
+ ZipEntry.prototype.hasDataDescriptor = function ()
+ {
+ return (this.bitFlag & 0x0008) === 0x0008;
+ };
+ /**
+ * say if the file is a zip64 file.
+ * @return {boolean} true if the file is zip64, false otherwise.
+ */
+ ZipEntry.prototype.isZIP64 = function ()
+ {
+ return this.options.zip64;
+ };
+ /**
+ * Read the local part header of a zip file and add the info in this object.
+ * @param {StreamReader} reader the reader to use.
+ */
+ ZipEntry.prototype.readLocalPartHeader = function(reader)
+ {
+ // the signature has already been consumed
+ this.versionNeeded = reader.int(2);
+ this.bitFlag = reader.int(2);
+ this.compressionMethod = reader.string(2);
+ this.date = reader.date();
+ this.crc32 = reader.int(4);
+ this.compressedSize = reader.int(4);
+ this.uncompressedSize = reader.int(4);
+ this.fileNameLength = reader.int(2);
+ this.extraFieldsLength = reader.int(2);
+
+ if (this.isEncrypted())
+ {
+ throw new Error("Encrypted zip are not supported");
+ }
+ };
+ /**
+ * Read the local part of a zip file and add the info in this object.
+ * @param {StreamReader} reader the reader to use.
+ */
+ ZipEntry.prototype.readLocalPart = function(reader)
+ {
+ var compression;
+
+ this.readLocalPartHeader(reader);
+
+ this.fileName = reader.string(this.fileNameLength);
+ this.readExtraFields(reader, this.extraFieldsLength);
+
+ if (!this.hasDataDescriptor())
+ {
+ // easy : we know the file length
+ this.compressedFileData = reader.string(this.compressedSize);
+ }
+ else
+ {
+ // hard way : find the data descriptor manually
+ this.compressedFileData = this.findDataUntilDataDescriptor(reader);
+ this.crc32 = reader.int(4);
+ this.compressedSize = reader.int(this.isZIP64() ? 8 : 4);
+ this.uncompressedSize = reader.int(this.isZIP64() ? 8 : 4);
+
+ if (this.compressedFileData.length !== this.compressedSize)
+ {
+ throw new Error("Bug : data descriptor incorrectly read (size mismatch)");
+ }
+ }
+ this.uncompressedFileData = null;
+
+ compression = findCompression(this.compressionMethod);
+ if (compression === null) // no compression found
+ {
+ throw new Error("Corrupted zip : compression " + pretty(this.compressionMethod) +
+ " unknown (inner file : " + this.fileName + ")");
+ }
+ this.uncompressedFileData = compression.uncompress(this.compressedFileData);
+ };
+
+ /**
+ * Read data until a data descriptor signature is found.
+ * @param {StreamReader} reader the reader to use.
+ */
+ ZipEntry.prototype.findDataUntilDataDescriptor = function(reader)
+ {
+ var data = "",
+ buffer = reader.string(4),
+ byte;
+
+ while(buffer !== JSZip.signature.DATA_DESCRIPTOR)
+ {
+ byte = reader.string(1);
+ data += buffer.slice(0, 1);
+ buffer = (buffer + byte).slice(-4);
+ }
+ return data;
+ };
+ /**
+ * Read the central part of a zip file and add the info in this object.
+ * @param {StreamReader} reader the reader to use.
+ */
+ ZipEntry.prototype.readCentralPart = function(reader)
+ {
+ this.versionMadeBy = reader.string(2);
+
+ this.readLocalPartHeader(reader);
+
+ this.fileCommentLength = reader.int(2);
+ this.diskNumberStart = reader.int(2);
+ this.internalFileAttributes = reader.int(2);
+ this.externalFileAttributes = reader.int(4);
+ this.localHeaderOffset = reader.int(4);
+
+ this.fileName = reader.string(this.fileNameLength);
+ this.readExtraFields(reader, this.extraFieldsLength);
+ this.fileComment = reader.string(this.fileCommentLength);
+
+ // warning, this is true only for zip with madeBy == DOS (plateform dependent feature)
+ this.dir = this.externalFileAttributes & 0x00000010 ? true : false;
+ };
+ /**
+ * Parse the ZIP64 extra field and merge the info in the current ZipEntry.
+ * @param {StreamReader} reader the reader to use.
+ */
+ ZipEntry.prototype.parseZIP64ExtraField = function(reader)
+ {
+ // should be something, preparing the extra reader
+ var extraReader = new StreamReader(this.extraFields[0x0001].value);
+ if(this.uncompressedSize === -1)
+ {
+ this.uncompressedSize = extraReader.int(8);
+ }
+ if(this.compressedSize === -1)
{
- throw new Error("Bug : data descriptor incorrectly read (size mismatch)");
+ this.compressedSize = extraReader.int(8);
}
- }
- this.uncompressedFileData = null;
-
- compression = findCompression(this.compressionMethod);
- if (compression === null) // no compression found
- {
- throw new Error("Corrupted zip : compression " + pretty(this.compressionMethod) +
- " unknown (inner file : " + this.fileName + ")");
- }
- this.uncompressedFileData = compression.uncompress(this.compressedFileData);
- };
-
- /**
- * Read data until a data descriptor signature is found.
- * @param reader the reader to use.
- */
- ZipEntry.prototype.findDataUntilDataDescriptor = function(reader)
- {
- var data = "",
- buffer = reader.string(4),
- byte;
-
- while(buffer !== "\x50\x4b\x07\x08")
- {
- byte = reader.string(1);
- data += buffer.slice(0, 1);
- buffer = (buffer + byte).slice(-4);
- }
- return data;
- };
- /**
- * Read the central part of a zip file and add the info in this object.
- * @param reader the reader to use.
- */
- ZipEntry.prototype.readCentralPart = function(reader)
- {
- this.versionMadeBy = reader.string(2);
-
- this.readLocalPartHeader(reader);
-
- this.fileCommentLength = reader.int(2);
- this.diskNumberStart = reader.int(2);
- this.internalFileAttributes = reader.int(2);
- this.externalFileAttributes = reader.int(4);
- this.localHeaderOffset = reader.int(4);
-
- this.fileName = reader.string(this.fileNameLength);
- this.readExtraFields(reader, this.extraFieldsLength);
- this.fileComment = reader.string(this.fileCommentLength);
-
- // warning, this is true only for zip with madeBy == DOS (plateform dependent feature)
- this.dir = this.externalFileAttributes & 0x00000010 ? true : false;
- };
- /**
- * Read the central part of a zip file and add the info in this object.
- * @param reader the reader to use.
- */
- ZipEntry.prototype.readExtraFields = function(reader, extraFieldsLength)
- {
- var start = reader.index,
- extraFieldId,
- extraFieldLength,
- extraFieldValue;
-
- this.extraFields = this.extraFields || [];
-
- while (reader.index < start + extraFieldsLength)
- {
- extraFieldId = reader.int(2);
- extraFieldLength = reader.int(2);
- extraFieldValue = reader.string(extraFieldLength);
-
- this.extraFields.push({
- id: extraFieldId,
- length: extraFieldLength,
- value: extraFieldValue
- });
- }
- }
-
-
- /**
- * @Class ZipEntries
- * All the entries in the zip file.
- * @param data the binary stream to load.
- */
- function ZipEntries(data)
- {
- this.files = {};
- if (data) this.load(data);
- }
- /**
- * Add a ZipEntry created after a local record.
- * @param zipEntry the zip entry to add.
- * @param offset the number of bytes from the start of this archive.
- */
- ZipEntries.prototype.addLocal = function (zipEntry, offset)
- {
- zipEntry.localHeaderOffset = offset;
- this.files['disk0-offset' + offset] = zipEntry;
- };
- /**
- * Add a ZipEntry created after a central directory record.
- * @param zipEntry the zip entry to add.
- */
- ZipEntries.prototype.addCentral = function(zipEntry)
- {
- var file, i;
- // if we have a central directory recort, the local file header must be already here.
- file = this.files['disk0-offset' + zipEntry.localHeaderOffset];
- if (!file)
- {
- throw new Error("Corrupted zip file : wrong local header offset (" + zipEntry.localHeaderOffset + ")");
- }
- // they contain similiar informations, merging
- for(i in zipEntry)
- {
- file[i] = (typeof file[i] === "undefined" || file[i] === null) ? zipEntry[i] : file[i];
- }
- };
- /**
- * Read the end of the central directory.
- * @param reader the reader to use.
- */
- ZipEntries.prototype.readEndOfCentral = function (reader)
- {
- this.diskNumber = reader.int(2);
- this.diskWithCentralDirStart = reader.int(2);
- this.centralDirRecordsOnThisDisk = reader.int(2);
- this.centralDirRecords = reader.int(2);
- this.centralDirSize = reader.int(4);
- this.centralDirOffset = reader.int(4);
- this.zipCommentLength = reader.int(2);
- this.zipComment = reader.string(this.zipCommentLength);
- };
- /**
- * Read a zip file and create ZipEntries.
- * @param data the binary string representing a zip file.
- */
- ZipEntries.prototype.load = function(data)
- {
- var reader = new StreamReader(data),
- hasMoreFiles = true,
- offset, signature, file, size;
- while(hasMoreFiles)
- {
- offset = reader.index;
- signature = reader.string(4);
- // console.log(pretty(signature));
- switch(signature)
+ if(this.localHeaderOffset === -1)
{
- case JSZip.signature.LOCAL_FILE_HEADER: // local file header signature
- file = new ZipEntry();
- file.readLocalPart(reader);
- this.addLocal(file, offset);
- break;
- case JSZip.signature.CENTRAL_FILE_HEADER: // central file header signature
- file = new ZipEntry();
- file.readCentralPart(reader);
- this.addCentral(file);
- break;
- case JSZip.signature.CENTRAL_DIRECTORY_END: // end of central dir signature
- this.readEndOfCentral(reader);
- break;
- case "\x50\x4b\x06\x08": // archive extra data signature
- throw new Error("Central Directory Encryption Feature not supported");
- case "\x50\x4b\x05\x05": // Digital signature
- // not supported, but don't block the process : discard information
- size = reader.int(2);
- reader.string(size);
- break;
- case "\x50\x4b\x06\x06": // zip64 end of central directory record
- case "\x50\x4b\x06\x07": // zip64 end of central directory locator
- throw new Error("ZIP64 Feature not supported");
- case "\x50\x4b\x07\x08": // data descriptor record
- throw new Error("Data descriptor : unexpected signature");
- break;
- default:
- throw new Error("Corrupted or unsupported zip : " +
- "signature " + pretty(signature) + " unknown");
+ this.localHeaderOffset = extraReader.int(8);
}
- hasMoreFiles = !reader.eof();
- }
- };
-
- /**
- * Implementation of the load method of JSZip.
- * It uses the above classes to decode a zip file, and load every files.
- * @param data the data to load.
- * @param options Options for loading the stream.
- * options.base64 : is the stream in base64 ? default : false
- */
- JSZip.prototype.load = function(data, options)
- {
- var files, zipEntries, i, input;
- options = options || {};
- if(options.base64)
- {
- data = JSZipBase64.decode(data);
- }
-
- zipEntries = new ZipEntries(data);
- files = zipEntries.files;
- for (i in files)
- {
- input = files[i];
- this.file(input.fileName, input.uncompressedFileData, {
- binary:true,
- date:input.date,
- dir:input.dir
- });
- }
-
- return this;
- }
+ if(this.diskNumberStart === -1)
+ {
+ this.diskNumberStart = extraReader.int(4);
+ }
+ };
+ /**
+ * Read the central part of a zip file and add the info in this object.
+ * @param {StreamReader} reader the reader to use.
+ */
+ ZipEntry.prototype.readExtraFields = function(reader, extraFieldsLength)
+ {
+ var start = reader.index,
+ extraFieldId,
+ extraFieldLength,
+ extraFieldValue;
+
+ this.extraFields = this.extraFields || {};
+
+ while (reader.index < start + extraFieldsLength)
+ {
+ extraFieldId = reader.int(2);
+ extraFieldLength = reader.int(2);
+ extraFieldValue = reader.string(extraFieldLength);
+
+ this.extraFields[extraFieldId] = {
+ id: extraFieldId,
+ length: extraFieldLength,
+ value: extraFieldValue
+ };
+ }
+
+ if(this.isZIP64() && this.extraFields[0x0001])
+ {
+ this.parseZIP64ExtraField(reader);
+ }
+ }
+ // }}} end of ZipEntry
+
+ // ZipEntries {{{
+ /**
+ * @Class ZipEntries
+ * All the entries in the zip file.
+ * @param data the binary stream to load.
+ */
+ function ZipEntries(data)
+ {
+ this.files = [];
+ if (data) this.load(data);
+ }
+ /**
+ * Check that the reader is on the speficied signature.
+ * @param {String} expectedSignature the expected signature.
+ * @throws {Error} if it is an other signature.
+ */
+ ZipEntries.prototype.checkSignature = function(expectedSignature)
+ {
+ var signature = this.reader.string(4);
+ if (signature !== expectedSignature)
+ {
+ throw new Error("Corrupted zip or bug : unexpected signature " +
+ "(" + pretty(signature) + ", expected " + pretty(expectedSignature) + ")");
+ }
+ };
+ /**
+ * Read the end of the central directory.
+ */
+ ZipEntries.prototype.readBlockEndOfCentral = function ()
+ {
+ this.diskNumber = this.reader.int(2);
+ this.diskWithCentralDirStart = this.reader.int(2);
+ this.centralDirRecordsOnThisDisk = this.reader.int(2);
+ this.centralDirRecords = this.reader.int(2);
+ this.centralDirSize = this.reader.int(4);
+ this.centralDirOffset = this.reader.int(4);
+
+ this.zipCommentLength = this.reader.int(2);
+ this.zipComment = this.reader.string(this.zipCommentLength);
+ };
+ /**
+ * Read the end of the Zip 64 central directory.
+ * Not merged with the method readEndOfCentral :
+ * The end of central can coexist with its Zip64 brother,
+ * I don't want to read the wrong number of bytes !
+ */
+ ZipEntries.prototype.readBlockZip64EndOfCentral = function ()
+ {
+ this.zip64EndOfCentralSize = this.reader.int(8);
+ this.versionMadeBy = this.reader.string(2);
+ this.versionNeeded = this.reader.int(2);
+ this.diskNumber = this.reader.int(4);
+ this.diskWithCentralDirStart = this.reader.int(4);
+ this.centralDirRecordsOnThisDisk = this.reader.int(8);
+ this.centralDirRecords = this.reader.int(8);
+ this.centralDirSize = this.reader.int(8);
+ this.centralDirOffset = this.reader.int(8);
+
+ this.zip64ExtensibleData = {};
+ var extraDataSize = this.zip64EndOfCentralSize - 44,
+ index = 0,
+ extraFieldId,
+ extraFieldLength,
+ extraFieldValue;
+ while(index < extraDataSize)
+ {
+ extraFieldId = this.reader.int(2);
+ extraFieldLength = this.reader.int(4);
+ extraFieldValue = this.reader.string(extraFieldLength);
+ this.zip64ExtensibleData[extraFieldId] = {
+ id: extraFieldId,
+ length: extraFieldLength,
+ value: extraFieldValue
+ };
+ }
+ };
+ /**
+ * Read the end of the Zip 64 central directory locator.
+ */
+ ZipEntries.prototype.readBlockZip64EndOfCentralLocator = function ()
+ {
+ this.diskWithZip64CentralDirStart = this.reader.int(4);
+ this.relativeOffsetEndOfZip64CentralDir = this.reader.int(8);
+ this.disksCount = this.reader.int(4);
+ if (this.disksCount > 1)
+ {
+ throw new Error("Multi-volumes zip are not supported");
+ }
+ };
+ /**
+ * Read the local files, based on the offset read in the central part.
+ */
+ ZipEntries.prototype.readLocalFiles = function()
+ {
+ for(var i = 0; i < this.files.length; i++)
+ {
+ var file = this.files[i];
+ this.reader.setIndex(file.localHeaderOffset);
+ this.checkSignature(JSZip.signature.LOCAL_FILE_HEADER);
+ file.readLocalPart(this.reader);
+ }
+ };
+ /**
+ * Read the central directory.
+ */
+ ZipEntries.prototype.readCentralDir = function()
+ {
+ this.reader.setIndex(this.centralDirOffset);
+ while(this.reader.string(4) === JSZip.signature.CENTRAL_FILE_HEADER)
+ {
+ var file = new ZipEntry({
+ zip64: this.zip64
+ });
+ file.readCentralPart(this.reader);
+ this.files.push(file);
+ }
+ };
+ /**
+ * Read the end of central directory.
+ */
+ ZipEntries.prototype.readEndOfCentral = function()
+ {
+ // zip 64 ?
+ var offset = this.reader.stream.lastIndexOf(JSZip.signature.ZIP64_CENTRAL_DIRECTORY_LOCATOR);
+ if (offset == -1) // nope
+ {
+ this.zip64 = false;
+ offset = this.reader.stream.lastIndexOf(JSZip.signature.CENTRAL_DIRECTORY_END);
+ if (offset == -1)
+ {
+ throw new Error("Corrupted zip : can't find end of central directory");
+ }
+
+ this.reader.setIndex(offset);
+ this.checkSignature(JSZip.signature.CENTRAL_DIRECTORY_END);
+ this.readBlockEndOfCentral();
+ }
+ else // zip 64 !
+ {
+ this.zip64 = true;
+ this.reader.setIndex(offset);
+ this.checkSignature(JSZip.signature.ZIP64_CENTRAL_DIRECTORY_LOCATOR);
+ this.readBlockZip64EndOfCentralLocator();
+
+ this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir);
+ this.checkSignature(JSZip.signature.ZIP64_CENTRAL_DIRECTORY_END);
+ this.readBlockZip64EndOfCentral();
+ }
+ };
+ /**
+ * Read a zip file and create ZipEntries.
+ * @param {String} data the binary string representing a zip file.
+ */
+ ZipEntries.prototype.load = function(data)
+ {
+ var hasMoreFiles = true,
+ offset, signature, file, size;
+
+ this.reader = new StreamReader(data);
+
+ this.readEndOfCentral();
+ this.readCentralDir();
+ this.readLocalFiles();
+
+ };
+ // }}} end of ZipEntries
+
+ /**
+ * Implementation of the load method of JSZip.
+ * It uses the above classes to decode a zip file, and load every files.
+ * @param {String} data the data to load.
+ * @param {Object} options Options for loading the stream.
+ * options.base64 : is the stream in base64 ? default : false
+ */
+ JSZip.prototype.load = function(data, options)
+ {
+ var files, zipEntries, i, input;
+ options = options || {};
+ if(options.base64)
+ {
+ data = JSZipBase64.decode(data);
+ }
+
+ zipEntries = new ZipEntries(data);
+ files = zipEntries.files;
+ for (i in files)
+ {
+ input = files[i];
+ this.file(input.fileName, input.uncompressedFileData, {
+ binary:true,
+ date:input.date,
+ dir:input.dir
+ });
+ }
+
+ return this;
+ }
})();
+// enforcing Stuk's coding style
+// vim: set shiftwidth=3 softtabstop=3 foldmethod=marker:
6 jszip.js
View
@@ -35,7 +35,10 @@ var JSZip = function(data, options)
JSZip.signature = {
LOCAL_FILE_HEADER : "\x50\x4b\x03\x04",
CENTRAL_FILE_HEADER : "\x50\x4b\x01\x02",
- CENTRAL_DIRECTORY_END : "\x50\x4b\x05\x06"
+ CENTRAL_DIRECTORY_END : "\x50\x4b\x05\x06",
+ ZIP64_CENTRAL_DIRECTORY_LOCATOR : "\x50\x4b\x06\x07",
+ ZIP64_CENTRAL_DIRECTORY_END : "\x50\x4b\x06\x06",
+ DATA_DESCRIPTOR : "\x50\x4b\x07\x08",
};
// Default properties for a new file
@@ -623,4 +626,5 @@ var JSZipBase64 = function() {
};
}();
+// enforcing Stuk's coding style
// vim: set shiftwidth=3 softtabstop=3:
60 test/index.html
View
@@ -161,7 +161,7 @@
ok(zip, "Constructor works");
});
- module("Essential");
+ module("Essential"); // {{{
testZipFile("Zip text file !", "ref/text.zip", function(expected)
{
@@ -367,8 +367,9 @@
equals(root.folder(/^subsub1/).length, 1, "relative folder path is used");
equals(root.folder(/root/).length, 0, "parent folder is not matched");
});
+ // }}} module Essential
- module("More advanced");
+ module("More advanced"); // {{{
testZipFile("Delete file", "ref/text.zip", function(expected)
{
@@ -563,11 +564,13 @@
ok(similar(actual, expected, 18) , "Generated ZIP matches reference ZIP");
});
+ // }}} More advanced
- module("Load file, not supported features");
+ module("Load file, not supported features"); // {{{
// zip -0 -X -e encrypted.zip Hello.txt
- testZipFile("basic encryption", "ref/encrypted.zip", function(file) {
+ testZipFile("basic encryption", "ref/encrypted.zip", function(file)
+ {
try
{
var zip = new JSZip(file);
@@ -578,10 +581,12 @@
equal(e.message, "Encrypted zip are not supported", "the error message is useful");
}
});
+ // }}} Load file, not supported features
- module("Load file, corrupted zip");
+ module("Load file, corrupted zip"); // {{{
- testZipFile("bad compression method", "ref/invalid/compression.zip", function(file) {
+ testZipFile("bad compression method", "ref/invalid/compression.zip", function(file)
+ {
try
{
var zip = new JSZip(file);
@@ -593,7 +598,8 @@
}
});
- testZipFile("bad offset", "ref/invalid/bad_offset.zip", function(file) {
+ testZipFile("bad offset", "ref/invalid/bad_offset.zip", function(file)
+ {
try
{
var zip = new JSZip(file);
@@ -604,39 +610,59 @@
ok(e.message.match("Corrupted zip"), "the error message is useful");
}
});
+ // }}} Load file, corrupted zip
+ module("Load file"); // {{{
- module("Load file");
+ // zip -6 -X deflate.zip Hello.txt
+ testZipFile("zip with DEFLATE", "ref/deflate.zip", function(file)
+ {
+ var zip = new JSZip(file);
+ equal(zip.file("Hello.txt").data, "This a looong file : we need to see the difference between the different compression methods.\n", "the zip was correctly read.");
+ });
// zip -0 -z -c archive_comment.zip Hello.txt
- testZipFile("zip with comment", "ref/archive_comment.zip", function(file) {
+ testZipFile("zip with comment", "ref/archive_comment.zip", function(file)
+ {
var zip = new JSZip(file);
equal(zip.file("Hello.txt").data, "Hello World\n", "the zip was correctly read.");
});
// zip -0 extra_attributes.zip Hello.txt
- testZipFile("zip with extra attributes", "ref/extra_attributes.zip", function(file) {
+ testZipFile("zip with extra attributes", "ref/extra_attributes.zip", function(file)
+ {
var zip = new JSZip(file);
equal(zip.file("Hello.txt").data, "Hello World\n", "the zip was correctly read.");
});
+ // when zip doesn't know the size of the stream, it will be cautious and use
+ // the ZIP64 feature, just in case.
+ // cat file.txt|zip -X -0 > zip64.zip
+ testZipFile("zip 64", "ref/zip64.zip", function(file)
+ {
+ var zip = new JSZip(file);
+ equal(zip.file("-").data, "Hello World\n", "the zip was correctly read.");
+ });
+
// jar produces zip with data descriptors
- // jar c Hello.txt > data_descriptor.zip
- testZipFile("zip with data descriptor", "ref/data_descriptor.zip", function(file) {
+ // jar c0Mvf data_descriptor.zip Hello.txt
+ testZipFile("zip with data descriptor", "ref/data_descriptor.zip", function(file)
+ {
var zip = new JSZip(file);
equal(zip.file("Hello.txt").data, "Hello World\n", "the zip was correctly read.");
});
// zip -0 -X zip_within_zip.zip Hello.txt && zip -0 -X nested.zip Hello.txt zip_within_zip.zip
- testZipFile("nested zip", "ref/nested.zip", function(file) {
+ testZipFile("nested zip", "ref/nested.zip", function(file)
+ {
var zip = new JSZip(file);
equal(zip.file("Hello.txt").data, "Hello World\n", "the zip was correctly read.");
- console.log(zip);
var nested = new JSZip(zip.file("zip_within_zip.zip").data);
equal(nested.file("Hello.txt").data, "Hello World\n", "the inner zip was correctly read.");
});
+ // }}} Load file
- module("Load complex files");
+ module("Load complex files"); // {{{
// http://www.feedbooks.com/book/8/the-metamorphosis
testZipFile("an epub is fine too", "ref/complex_files/Franz Kafka - The Metamorphosis.epub", function(file) {
@@ -652,6 +678,7 @@
ok(zip.file("[Content_Types].xml").data.indexOf("application/vnd.ms-package.xps-fixeddocument+xml") !== -1, "the zip was correctly read.");
});
+ // }}} Load complex files
</script>
</head>
<body>
@@ -662,4 +689,5 @@ <h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
</body>
</html>
-<!-- vim: set shiftwidth=3 softtabstop=3: -->
+<!-- enforcing Stuk's coding style -->
+<!-- vim: set shiftwidth=3 softtabstop=3 foldmethod=marker: -->
BIN  test/ref/data_descriptor.zip
View
Binary file not shown
BIN  test/ref/zip64.zip
View
Binary file not shown
Please sign in to comment.
Something went wrong with that request. Please try again.