mirror of
https://github.com/flynx/PortableMag.git
synced 2025-10-28 10:40:07 +00:00
546 lines
21 KiB
JavaScript
Executable File
546 lines
21 KiB
JavaScript
Executable File
/**
|
|
|
|
JSZip - A Javascript class for generating and reading zip files
|
|
<http://stuartk.com/jszip>
|
|
|
|
(c) 2011 David Duponchel <d.duponchel@gmail.com>
|
|
Dual licenced under the MIT license or GPLv3. See LICENSE.markdown.
|
|
|
|
**/
|
|
/*global JSZip,JSZipBase64 */
|
|
(function () {
|
|
|
|
var MAX_VALUE_16BITS = 65535;
|
|
var MAX_VALUE_32BITS = -1; // well, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" is parsed as -1
|
|
|
|
/**
|
|
* 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++) {
|
|
code = str.charCodeAt(i);
|
|
res += '\\x' + (code < 16 ? "0" : "") + code.toString(16).toUpperCase();
|
|
}
|
|
return res;
|
|
};
|
|
|
|
/**
|
|
* Find a compression registered in JSZip.
|
|
* @param {string} compressionMethod the method magic to find.
|
|
* @return {Object|null} the JSZip compression object, null if none found.
|
|
*/
|
|
var findCompression = function (compressionMethod) {
|
|
for (var method in JSZip.compressions) {
|
|
if( !JSZip.compressions.hasOwnProperty(method) ) { continue; }
|
|
if (JSZip.compressions[method].magic === compressionMethod) {
|
|
return JSZip.compressions[method];
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// class StreamReader {{{
|
|
/**
|
|
* Read bytes from a stream.
|
|
* Developer tip : when debugging, a watch on pretty(this.reader.stream.slice(this.reader.index))
|
|
* is very useful :)
|
|
* @constructor
|
|
* @param {String|ArrayBuffer|Uint8Array} stream the stream to read.
|
|
*/
|
|
function StreamReader(stream) {
|
|
this.stream = "";
|
|
if (JSZip.support.uint8array && stream instanceof Uint8Array) {
|
|
this.stream = JSZip.utils.uint8Array2String(stream);
|
|
} else if (JSZip.support.arraybuffer && stream instanceof ArrayBuffer) {
|
|
var bufferView = new Uint8Array(stream);
|
|
this.stream = JSZip.utils.uint8Array2String(bufferView);
|
|
} else {
|
|
this.stream = JSZip.utils.string2binary(stream);
|
|
}
|
|
this.index = 0;
|
|
}
|
|
StreamReader.prototype = {
|
|
/**
|
|
* 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.
|
|
*/
|
|
checkOffset : function (offset) {
|
|
this.checkIndex(this.index + offset);
|
|
},
|
|
/**
|
|
* Check that the specifed index will not be too far.
|
|
* @param {string} newIndex the index to check.
|
|
* @throws {Error} an Error if the index is out of bounds.
|
|
*/
|
|
checkIndex : function (newIndex) {
|
|
if (this.stream.length < newIndex || newIndex < 0) {
|
|
throw new Error("End of stream reached (stream length = " +
|
|
this.stream.length + ", asked index = " +
|
|
(newIndex) + "). Corrupted zip ?");
|
|
}
|
|
},
|
|
/**
|
|
* Change the index.
|
|
* @param {number} newIndex The new index.
|
|
* @throws {Error} if the new index is out of the stream.
|
|
*/
|
|
setIndex : function (newIndex) {
|
|
this.checkIndex(newIndex);
|
|
this.index = newIndex;
|
|
},
|
|
/**
|
|
* Skip the next n bytes.
|
|
* @param {number} n the number of bytes to skip.
|
|
* @throws {Error} if the new index is out of the stream.
|
|
*/
|
|
skip : function (n) {
|
|
this.setIndex(this.index + n);
|
|
},
|
|
/**
|
|
* Get the byte at the specified index.
|
|
* @param {number} i the index to use.
|
|
* @return {number} a byte.
|
|
*/
|
|
byteAt : function(i) {
|
|
return this.stream.charCodeAt(i);
|
|
},
|
|
/**
|
|
* Get the next number with a given byte size.
|
|
* @param {number} size the number of bytes to read.
|
|
* @return {number} the corresponding number.
|
|
*/
|
|
readInt : function (size) {
|
|
var result = 0, i;
|
|
this.checkOffset(size);
|
|
for(i = this.index + size - 1; i >= this.index; i--) {
|
|
result = (result << 8) + this.byteAt(i);
|
|
}
|
|
this.index += size;
|
|
return result;
|
|
},
|
|
/**
|
|
* Get the next string with a given byte size.
|
|
* @param {number} size the number of bytes to read.
|
|
* @return {string} the corresponding string.
|
|
*/
|
|
readString : function (size) {
|
|
this.checkOffset(size);
|
|
// this will work because the constructor applied the "& 0xff" mask.
|
|
var result = this.stream.slice(this.index, this.index + size);
|
|
this.index += size;
|
|
return result;
|
|
},
|
|
/**
|
|
* Get the next date.
|
|
* @return {Date} the date.
|
|
*/
|
|
readDate : function () {
|
|
var dostime = this.readInt(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
|
|
|
|
// class ZipEntry {{{
|
|
/**
|
|
* An entry in the zip file.
|
|
* @constructor
|
|
* @param {Object} options Options of the current file.
|
|
* @param {Object} loadOptions Options for loading the stream.
|
|
*/
|
|
function ZipEntry(options, loadOptions) {
|
|
this.options = options;
|
|
this.loadOptions = loadOptions;
|
|
}
|
|
ZipEntry.prototype = {
|
|
/**
|
|
* say if the file is encrypted.
|
|
* @return {boolean} true if the file is encrypted, false otherwise.
|
|
*/
|
|
isEncrypted : function () {
|
|
// bit 1 is set
|
|
return (this.bitFlag & 0x0001) === 0x0001;
|
|
},
|
|
/**
|
|
* say if the file has utf-8 filename/comment.
|
|
* @return {boolean} true if the filename/comment is in utf-8, false otherwise.
|
|
*/
|
|
useUTF8 : function () {
|
|
// bit 11 is set
|
|
return (this.bitFlag & 0x0800) === 0x0800;
|
|
},
|
|
/**
|
|
* Read the local part of a zip file and add the info in this object.
|
|
* @param {StreamReader} reader the reader to use.
|
|
*/
|
|
readLocalPart : function(reader) {
|
|
var compression, localExtraFieldsLength;
|
|
|
|
// we already know everything from the central dir !
|
|
// If the central dir data are false, we are doomed.
|
|
// On the bright side, the local part is scary : zip64, data descriptors, both, etc.
|
|
// The less data we get here, the more reliable this should be.
|
|
// Let's skip the whole header and dash to the data !
|
|
reader.skip(22);
|
|
// in some zip created on windows, the filename stored in the central dir contains \ instead of /.
|
|
// Strangely, the filename here is OK.
|
|
// I would love to treat these zip files as corrupted (see http://www.info-zip.org/FAQ.html#backslashes
|
|
// or APPNOTE#4.4.17.1, "All slashes MUST be forward slashes '/'") but there are a lot of bad zip generators...
|
|
// Search "unzip mismatching "local" filename continuing with "central" filename version" on
|
|
// the internet.
|
|
//
|
|
// I think I see the logic here : the central directory is used to display
|
|
// content and the local directory is used to extract the files. Mixing / and \
|
|
// may be used to display \ to windows users and use / when extracting the files.
|
|
// Unfortunately, this lead also to some issues : http://seclists.org/fulldisclosure/2009/Sep/394
|
|
this.fileNameLength = reader.readInt(2);
|
|
localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir
|
|
this.fileName = reader.readString(this.fileNameLength);
|
|
reader.skip(localExtraFieldsLength);
|
|
|
|
if (this.compressedSize == -1 || this.uncompressedSize == -1) {
|
|
throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory " +
|
|
"(compressedSize == -1 || uncompressedSize == -1)");
|
|
}
|
|
this.compressedFileData = reader.readString(this.compressedSize);
|
|
|
|
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);
|
|
|
|
if (this.uncompressedFileData.length !== this.uncompressedSize) {
|
|
throw new Error("Bug : uncompressed data size mismatch");
|
|
}
|
|
|
|
if (this.loadOptions.checkCRC32 && JSZip.prototype.crc32(this.uncompressedFileData) !== this.crc32) {
|
|
throw new Error("Corrupted zip : CRC32 mismatch");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Read the central part of a zip file and add the info in this object.
|
|
* @param {StreamReader} reader the reader to use.
|
|
*/
|
|
readCentralPart : function(reader) {
|
|
this.versionMadeBy = reader.readString(2);
|
|
this.versionNeeded = reader.readInt(2);
|
|
this.bitFlag = reader.readInt(2);
|
|
this.compressionMethod = reader.readString(2);
|
|
this.date = reader.readDate();
|
|
this.crc32 = reader.readInt(4);
|
|
this.compressedSize = reader.readInt(4);
|
|
this.uncompressedSize = reader.readInt(4);
|
|
this.fileNameLength = reader.readInt(2);
|
|
this.extraFieldsLength = reader.readInt(2);
|
|
this.fileCommentLength = reader.readInt(2);
|
|
this.diskNumberStart = reader.readInt(2);
|
|
this.internalFileAttributes = reader.readInt(2);
|
|
this.externalFileAttributes = reader.readInt(4);
|
|
this.localHeaderOffset = reader.readInt(4);
|
|
|
|
if (this.isEncrypted()) {
|
|
throw new Error("Encrypted zip are not supported");
|
|
}
|
|
|
|
this.fileName = reader.readString(this.fileNameLength);
|
|
this.readExtraFields(reader);
|
|
this.parseZIP64ExtraField(reader);
|
|
this.fileComment = reader.readString(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.
|
|
*/
|
|
parseZIP64ExtraField : function(reader) {
|
|
|
|
if(!this.extraFields[0x0001]) {
|
|
return;
|
|
}
|
|
|
|
// should be something, preparing the extra reader
|
|
var extraReader = new StreamReader(this.extraFields[0x0001].value);
|
|
|
|
// I really hope that these 64bits integer can fit in 32 bits integer, because js
|
|
// won't let us have more.
|
|
if(this.uncompressedSize === MAX_VALUE_32BITS) {
|
|
this.uncompressedSize = extraReader.readInt(8);
|
|
}
|
|
if(this.compressedSize === MAX_VALUE_32BITS) {
|
|
this.compressedSize = extraReader.readInt(8);
|
|
}
|
|
if(this.localHeaderOffset === MAX_VALUE_32BITS) {
|
|
this.localHeaderOffset = extraReader.readInt(8);
|
|
}
|
|
if(this.diskNumberStart === MAX_VALUE_32BITS) {
|
|
this.diskNumberStart = extraReader.readInt(4);
|
|
}
|
|
},
|
|
/**
|
|
* Read the central part of a zip file and add the info in this object.
|
|
* @param {StreamReader} reader the reader to use.
|
|
*/
|
|
readExtraFields : function(reader) {
|
|
var start = reader.index,
|
|
extraFieldId,
|
|
extraFieldLength,
|
|
extraFieldValue;
|
|
|
|
this.extraFields = this.extraFields || {};
|
|
|
|
while (reader.index < start + this.extraFieldsLength) {
|
|
extraFieldId = reader.readInt(2);
|
|
extraFieldLength = reader.readInt(2);
|
|
extraFieldValue = reader.readString(extraFieldLength);
|
|
|
|
this.extraFields[extraFieldId] = {
|
|
id: extraFieldId,
|
|
length: extraFieldLength,
|
|
value: extraFieldValue
|
|
};
|
|
}
|
|
},
|
|
/**
|
|
* Apply an UTF8 transformation if needed.
|
|
*/
|
|
handleUTF8 : function() {
|
|
if (this.useUTF8()) {
|
|
this.fileName = JSZip.prototype.utf8decode(this.fileName);
|
|
this.fileComment = JSZip.prototype.utf8decode(this.fileComment);
|
|
}
|
|
}
|
|
};
|
|
// }}} end of ZipEntry
|
|
|
|
// 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 " +
|
|
"(" + pretty(signature) + ", expected " + pretty(expectedSignature) + ")");
|
|
}
|
|
},
|
|
/**
|
|
* 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);
|
|
this.zipComment = this.reader.readString(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 !
|
|
*/
|
|
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(JSZip.signature.LOCAL_FILE_HEADER);
|
|
file.readLocalPart(this.reader);
|
|
file.handleUTF8();
|
|
}
|
|
},
|
|
/**
|
|
* Read the central directory.
|
|
*/
|
|
readCentralDir : function() {
|
|
var file;
|
|
|
|
this.reader.setIndex(this.centralDirOffset);
|
|
while(this.reader.readString(4) === JSZip.signature.CENTRAL_FILE_HEADER) {
|
|
file = new ZipEntry({
|
|
zip64: this.zip64
|
|
}, this.loadOptions);
|
|
file.readCentralPart(this.reader);
|
|
this.files.push(file);
|
|
}
|
|
},
|
|
/**
|
|
* Read the end of central directory.
|
|
*/
|
|
readEndOfCentral : function() {
|
|
var 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();
|
|
|
|
|
|
/* 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 === MAX_VALUE_16BITS
|
|
|| this.diskWithCentralDirStart === MAX_VALUE_16BITS
|
|
|| this.centralDirRecordsOnThisDisk === MAX_VALUE_16BITS
|
|
|| this.centralDirRecords === MAX_VALUE_16BITS
|
|
|| this.centralDirSize === MAX_VALUE_32BITS
|
|
|| this.centralDirOffset === 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.stream.lastIndexOf(JSZip.signature.ZIP64_CENTRAL_DIRECTORY_LOCATOR);
|
|
if (offset === -1) {
|
|
throw new Error("Corrupted zip : can't find the ZIP64 end of central directory locator");
|
|
}
|
|
this.reader.setIndex(offset);
|
|
this.checkSignature(JSZip.signature.ZIP64_CENTRAL_DIRECTORY_LOCATOR);
|
|
this.readBlockZip64EndOfCentralLocator();
|
|
|
|
// now the zip64 EOCD record
|
|
this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir);
|
|
this.checkSignature(JSZip.signature.ZIP64_CENTRAL_DIRECTORY_END);
|
|
this.readBlockZip64EndOfCentral();
|
|
}
|
|
},
|
|
/**
|
|
* Read a zip file and create ZipEntries.
|
|
* @param {String|ArrayBuffer|Uint8Array} data the binary string representing a zip file.
|
|
*/
|
|
load : function(data) {
|
|
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|ArrayBuffer|Uint8Array} 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, options);
|
|
files = zipEntries.files;
|
|
for (i = 0; i < files.length; i++) {
|
|
input = files[i];
|
|
this.file(input.fileName, input.uncompressedFileData, {
|
|
binary:true,
|
|
optimizedBinaryString:true,
|
|
date:input.date,
|
|
dir:input.dir
|
|
});
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
}());
|
|
// enforcing Stuk's coding style
|
|
// vim: set shiftwidth=3 softtabstop=3 foldmethod=marker:
|