diff --git a/lib/dataReader.js b/lib/dataReader.js index 22e8cb38..5f0130b7 100644 --- a/lib/dataReader.js +++ b/lib/dataReader.js @@ -5,6 +5,7 @@ function DataReader(data) { this.data = null; // type : see implementation this.length = 0; this.index = 0; + this.zero = 0; } DataReader.prototype = { /** @@ -13,7 +14,7 @@ DataReader.prototype = { * @throws {Error} an Error if the offset is out of bounds. */ checkOffset: function(offset) { - this.checkIndex(this.index + offset); + this.checkIndex(this.zero + this.index + offset); }, /** * Check that the specifed index will not be too far. diff --git a/lib/nodeBufferReader.js b/lib/nodeBufferReader.js index 223bd0b9..b603d5a9 100644 --- a/lib/nodeBufferReader.js +++ b/lib/nodeBufferReader.js @@ -5,6 +5,7 @@ function NodeBufferReader(data) { this.data = data; this.length = this.data.length; this.index = 0; + this.zero = 0; } NodeBufferReader.prototype = new Uint8ArrayReader(); @@ -13,7 +14,7 @@ NodeBufferReader.prototype = new Uint8ArrayReader(); */ NodeBufferReader.prototype.readData = function(size) { this.checkOffset(size); - var result = this.data.slice(this.index, this.index + size); + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); this.index += size; return result; }; diff --git a/lib/stringReader.js b/lib/stringReader.js index 895331e4..757d53e2 100644 --- a/lib/stringReader.js +++ b/lib/stringReader.js @@ -9,19 +9,20 @@ function StringReader(data, optimizedBinaryString) { } this.length = this.data.length; this.index = 0; + this.zero = 0; } StringReader.prototype = new DataReader(); /** * @see DataReader.byteAt */ StringReader.prototype.byteAt = function(i) { - return this.data.charCodeAt(i); + return this.data.charCodeAt(this.zero + i); }; /** * @see DataReader.lastIndexOfSignature */ StringReader.prototype.lastIndexOfSignature = function(sig) { - return this.data.lastIndexOf(sig); + return this.data.lastIndexOf(sig) - this.zero; }; /** * @see DataReader.readData @@ -29,7 +30,7 @@ StringReader.prototype.lastIndexOfSignature = function(sig) { StringReader.prototype.readData = function(size) { this.checkOffset(size); // this will work because the constructor applied the "& 0xff" mask. - var result = this.data.slice(this.index, this.index + size); + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); this.index += size; return result; }; diff --git a/lib/uint8ArrayReader.js b/lib/uint8ArrayReader.js index ce8d1a8b..fd4be09f 100644 --- a/lib/uint8ArrayReader.js +++ b/lib/uint8ArrayReader.js @@ -6,6 +6,7 @@ function Uint8ArrayReader(data) { this.data = data; this.length = this.data.length; this.index = 0; + this.zero = 0; } } Uint8ArrayReader.prototype = new DataReader(); @@ -13,7 +14,7 @@ Uint8ArrayReader.prototype = new DataReader(); * @see DataReader.byteAt */ Uint8ArrayReader.prototype.byteAt = function(i) { - return this.data[i]; + return this.data[this.zero + i]; }; /** * @see DataReader.lastIndexOfSignature @@ -25,7 +26,7 @@ Uint8ArrayReader.prototype.lastIndexOfSignature = function(sig) { sig3 = sig.charCodeAt(3); for (var i = this.length - 4; i >= 0; --i) { if (this.data[i] === sig0 && this.data[i + 1] === sig1 && this.data[i + 2] === sig2 && this.data[i + 3] === sig3) { - return i; + return i - this.zero; } } @@ -40,7 +41,7 @@ Uint8ArrayReader.prototype.readData = function(size) { // in IE10, when using subarray(idx, idx), we get the array [0x00] instead of []. return new Uint8Array(0); } - var result = this.data.subarray(this.index, this.index + size); + var result = this.data.subarray(this.zero + this.index, this.zero + this.index + size); this.index += size; return result; }; diff --git a/lib/zipEntries.js b/lib/zipEntries.js index 4b825613..27a4063a 100644 --- a/lib/zipEntries.js +++ b/lib/zipEntries.js @@ -33,6 +33,20 @@ ZipEntries.prototype = { throw new Error("Corrupted zip or bug : unexpected signature " + "(" + utils.pretty(signature) + ", expected " + utils.pretty(expectedSignature) + ")"); } }, + /** + * Check if the given signature is at the given index. + * @param {number} askedIndex the index to check. + * @param {string} expectedSignature the signature to expect. + * @return {boolean} true if the signature is here, false otherwise. + */ + isSignature: function(askedIndex, expectedSignature) { + var currentIndex = this.reader.index; + this.reader.setIndex(askedIndex); + var signature = this.reader.readString(4); + var result = signature === expectedSignature; + this.reader.setIndex(currentIndex); + return result; + }, /** * Read the end of the central directory. */ @@ -126,24 +140,31 @@ ZipEntries.prototype = { file.readCentralPart(this.reader); this.files.push(file); } + + if (this.centralDirRecords !== this.files.length) { + if (this.centralDirRecords !== 0 && this.files.length === 0) { + // We expected some records but couldn't find ANY. + // This is really suspicious, as if something went wrong. + throw new Error("Corrupted zip or bug: expected " + this.centralDirRecords + " records in central dir, got " + this.files.length); + } else { + // We found some records but not all. + // Something is wrong but we got something for the user: no error here. + // console.warn("expected", this.centralDirRecords, "records in central dir, got", this.files.length); + } + } }, /** * Read the end of central directory. */ readEndOfCentral: function() { var offset = this.reader.lastIndexOfSignature(sig.CENTRAL_DIRECTORY_END); - if (offset === -1) { + if (offset < 0) { // Check if the content is a truncated zip or complete garbage. // A "LOCAL_FILE_HEADER" is not required at the beginning (auto // extractible zip for example) but it can give a good hint. // If an ajax request was used without responseType, we will also // get unreadable data. - var isGarbage = true; - try { - this.reader.setIndex(0); - this.checkSignature(sig.LOCAL_FILE_HEADER); - isGarbage = false; - } catch (e) {} + var isGarbage = !this.isSignature(0, sig.LOCAL_FILE_HEADER); if (isGarbage) { throw new Error("Can't find end of central directory : is this a zip file ? " + @@ -153,6 +174,7 @@ ZipEntries.prototype = { } } this.reader.setIndex(offset); + var endOfCentralDirOffset = offset; this.checkSignature(sig.CENTRAL_DIRECTORY_END); this.readBlockEndOfCentral(); @@ -181,7 +203,7 @@ ZipEntries.prototype = { // should look for a zip64 EOCD locator offset = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR); - if (offset === -1) { + if (offset < 0) { throw new Error("Corrupted zip : can't find the ZIP64 end of central directory locator"); } this.reader.setIndex(offset); @@ -189,10 +211,39 @@ ZipEntries.prototype = { this.readBlockZip64EndOfCentralLocator(); // now the zip64 EOCD record + if (!this.isSignature(this.relativeOffsetEndOfZip64CentralDir, sig.ZIP64_CENTRAL_DIRECTORY_END)) { + // console.warn("ZIP64 end of central directory not where expected."); + this.relativeOffsetEndOfZip64CentralDir = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_END); + if (this.relativeOffsetEndOfZip64CentralDir < 0) { + throw new Error("Corrupted zip : can't find the ZIP64 end of central directory"); + } + } this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir); this.checkSignature(sig.ZIP64_CENTRAL_DIRECTORY_END); this.readBlockZip64EndOfCentral(); } + + var expectedEndOfCentralDirOffset = this.centralDirOffset + this.centralDirSize; + if (this.zip64) { + expectedEndOfCentralDirOffset += 20; // end of central dir 64 locator + expectedEndOfCentralDirOffset += 12 /* should not include the leading 12 bytes */ + this.zip64EndOfCentralSize; + } + + var extraBytes = endOfCentralDirOffset - expectedEndOfCentralDirOffset; + + if (extraBytes > 0) { + // console.warn(extraBytes, "extra bytes at beginning or within zipfile"); + if (this.isSignature(endOfCentralDirOffset, sig.CENTRAL_FILE_HEADER)) { + // The offsets seem wrong, but we have something at the specified offset. + // So… we keep it. + } else { + // the offset is wrong, update the "zero" of the reader + // this happens if data has been prepended (crx files for example) + this.reader.zero = extraBytes; + } + } else if (extraBytes < 0) { + throw new Error("Corrupted zip: missing " + Math.abs(extraBytes) + " bytes."); + } }, prepareReader: function(data) { var type = utils.getTypeOf(data); diff --git a/test/ref/all_appended_bytes.zip b/test/ref/all_appended_bytes.zip new file mode 100644 index 00000000..3bc12917 Binary files /dev/null and b/test/ref/all_appended_bytes.zip differ diff --git a/test/ref/all_missing_bytes.zip b/test/ref/all_missing_bytes.zip new file mode 100644 index 00000000..f562db21 Binary files /dev/null and b/test/ref/all_missing_bytes.zip differ diff --git a/test/ref/all_prepended_bytes.zip b/test/ref/all_prepended_bytes.zip new file mode 100644 index 00000000..4887644c Binary files /dev/null and b/test/ref/all_prepended_bytes.zip differ diff --git a/test/ref/zip64_appended_bytes.zip b/test/ref/zip64_appended_bytes.zip new file mode 100644 index 00000000..f4de49d4 Binary files /dev/null and b/test/ref/zip64_appended_bytes.zip differ diff --git a/test/ref/zip64_missing_bytes.zip b/test/ref/zip64_missing_bytes.zip new file mode 100644 index 00000000..31a9c779 Binary files /dev/null and b/test/ref/zip64_missing_bytes.zip differ diff --git a/test/ref/zip64_prepended_bytes.zip b/test/ref/zip64_prepended_bytes.zip new file mode 100644 index 00000000..15b1bec4 Binary files /dev/null and b/test/ref/zip64_prepended_bytes.zip differ diff --git a/test/test.js b/test/test.js index 0eb6b9aa..3f4f0b4e 100644 --- a/test/test.js +++ b/test/test.js @@ -1087,6 +1087,26 @@ test("truncated zip file", function() { } }); +// dd if=all.zip of=all_missing_bytes.zip bs=32 skip=1 +testZipFile("zip file with missing bytes", "ref/all_missing_bytes.zip", function(file) { + try { + var zip = new JSZip(file); + ok(false, "no exception were thrown"); + } catch(e) { + ok(e.message.match("Corrupted zip"), "the error message is useful"); + } +}); + +// dd if=zip64.zip of=zip64_missing_bytes.zip bs=32 skip=1 +testZipFile("zip64 file with missing bytes", "ref/zip64_missing_bytes.zip", function(file) { + try { + var zip = new JSZip(file); + ok(false, "no exception were thrown"); + } catch(e) { + ok(e.message.match("Corrupted zip"), "the error message is useful"); + } +}); + test("not a zip file", function() { try { var zip = new JSZip("I'm not a zip file"); @@ -1458,6 +1478,30 @@ testZipFile("permissions on windows : file created by izarc, reloaded", "ref/per testZipFile("permissions on windows : file created by winrar", "ref/permissions/windows_winrar.zip", assertDosPermissions); testZipFile("permissions on windows : file created by winrar, reloaded", "ref/permissions/windows_winrar.zip", reloadAndAssertDosPermissions); +// cat Hello.txt all.zip > all_prepended_bytes.zip +testZipFile("zip file with prepended bytes", "ref/all_prepended_bytes.zip", function(file) { + var zip = new JSZip(file); + equal(zip.file("Hello.txt").asText(), "Hello World\n", "the zip was correctly read."); +}); + +// cat all.zip Hello.txt > all_appended_bytes.zip +testZipFile("zip file with appended bytes", "ref/all_appended_bytes.zip", function(file) { + var zip = new JSZip(file); + equal(zip.file("Hello.txt").asText(), "Hello World\n", "the zip was correctly read."); +}); + +// cat Hello.txt zip64.zip > zip64_prepended_bytes.zip +testZipFile("zip64 file with extra bytes", "ref/zip64_prepended_bytes.zip", function(file) { + var zip = new JSZip(file); + equal(zip.file("Hello.txt").asText(), "Hello World\n", "the zip was correctly read."); +}); + +// cat zip64.zip Hello.txt > zip64_appended_bytes.zip +testZipFile("zip64 file with extra bytes", "ref/zip64_appended_bytes.zip", function(file) { + var zip = new JSZip(file); + equal(zip.file("Hello.txt").asText(), "Hello World\n", "the zip was correctly read."); +}); + // }}} Load file QUnit.module("Load complex files"); // {{{