zipEntries.js
12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
'use strict';
var StringReader = require('./stringReader');
var NodeBufferReader = require('./nodeBufferReader');
var Uint8ArrayReader = require('./uint8ArrayReader');
var ArrayReader = require('./arrayReader');
var utils = require('./utils');
var sig = require('./signature');
var ZipEntry = require('./zipEntry');
var support = require('./support');
var jszipProto = require('./object');
// class ZipEntries {{{
/**
* All the entries in the zip file.
* @constructor
* @param {String|ArrayBuffer|Uint8Array} data the binary stream to load.
* @param {Object} loadOptions Options for loading the stream.
*/
function ZipEntries(data, loadOptions) {
this.files = [];
this.loadOptions = loadOptions;
if (data) {
this.load(data);
}
}
ZipEntries.prototype = {
/**
* Check that the reader is on the speficied signature.
* @param {string} expectedSignature the expected signature.
* @throws {Error} if it is an other signature.
*/
checkSignature: function(expectedSignature) {
var signature = this.reader.readString(4);
if (signature !== expectedSignature) {
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.
*/
readBlockEndOfCentral: function() {
this.diskNumber = this.reader.readInt(2);
this.diskWithCentralDirStart = this.reader.readInt(2);
this.centralDirRecordsOnThisDisk = this.reader.readInt(2);
this.centralDirRecords = this.reader.readInt(2);
this.centralDirSize = this.reader.readInt(4);
this.centralDirOffset = this.reader.readInt(4);
this.zipCommentLength = this.reader.readInt(2);
// warning : the encoding depends of the system locale
// On a linux machine with LANG=en_US.utf8, this field is utf8 encoded.
// On a windows machine, this field is encoded with the localized windows code page.
var zipComment = this.reader.readData(this.zipCommentLength);
var decodeParamType = support.uint8array ? "uint8array" : "array";
// To get consistent behavior with the generation part, we will assume that
// this is utf8 encoded unless specified otherwise.
var decodeContent = utils.transformTo(decodeParamType, zipComment);
this.zipComment = this.loadOptions.decodeFileName(decodeContent);
},
/**
* 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 !
*/
readBlockZip64EndOfCentral: function() {
this.zip64EndOfCentralSize = this.reader.readInt(8);
this.versionMadeBy = this.reader.readString(2);
this.versionNeeded = this.reader.readInt(2);
this.diskNumber = this.reader.readInt(4);
this.diskWithCentralDirStart = this.reader.readInt(4);
this.centralDirRecordsOnThisDisk = this.reader.readInt(8);
this.centralDirRecords = this.reader.readInt(8);
this.centralDirSize = this.reader.readInt(8);
this.centralDirOffset = this.reader.readInt(8);
this.zip64ExtensibleData = {};
var extraDataSize = this.zip64EndOfCentralSize - 44,
index = 0,
extraFieldId,
extraFieldLength,
extraFieldValue;
while (index < extraDataSize) {
extraFieldId = this.reader.readInt(2);
extraFieldLength = this.reader.readInt(4);
extraFieldValue = this.reader.readString(extraFieldLength);
this.zip64ExtensibleData[extraFieldId] = {
id: extraFieldId,
length: extraFieldLength,
value: extraFieldValue
};
}
},
/**
* Read the end of the Zip 64 central directory locator.
*/
readBlockZip64EndOfCentralLocator: function() {
this.diskWithZip64CentralDirStart = this.reader.readInt(4);
this.relativeOffsetEndOfZip64CentralDir = this.reader.readInt(8);
this.disksCount = this.reader.readInt(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.
*/
readLocalFiles: function() {
var i, file;
for (i = 0; i < this.files.length; i++) {
file = this.files[i];
this.reader.setIndex(file.localHeaderOffset);
this.checkSignature(sig.LOCAL_FILE_HEADER);
file.readLocalPart(this.reader);
file.handleUTF8();
file.processAttributes();
}
},
/**
* Read the central directory.
*/
readCentralDir: function() {
var file;
this.reader.setIndex(this.centralDirOffset);
while (this.reader.readString(4) === sig.CENTRAL_FILE_HEADER) {
file = new ZipEntry({
zip64: this.zip64
}, this.loadOptions);
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 < 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 = !this.isSignature(0, sig.LOCAL_FILE_HEADER);
if (isGarbage) {
throw new Error("Can't find end of central directory : is this a zip file ? " +
"If it is, see http://stuk.github.io/jszip/documentation/howto/read_zip.html");
} else {
throw new Error("Corrupted zip : can't find end of central directory");
}
}
this.reader.setIndex(offset);
var endOfCentralDirOffset = offset;
this.checkSignature(sig.CENTRAL_DIRECTORY_END);
this.readBlockEndOfCentral();
/* extract from the zip spec :
4) If one of the fields in the end of central directory
record is too small to hold required data, the field
should be set to -1 (0xFFFF or 0xFFFFFFFF) and the
ZIP64 format record should be created.
5) The end of central directory record and the
Zip64 end of central directory locator record must
reside on the same disk when splitting or spanning
an archive.
*/
if (this.diskNumber === utils.MAX_VALUE_16BITS || this.diskWithCentralDirStart === utils.MAX_VALUE_16BITS || this.centralDirRecordsOnThisDisk === utils.MAX_VALUE_16BITS || this.centralDirRecords === utils.MAX_VALUE_16BITS || this.centralDirSize === utils.MAX_VALUE_32BITS || this.centralDirOffset === utils.MAX_VALUE_32BITS) {
this.zip64 = true;
/*
Warning : the zip64 extension is supported, but ONLY if the 64bits integer read from
the zip file can fit into a 32bits integer. This cannot be solved : Javascript represents
all numbers as 64-bit double precision IEEE 754 floating point numbers.
So, we have 53bits for integers and bitwise operations treat everything as 32bits.
see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators
and http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf section 8.5
*/
// should look for a zip64 EOCD locator
offset = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR);
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);
utils.checkSupport(type);
if (type === "string" && !support.uint8array) {
this.reader = new StringReader(data, this.loadOptions.optimizedBinaryString);
}
else if (type === "nodebuffer") {
this.reader = new NodeBufferReader(data);
}
else if (support.uint8array) {
this.reader = new Uint8ArrayReader(utils.transformTo("uint8array", data));
} else if (support.array) {
this.reader = new ArrayReader(utils.transformTo("array", data));
} else {
throw new Error("Unexpected error: unsupported type '" + type + "'");
}
},
/**
* Read a zip file and create ZipEntries.
* @param {String|ArrayBuffer|Uint8Array|Buffer} data the binary string representing a zip file.
*/
load: function(data) {
this.prepareReader(data);
this.readEndOfCentral();
this.readCentralDir();
this.readLocalFiles();
}
};
// }}} end of ZipEntries
module.exports = ZipEntries;