318 lines
11 KiB
JavaScript
318 lines
11 KiB
JavaScript
"use strict";
|
|
const StringReader = require("./stringReader.js");
|
|
const NodeBufferReader = require("./nodeBufferReader.js");
|
|
const Uint8ArrayReader = require("./uint8ArrayReader.js");
|
|
const ArrayReader = require("./arrayReader.js");
|
|
const utils = require("./utils.js");
|
|
const sig = require("./signature.js");
|
|
const ZipEntry = require("./zipEntry.js");
|
|
const support = require("./support.js");
|
|
// 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(expectedSignature) {
|
|
const 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(askedIndex, expectedSignature) {
|
|
const currentIndex = this.reader.index;
|
|
this.reader.setIndex(askedIndex);
|
|
const signature = this.reader.readString(4);
|
|
const result = signature === expectedSignature;
|
|
this.reader.setIndex(currentIndex);
|
|
return result;
|
|
},
|
|
/**
|
|
* Read the end of the central directory.
|
|
*/
|
|
readBlockEndOfCentral() {
|
|
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.
|
|
const zipComment = this.reader.readData(this.zipCommentLength);
|
|
const decodeParamType = support.uint8array ? "uint8array" : "array";
|
|
// To get consistent behavior with the generation part, we will assume that
|
|
// this is utf8 encoded unless specified otherwise.
|
|
const 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() {
|
|
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 = {};
|
|
const extraDataSize = this.zip64EndOfCentralSize - 44;
|
|
const index = 0;
|
|
let 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() {
|
|
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() {
|
|
let 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() {
|
|
let 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() {
|
|
let 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.
|
|
const 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 ?"
|
|
);
|
|
} else {
|
|
throw new Error("Corrupted zip : can't find end of central directory");
|
|
}
|
|
}
|
|
this.reader.setIndex(offset);
|
|
const 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();
|
|
}
|
|
|
|
let 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;
|
|
}
|
|
|
|
const 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(data) {
|
|
const 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(data) {
|
|
this.prepareReader(data);
|
|
this.readEndOfCentral();
|
|
this.readCentralDir();
|
|
this.readLocalFiles();
|
|
},
|
|
};
|
|
// }}} end of ZipEntries
|
|
module.exports = ZipEntries;
|