Skip to content

Commit 825e737

Browse files
committed
Add option to recover corrupted zips
Needs more work to improve reliability, but this is better than nothing. #2
1 parent 50dd4cc commit 825e737

File tree

4 files changed

+123
-3
lines changed

4 files changed

+123
-3
lines changed

lib/zipEntries.js

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,85 @@ ZipEntries.prototype = {
245245
prepareReader: function(data) {
246246
this.reader = readerFor(data);
247247
},
248+
/**
249+
* Attempt to parse a zip without a central directory.
250+
*/
251+
tryRecoverCorruptedZip: function() {
252+
// Undo anything done by the central directory reader
253+
this.reader.setIndex(0);
254+
this.files = [];
255+
256+
// Zip comment is in central directory, so we have no comment
257+
this.zipCommentLength = 0;
258+
this.zipComment = [];
259+
260+
let possibleHeaders = 0;
261+
262+
// Without central directory, have to just search for file entry magic bytes
263+
const length = this.reader.length;
264+
for (let i = 0; i < length - 4; i++) {
265+
if (
266+
this.reader.byteAt(i) === 0x50 &&
267+
this.reader.byteAt(i + 1) === 0x4B &&
268+
this.reader.byteAt(i + 2) === 0x03 &&
269+
this.reader.byteAt(i + 3) === 0x04
270+
) {
271+
possibleHeaders++;
272+
const zipEntry = this.tryRecoverFileEntry(i);
273+
if (zipEntry) {
274+
this.files.push(zipEntry);
275+
}
276+
}
277+
}
278+
279+
if (this.files.length === 0) {
280+
if (possibleHeaders === 0) {
281+
throw new Error("Corrupted zip: no central directory or any file headers");
282+
}
283+
throw new Error(`Corrupted zip: no central directory, and ${possibleHeaders} possible local headers could not be recovered`);
284+
}
285+
},
286+
/**
287+
* Attempts to read a zip entry from only the local header.
288+
* @param {number} index Index in this.reader where a file header appears to start
289+
* @returns {ZipEntry|null} The zip entry if a valid one could be parsed, otherwise null.
290+
*/
291+
tryRecoverFileEntry: function(index) {
292+
this.reader.setIndex(index);
293+
294+
const zipEntry = new ZipEntry({
295+
zip64: this.zip64
296+
}, this.loadOptions);
297+
298+
try {
299+
zipEntry.readLocalPartFromCorruptedZip(this.reader);
300+
zipEntry.handleUTF8();
301+
zipEntry.processAttributes();
302+
return zipEntry;
303+
} catch (e) {
304+
// Entry is invalid.
305+
console.error(e);
306+
return null;
307+
}
308+
},
248309
/**
249310
* Read a zip file and create ZipEntries.
250311
* @param {String|ArrayBuffer|Uint8Array|Buffer} data the binary string representing a zip file.
251312
*/
252313
load: function(data) {
253314
this.prepareReader(data);
254-
this.readEndOfCentral();
255-
this.readCentralDir();
256-
this.readLocalFiles();
315+
316+
try {
317+
this.readEndOfCentral();
318+
this.readCentralDir();
319+
this.readLocalFiles();
320+
} catch (error) {
321+
if (this.loadOptions.recoverCorrupted) {
322+
this.tryRecoverCorruptedZip();
323+
} else {
324+
throw error;
325+
}
326+
}
257327
}
258328
};
259329
// }}} end of ZipEntries

lib/zipEntry.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var crc32fn = require("./crc32");
66
var utf8 = require("./utf8");
77
var compressions = require("./compressions");
88
var support = require("./support");
9+
var sig = require("./signature");
910

1011
var MADE_BY_DOS = 0x00;
1112
var MADE_BY_UNIX = 0x03;
@@ -129,6 +130,36 @@ ZipEntry.prototype = {
129130
this.fileComment = reader.readData(this.fileCommentLength);
130131
},
131132

133+
/**
134+
* Reads the local part of a zip file. Used when the central part of the zip is missing.
135+
* @param {DataReader} reader Reader pointing to the start of local part header signature
136+
* @throws if the header is invalid
137+
*/
138+
readLocalPartFromCorruptedZip: function(reader) {
139+
// Read everything in the possible header
140+
reader.readAndCheckSignature(sig.LOCAL_FILE_HEADER);
141+
this.versionMadeBy = reader.readInt(2);
142+
this.bitFlag = reader.readInt(2);
143+
this.compressionMethod = reader.readString(2);
144+
this.date = reader.readDate();
145+
this.crc32 = reader.readInt(4);
146+
this.compressedSize = reader.readInt(4);
147+
this.uncompressedSize = reader.readInt(4);
148+
this.fileNameLength = reader.readInt(2);
149+
this.extraFieldsLength = reader.readInt(2);
150+
this.fileName = reader.readData(this.fileNameLength);
151+
this.readExtraFields(reader);
152+
this.parseZIP64ExtraField(reader);
153+
154+
// TODO: more checks to verify that the header makes sense
155+
156+
var compression = findCompression(this.compressionMethod);
157+
if (compression === null) {
158+
throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")");
159+
}
160+
this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize));
161+
},
162+
132163
/**
133164
* Parse the external file attributes and get the unix/dos permissions.
134165
*/

test/asserts/corruption.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use strict";
2+
3+
QUnit.module("corruption", function () {
4+
JSZipTestUtils.testZipFile("load(string) works", "ref/no-central-directory/truncated.zip", function(assert, file) {
5+
var done = assert.async();
6+
JSZip.loadAsync(file, {
7+
recoverCorrupted: true
8+
})
9+
.then(function (zip) {
10+
return zip.file("project.json").async("string");
11+
})
12+
.then(function(result) {
13+
const parsed = JSON.parse(result);
14+
assert.equal(parsed.targets[0].name, "Stage");
15+
done();
16+
})
17+
.catch(JSZipTestUtils.assertNoError);
18+
});
19+
});
2.14 KB
Binary file not shown.

0 commit comments

Comments
 (0)