Skip to content

Commit

Permalink
Handle prepended data.
Browse files Browse the repository at this point in the history
In some cases (crx files for example), data have been prepended but the
offset didn't change: the previous version read 0 file, didn't check it
and stopped.
This version tries harder to recover from a bad offset. If/when we read
0 file, we will throw an error.

Other tools (unzip, 7zip, etc) handle that case, now JSZip too.

Fix #265.
  • Loading branch information
dduponchel committed Mar 21, 2016
1 parent 585812b commit b0e58c2
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 17 deletions.
7 changes: 4 additions & 3 deletions lib/arrayReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ function ArrayReader(data) {
this.data = data;
this.length = this.data.length;
this.index = 0;
this.zero = 0;

for(var i = 0; i < this.data.length; i++) {
data[i] = data[i] & 0xFF;
Expand All @@ -17,7 +18,7 @@ ArrayReader.prototype = new DataReader();
* @see DataReader.byteAt
*/
ArrayReader.prototype.byteAt = function(i) {
return this.data[i];
return this.data[this.zero + i];
};
/**
* @see DataReader.lastIndexOfSignature
Expand All @@ -29,7 +30,7 @@ ArrayReader.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;
}
}

Expand All @@ -43,7 +44,7 @@ ArrayReader.prototype.readData = function(size) {
if(size === 0) {
return [];
}
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;
};
Expand Down
3 changes: 2 additions & 1 deletion lib/dataReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ function DataReader(data) {
this.data = null; // type : see implementation
this.length = 0;
this.index = 0;
this.zero = 0;
}
DataReader.prototype = {
/**
Expand All @@ -21,7 +22,7 @@ DataReader.prototype = {
* @throws {Error} an Error if the index is out of bounds.
*/
checkIndex: function(newIndex) {
if (this.length < newIndex || newIndex < 0) {
if (this.length < this.zero + newIndex || newIndex < 0) {
throw new Error("End of data reached (data length = " + this.length + ", asked index = " + (newIndex) + "). Corrupted zip ?");
}
},
Expand Down
3 changes: 2 additions & 1 deletion lib/nodeBufferReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
};
Expand Down
7 changes: 4 additions & 3 deletions lib/stringReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,28 @@ 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
*/
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;
};
Expand Down
3 changes: 2 additions & 1 deletion lib/uint8ArrayReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ function Uint8ArrayReader(data) {
this.data = data;
this.length = this.data.length;
this.index = 0;
this.zero = 0;
}
}
Uint8ArrayReader.prototype = new ArrayReader();
Expand All @@ -18,7 +19,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;
};
Expand Down
67 changes: 59 additions & 8 deletions lib/zipEntries.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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.
*/
Expand Down Expand Up @@ -129,24 +143,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 ? " +
Expand All @@ -156,6 +177,7 @@ ZipEntries.prototype = {
}
}
this.reader.setIndex(offset);
var endOfCentralDirOffset = offset;
this.checkSignature(sig.CENTRAL_DIRECTORY_END);
this.readBlockEndOfCentral();

Expand Down Expand Up @@ -184,18 +206,47 @@ 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);
this.checkSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR);
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);
Expand Down
Binary file added test/ref/all_appended_bytes.zip
Binary file not shown.
Binary file added test/ref/all_missing_bytes.zip
Binary file not shown.
Binary file added test/ref/all_prepended_bytes.zip
Binary file not shown.
Binary file added test/ref/zip64_appended_bytes.zip
Binary file not shown.
Binary file added test/ref/zip64_missing_bytes.zip
Binary file not shown.
Binary file added test/ref/zip64_prepended_bytes.zip
Binary file not shown.
44 changes: 44 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,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");
Expand Down Expand Up @@ -1520,6 +1540,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"); // {{{
Expand Down

0 comments on commit b0e58c2

Please sign in to comment.