yauzl-promise
Version:
Unzip library for NodeJS
1,177 lines (1,068 loc) • 51.5 kB
JavaScript
/* --------------------
* yauzl-promise module
* `Zip` class
* ------------------*/
/* global WeakRef */
'use strict';
// Modules
const calculateCrc32 = require('@node-rs/crc32').crc32,
assert = require('simple-invariant'),
{isPositiveIntegerOrZero} = require('is-it-type');
// Imports
const Entry = require('./entry.js'),
{INTERNAL_SYMBOL, uncertainUncompressedSizeEntriesRegistry} = require('./shared.js'),
{decodeBuffer, validateFilename, readUInt64LE} = require('./utils.js');
// Exports
// Spec of ZIP format is here: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
// Also: https://libzip.org/specifications/appnote_iz.txt
const EOCDR_WITHOUT_COMMENT_SIZE = 22,
MAX_EOCDR_COMMENT_SIZE = 0xFFFF,
MAC_CDH_EXTRA_FIELD_ID = 22613,
MAC_CDH_EXTRA_FIELD_LENGTH = 8,
MAC_CDH_EXTRA_FIELDS_LENGTH = MAC_CDH_EXTRA_FIELD_LENGTH + 4, // Field data + ID + len (2 bytes each)
MAC_LFH_EXTRA_FIELDS_LENGTH = 16,
CDH_MIN_LENGTH = 46,
CDH_MAX_LENGTH = CDH_MIN_LENGTH + 0xFFFF * 3, // 3 = Filename, extra fields, comment
CDH_MAX_LENGTH_MAC = CDH_MIN_LENGTH + 0xFFFF + MAC_CDH_EXTRA_FIELDS_LENGTH, // No comment
FOUR_GIB = 0x100000000; // Math.pow(2, 32)
class Zip {
/**
* Class representing ZIP file.
* Class is exported in public interface, for purpose of `instanceof` checks, but constructor cannot
* be called by user. This is enforced by use of private symbol `INTERNAL_SYMBOL`.
* @class
* @param {Object} testSymbol - Must be `INTERNAL_SYMBOL`
* @param {Object} reader - `Reader` to use to access the ZIP
* @param {number} size - Size of ZIP file in bytes
* @param {Object} options - Options
* @param {boolean} [options.decodeStrings=true] - Decode filenames and comments to strings
* @param {boolean} [options.validateEntrySizes=true] - Validate entry sizes
* @param {boolean} [options.validateFilenames=true] - Validate filenames
* @param {boolean} [options.strictFilenames=false] - Don't allow backslashes (`\`) in filenames
* @param {boolean} [options.supportMacArchive=true] - Support Mac OS Archive Utility faulty ZIP files
*/
constructor(testSymbol, reader, size, options) {
assert(
testSymbol === INTERNAL_SYMBOL,
'Zip class cannot be instantiated directly. Use one of the static methods.'
);
this.reader = reader;
this.size = size;
Object.assign(this, options);
this.isZip64 = null;
this.entryCount = null;
this.entryCountIsCertain = true;
this.footerOffset = null;
this.centralDirectoryOffset = null;
this.centralDirectorySize = null;
this.centralDirectorySizeIsCertain = true;
this.comment = null;
this.numEntriesRead = 0;
this.isMacArchive = false;
this.isMaybeMacArchive = false;
this.compressedSizesAreCertain = true;
this.uncompressedSizesAreCertain = true;
this._isReading = false;
this._entryCursor = null;
this._fileCursor = null;
this._uncertainUncompressedSizeEntryRefs = null;
this._firstEntryProps = null;
}
/**
* Close ZIP file. Underlying reader will be closed.
* @async
* @returns {undefined}
*/
close() {
return this.reader.close();
}
/**
* Getter for whether `Zip` is open for reading.
* @returns {boolean} - `true` if open
*/
get isOpen() {
return this.reader.isOpen;
}
/**
* Locate Central Directory.
* @async
* @returns {undefined}
*/
async _init() {
// Parse End of Central Directory Record + ZIP64 extension
// to get location of the Central Directory
const eocdrBuffer = await this._locateEocdr();
this._parseEocdr(eocdrBuffer);
if (this.isZip64) await this._parseZip64Eocdr();
await this._locateCentralDirectory();
this._entryCursor = this.centralDirectoryOffset;
}
/**
* Locate End of Central Directory Record.
* @async
* @returns {Buffer} - Buffer containing EOCDR
*/
async _locateEocdr() {
// Last field of the End of Central Directory Record is a variable-length comment.
// The comment size is encoded in a 2-byte field in the EOCDR, which we can't find without trudging
// backwards through the comment to find it.
// As a consequence of this design decision, it's possible to have ambiguous ZIP file metadata
// if a coherent EOCDR was in the comment.
// Search backwards for a EOCDR signature.
let bufferSize = EOCDR_WITHOUT_COMMENT_SIZE + MAX_EOCDR_COMMENT_SIZE;
if (this.size < bufferSize) {
assert(this.size >= EOCDR_WITHOUT_COMMENT_SIZE, 'End of Central Directory Record not found');
bufferSize = this.size;
}
const bufferOffset = this.size - bufferSize;
const buffer = await this.reader.read(bufferOffset, bufferSize);
let pos;
for (pos = bufferSize - EOCDR_WITHOUT_COMMENT_SIZE; pos >= 0; pos--) {
if (buffer[pos] !== 0x50) continue;
if (buffer.readUInt32LE(pos) !== 0x06054b50) continue;
const commentLength = buffer.readUInt16LE(pos + 20);
if (commentLength === bufferSize - pos - EOCDR_WITHOUT_COMMENT_SIZE) {
this.footerOffset = bufferOffset + pos;
return buffer.subarray(pos);
}
}
throw new Error('End of Central Directory Record not found');
}
/**
* Parse End of Central Directory Record.
* Get Central Directory location, size and entry count.
* @param {Buffer} eocdrBuffer - Buffer containing EOCDR
* @returns {undefined}
*/
_parseEocdr(eocdrBuffer) {
// Bytes 0-3: End of Central Directory Record signature = 0x06054b50
// Bytes 4-5: Number of this disk
const diskNumber = eocdrBuffer.readUInt16LE(4);
assert(diskNumber === 0, 'Multi-disk ZIP files are not supported');
// Bytes 6-7: Disk where Central Directory starts
// Bytes 8-9: Number of Central Directory records on this disk
// Bytes 10-11: Total number of Central Directory records
this.entryCount = eocdrBuffer.readUInt16LE(10);
// Bytes 12-15: Size of Central Directory (bytes)
this.centralDirectorySize = eocdrBuffer.readUInt32LE(12);
// Bytes 16-19: Offset of Central Directory
this.centralDirectoryOffset = eocdrBuffer.readUInt32LE(16);
// Bytes 22-...: Comment. Encoding is always CP437.
// Copy buffer instead of slicing, so rest of buffer can be garbage collected.
this.comment = this.decodeStrings
? decodeBuffer(eocdrBuffer, 22, false)
: Buffer.from(eocdrBuffer.subarray(22));
// Original Yauzl does not check `centralDirectorySize` here, only offset, though ZIP spec suggests
// both should be checked. I suspect this is a bug in Yauzl, and it has remained undiscovered
// because ZIP files with a Central Directory > 4 GiB are vanishingly rare
// (would require millions of files, or thousands of files with very long filenames/comments).
this.isZip64 = this.entryCount === 0xFFFF || this.centralDirectoryOffset === 0xFFFFFFFF
|| this.centralDirectorySize === 0xFFFFFFFF;
}
/**
* Parse ZIP64 End of Central Directory Locator + Record.
* Get Central Directory location, size and entry count, where ZIP64 extension used.
* @async
* @returns {undefined}
*/
async _parseZip64Eocdr() {
// Parse ZIP64 End of Central Directory Locator
const zip64EocdlOffset = this.footerOffset - 20;
assert(zip64EocdlOffset >= 0, 'Cannot locate ZIP64 End of Central Directory Locator');
const zip64EocdlBuffer = await this.reader.read(zip64EocdlOffset, 20);
// Bytes 0-3: ZIP64 End of Central Directory Locator signature = 0x07064b50
if (zip64EocdlBuffer.readUInt32LE(0) !== 0x07064b50) {
if (this.supportMacArchive) {
// Assume this is a faulty Mac OS archive which happens to have entry count of 65535 (possible)
// or Central Directory size/offset of 4 GiB - 1 (much less likely, but possible).
// If it's not, we'll get another error when trying to read the Central Directory.
this.isMacArchive = true;
return;
}
throw new Error('Invalid ZIP64 End of Central Directory Locator signature');
}
// Bytes 4-7 - Number of the disk with the start of the ZIP64 End of Central Directory Record
// Bytes 8-15: Position of ZIP64 End of Central Directory Record
const zip64EocdrOffset = readUInt64LE(zip64EocdlBuffer, 8);
// Bytes 16-19: Total number of disks
// Parse ZIP64 End of Central Directory Record
assert(
zip64EocdrOffset + 56 <= zip64EocdlOffset,
'Cannot locate ZIP64 End of Central Directory Record'
);
const zip64EocdrBuffer = await this.reader.read(zip64EocdrOffset, 56);
// Bytes 0-3: ZIP64 End of Central Directory Record signature = 0x06064b50
assert(
zip64EocdrBuffer.readUInt32LE(0) === 0x06064b50,
'Invalid ZIP64 End of Central Directory Record signature'
);
// Bytes 4-11: Size of ZIP64 End of Central Directory Record (not inc first 12 bytes)
const zip64EocdrSize = readUInt64LE(zip64EocdrBuffer, 4);
assert(
zip64EocdrOffset + zip64EocdrSize + 12 <= zip64EocdlOffset,
'Invalid ZIP64 End of Central Directory Record'
);
// Bytes 12-13: Version made by
// Bytes 14-15: Version needed to extract
// Bytes 16-19: Number of this disk
// Bytes 20-23: Number of the disk with the start of the Central Directory
// Bytes 24-31: Total number of entries in the Central Directory on this disk
// Bytes 32-39: Total number of entries in the Central Directory
// Spec: "If an archive is in ZIP64 format and the value in this field is 0xFFFF, the size
// will be in the corresponding 8 byte zip64 end of central directory field."
// Original Yauzl expects correct entry count to always be recorded in ZIP64 EOCDR,
// but have altered that here to be more spec-compliant. Ditto Central Directory size + offset.
if (this.entryCount === 0xFFFF) this.entryCount = readUInt64LE(zip64EocdrBuffer, 32);
// Bytes 40-47: Size of the Central Directory
if (this.centralDirectorySize === 0xFFFFFFFF) {
this.centralDirectorySize = readUInt64LE(zip64EocdrBuffer, 40);
}
// Bytes 48-55: Offset of start of Central Directory with respect to the starting disk number
if (this.centralDirectoryOffset === 0xFFFFFFFF) {
this.centralDirectoryOffset = readUInt64LE(zip64EocdrBuffer, 48);
}
// Bytes 56-...: ZIP64 extensible data sector
// Record offset of start of footers.
// Either start of ZIP64 EOCDR (if it butts up to ZIP64 EOCDL), or ZIP64 EOCDL.
this.footerOffset = zip64EocdrOffset + zip64EocdrSize === zip64EocdlOffset
? zip64EocdrOffset
: zip64EocdlOffset;
}
/**
* Locate Central Directory.
*
* In a well-formed ZIP file, the EOCDR accurately gives us the offset and size of Central
* Directory, and the entry count.
*
* However Mac OS Archive Utility, instead of using ZIP64 extension to record Central Directory
* offset or size >= 4 GiB, or entry count >= 65536, truncates size and offset to lower 32 bits,
* and entry count to lower 16 bits.
* i.e.:
* Actual offset = reported offset + n * (1 << 32)
* Actual size = reported size + m * (1 << 32)
* Actual entry count = reported entry count + o * (1 << 16)
* (where `n`, `m` and `o` are unknown)
*
* Identify if this may be a faulty Mac OS Archive Utility ZIP. If so, find the actual location of
* the Central Directory. Deduce which of above properties cannot be known with certainty.
*
* In some cases, it's not possible to immediately determine if a ZIP is definitely a Mac OS ZIP.
* If it may be, but not sure yet, record which properties are unknown at present.
* Later calls to `readEntry()` or `openReadStream()` will reveal more about the ZIP, and the
* determinaton of whether ZIP is a faulty Mac OS ZIP or not will be made then.
*
* Try to do this while ensuring a spec-compliant ZIP will never be misinterpretted.
*
* @async
* @returns {undefined}
*/
async _locateCentralDirectory() {
// Skip this if Mac OS Archive Utility support disabled
if (!this.supportMacArchive) return;
// Mac OS archives don't use ZIP64 extension
if (this.isZip64) return;
// Mac Archives do not contain comment after End of Central Directory Record
if (this.size - this.footerOffset !== EOCDR_WITHOUT_COMMENT_SIZE) return;
// Mac Archives do not have gap between end of last Central Directory Header and start of EOCDR
let centralDirectoryEnd = this.centralDirectoryOffset + this.centralDirectorySize;
if (centralDirectoryEnd % FOUR_GIB !== this.footerOffset % FOUR_GIB) return;
// If claims to have no entries, and there's no room for any, this must be accurate.
// Handle this here to avoid trying to read beyond end of file.
if (this.entryCount === 0 && this.centralDirectoryOffset + CDH_MIN_LENGTH > this.footerOffset) {
assert(this.centralDirectorySize === 0, 'Inconsistent Central Directory size and entry count');
return;
}
// Ensure size and entry count comply with each other and adjust if they don't
if (this.centralDirectorySize < this.entryCount * CDH_MIN_LENGTH) {
// Central Directory size is too small to contain `entryCount` entries. Must be Mac OS ZIP.
// Check is room to grow Central Directory, and grow it up to EOCDR.
assert(
centralDirectoryEnd < this.footerOffset,
'Inconsistent Central Directory size and entry count'
);
this.isMacArchive = true;
centralDirectoryEnd = this.footerOffset;
this.centralDirectorySize = centralDirectoryEnd - this.centralDirectoryOffset;
}
if (this._recalculateEntryCount(0, this.centralDirectoryOffset)) {
// Entry count was too small. Must be Mac OS ZIP.
this.isMacArchive = true;
}
// Unless we already know this is a Mac ZIP, check if Central Directory is where EOCDR says it is
// (if we know it's a Mac ZIP, better to look in last possible position first)
let entry, alreadyCheckedOffset;
if (!this.isMacArchive) {
entry = await this._readEntryAt(this.centralDirectoryOffset);
// If found a non-Mac Central Directory Header, exit - it's not a Mac archive
if (entry && !firstEntryMaybeMac(entry)) {
assert(this.entryCount > 0, 'Inconsistent Central Directory size and entry count');
// Store entry, to be used in first call to `readEntry()`, to avoid reading from file again
this._firstEntryProps = entry;
return;
}
alreadyCheckedOffset = this.centralDirectoryOffset;
} else {
alreadyCheckedOffset = -1;
}
// If no Central Directory found where it should be, this ZIP is either:
// 1. Valid ZIP with no entries
// 2. Faulty Mac OS Archive Utility ZIP
// 3. Invalid ZIP
// If it's an invalid ZIP, all bets are off, so ignore that possibility.
// Try to locate Central Directory in possible locations it could be if this is
// a Mac OS Archive Utility ZIP (`centralDirectoryOffset + n * FOUR_GIB` where `n` is unknown).
// It's more common to have a ZIP containing large files, than a ZIP with
// so many files that the Central Directory is 4 GiB+ in size (likely requiring millions of files).
// So start with last possible position and work backwards towards start of file.
if (!entry) {
// Find last possible offset for Central Directory
let offset = this.footerOffset
- Math.max(this.centralDirectorySize, this.entryCount * CDH_MIN_LENGTH);
if (offset % FOUR_GIB < this.centralDirectoryOffset) {
assert(offset >= FOUR_GIB, 'Inconsistent Central Directory size and entry count');
offset -= FOUR_GIB;
}
offset = Math.floor(offset / FOUR_GIB) * FOUR_GIB + this.centralDirectoryOffset;
// Search for Central Directory
while (offset > alreadyCheckedOffset) {
entry = await this._readEntryAt(offset);
if (entry) {
assert(firstEntryMaybeMac(entry), 'Cannot locate Central Directory');
this.isMacArchive = true;
this.centralDirectoryOffset = offset;
break;
}
offset -= FOUR_GIB;
}
}
// If couldn't find Central Directory, it's a faulty ZIP, unless it has 0 entries
if (!entry) {
assert(
this.entryCount === 0 && this.centralDirectorySize === 0,
'Cannot locate Central Directory'
);
return;
}
// We've found Central Directory, and it is likely to be Mac OS ZIP, but we may not know for sure.
// If reported entry count was 0, but Central Directory found, must be a Mac OS ZIP.
if (this.entryCount === 0) this.isMacArchive = true;
if (this.isMacArchive) {
// We know for sure this is a Mac OS Archive Utility ZIP,
// because some of the size/offset/entry count data has proved faulty.
// Mac OS ZIPs always have Central Directory going all the way up to the EOCDR.
centralDirectoryEnd = this.footerOffset;
this.centralDirectorySize = centralDirectoryEnd - this.centralDirectoryOffset;
assert(this.centralDirectorySize > 0, 'Inconsistent Central Directory size and entry count');
// Recalculate minimum entry count
this._recalculateEntryCount(1, entry.entryEnd);
// Calculate if possible for one or more files to be 4 GiB larger than reported.
// Each entry takes at minimum 30 bytes for Local File Header + 16 bytes for Data Descriptor.
// Mac Archives repeat same filename in Local File Header as in Central Directory.
// Mac Archives contain 16 bytes Extra Fields in Local File Header if CDH contains an Extra Field.
// So minimum size occupied by first file can be included in this calculation.
const minTotalDataSize = this.entryCount * 46
+ entry.compressedSize
+ entry.filename.length
+ entry.extraFields.length * MAC_LFH_EXTRA_FIELDS_LENGTH;
if (minTotalDataSize + FOUR_GIB <= this.centralDirectoryOffset) {
this.compressedSizesAreCertain = false;
}
} else {
// ZIP has Central Directory where it should be, and format of first entry is consistent
// with this being a Mac OS ZIP, but we don't know for sure that it is
this.isMaybeMacArchive = true;
if (centralDirectoryEnd < this.footerOffset) {
// There's room for Central Directory to be 4 GiB or more bigger than reported.
// This implies entry count is uncertain too. An extra 4 GiB could fit up to ~9 million entries.
this.centralDirectorySizeIsCertain = false;
this.entryCountIsCertain = false;
} else {
// Recalculate minimum entry count
this._recalculateEntryCount(1, entry.entryEnd);
}
// Init set of uncertain uncompressed size entries
this._uncertainUncompressedSizeEntryRefs = new Set();
}
// Check if entry count could be higher than EOCDR says it is
if (
this.entryCountIsCertain
&& !entryCountIsCertain(this.entryCount - 1, centralDirectoryEnd - entry.entryEnd)
) this.entryCountIsCertain = false;
// Even if compressed file sizes are certain, uncompressed file sizes remain uncertain
// because a file could be < 4 GiB compressed, but >= 4 GiB uncompressed
this.uncompressedSizesAreCertain = false;
// Init local file header cursor
this._fileCursor = 0;
// Store entry, to be used in first call to `readEntry()`, to avoid reading from file again
this._firstEntryProps = entry;
}
/**
* Get next entry.
* @async
* @returns {Entry|null} - `Entry` object for next entry, or `null` if none remaining
*/
async readEntry() {
assert(!this._isReading, 'Cannot call `readEntry()` before previous call\'s promise has settled');
this._isReading = true;
try {
return await this._readEntry();
} finally {
this._isReading = false;
}
}
/**
* Get next entry.
* Implementation for `readEntry()`. Should not be called directly.
* @async
* @returns {Entry|null} - `Entry` object for next entry, or `null` if none remaining
*/
async _readEntry() {
if (this.numEntriesRead === this.entryCount && this.entryCountIsCertain) return null;
// Read Central Directory entry properties (or use the one already read)
let entryProps = this._firstEntryProps,
entryEnd;
if (entryProps) {
this._firstEntryProps = null;
entryEnd = entryProps.entryEnd;
} else {
entryProps = await this._readEntryAt(this._entryCursor);
const centralDirectoryEnd = this.centralDirectoryOffset + this.centralDirectorySize;
if (!entryProps) {
// Only way to get here if the ZIP file isn't corrupt is if Central Directory size wasn't
// certain, and therefore entry count wasn't certain either, so we weren't sure if this was
// the end or not. If we've reached end of reported entries, and are at end of reported
// Central Directory, then there being no entry means the Central Directory entry size
// and entry count are accurate, and this is indeed the end.
// That implies this can't be a Mac ZIP, because Central Directory doesn't go up to EOCDR.
// NB: No need to check for `this.centralDirectorySizeIsCertain === false` because if that
// was the case, `this.entryCountIsCertain` would be `false` too, and we wouldn't be here.
assert(
!this.isMacArchive && this.numEntriesRead === this.entryCount
&& this._entryCursor === centralDirectoryEnd,
'Invalid Central Directory File Header signature'
);
// `isMaybeMacArchive` must have been `true` at start of this function, but check it here
// just in case it was already changed in a call to `openReadStream()` made by user while async
// `_readEntryAt()` call above was executing.
if (this.isMaybeMacArchive) this._setAsNotMacArchive();
return null;
}
entryEnd = entryProps.entryEnd;
if (this.isMacArchive) {
// Properties have been found to be inconsistent already, signalling a Mac OS ZIP.
// So all entries must be Mac-type, or it isn't a Mac ZIP after all, and is corrupt.
// File data is tightly packed in Mac OS ZIPs with no gaps in between.
assert(
entryMaybeMac(entryProps) && entryProps.fileHeaderOffset === this._fileCursor % FOUR_GIB,
'Inconsistent Central Directory structure'
);
entryProps.fileHeaderOffset = this._fileCursor;
if (!this.entryCountIsCertain) {
this._recalculateEntryCount(this.numEntriesRead + 1, entryEnd);
this._recalculateEntryCountIsCertain(this.numEntriesRead + 1, entryEnd);
}
} else if (this.isMaybeMacArchive) {
if (this._fileCursor >= FOUR_GIB) {
// This ZIP is flagged as maybe Mac which means all data up to `_fileCursor`
// has been consumed by previous files.
// `fileHeaderOffset` is 32 bit (so < 4 GiB), and `_fileCursor` > 4 GiB, so either
// 1. file data for this entry covers data already consumed (invalid, possible ZIP bomb)
// or 2. this must be a Mac ZIP and `fileHeaderOffset` is more than stated.
assert(
entryMaybeMac(entryProps) && entryProps.fileHeaderOffset === this._fileCursor % FOUR_GIB,
'Inconsistent Central Directory structure'
);
this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
} else if (!entryMaybeMac(entryProps) || entryProps.fileHeaderOffset !== this._fileCursor) {
// Entry doesn't match signature of Mac entries, or file header is not where it would be
// in a Mac ZIP, so it can't be one
this._setAsNotMacArchive();
// If entries were meant to be exhausted, there's an error somewhere
assert(this.numEntriesRead !== this.entryCount, 'Central Directory contains too many entries');
} else if (!this.centralDirectorySizeIsCertain && (
entryEnd + (this.entryCount - this.numEntriesRead - 1) * CDH_MIN_LENGTH > centralDirectoryEnd
)) {
// Not enough space in Central Directory for number of entries remaining,
// so this must be a Mac ZIP. Grow Central Directory.
this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
} else if (!this.entryCountIsCertain) {
// Recalculate if entry count is now impossibly low
if (this._recalculateEntryCount(this.numEntriesRead + 1, entryEnd)) {
// Entry count was impossibly low for size of Central Directory so this must be Mac ZIP
this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
} else if (this.centralDirectorySizeIsCertain) {
// Check if entry count is now high enough vs remaining Central Directory space
// that it can't be any larger
this._recalculateEntryCountIsCertain(this.numEntriesRead + 1, entryEnd);
}
}
}
}
// Calculate what location of file data will be if this is a Mac OS ZIP.
// Mac OS ZIPs always contain Local File Header of 30 bytes
// + same filename as in Central Directory entry
// + 16 bytes Extra Fields if Central Directory entry has extra fields.
const fileDataOffsetIfMac = entryProps.fileHeaderOffset + 30 + entryProps.filename.length
+ entryProps.extraFields.length * MAC_LFH_EXTRA_FIELDS_LENGTH;
// Determine if possible for compressed data to be larger than reported,
// and, if so, the actual compressed size
if (!this.compressedSizesAreCertain) {
const isNowCertain = await this._determineCompressedSize(entryProps, fileDataOffsetIfMac);
if (isNowCertain) this.compressedSizesAreCertain = true;
}
// Determine if possible for this entry's uncompressed size to be larger than reported
if (!this.uncompressedSizesAreCertain) {
if (entryProps.compressionMethod === 0) {
// No compression - uncompressed size always equal to compressed.
// NB: We know encryption is not enabled as entry would have been flagged as non-Mac if it was.
entryProps.uncompressedSize = entryProps.compressedSize;
} else if (entryProps.compressionMethod !== 8) {
// Not Deflate compression - no idea what uncompressed size could be
entryProps.uncompressedSizeIsCertain = false;
} else {
// Deflate compression. Maximum compression ratio is 1032.
// https://stackoverflow.com/questions/16792189/gzip-compression-ratio-for-zeros/16794960#16794960
const maxUncompressedSize = entryProps.compressedSize * 1032;
if (
maxUncompressedSize > FOUR_GIB * 2
|| (
maxUncompressedSize > FOUR_GIB
&& maxUncompressedSize % FOUR_GIB > entryProps.uncompressedSize
)
) entryProps.uncompressedSizeIsCertain = false;
}
}
// Create entry object + advance cursor to next entry
const entry = this._validateAndDecodeEntry(entryProps);
this._entryCursor = entryEnd;
this.numEntriesRead++;
if (this.isMacArchive || this.isMaybeMacArchive) {
// Record offset of where next Local File Header will be if this is a Mac OS ZIP.
// 16 bytes for Data Descriptor after file data, unless it's a folder, empty file, or symlink.
this._fileCursor = fileDataOffsetIfMac + entry.compressedSize
+ (entryProps.compressionMethod === 8) * 16;
if (this.isMacArchive) {
// We know offset of file data for sure, so record it
entry.fileDataOffset = fileDataOffsetIfMac;
} else if (!entry.uncompressedSizeIsCertain) {
// This is a suspected Mac OS ZIP (but not for sure), and uncompressed size is uncertain.
// Record entry, so that if ZIP turns out not to be a Mac OS ZIP later,
// `uncompressedSizeIsCertain` can be changed to `true`.
// Entries are recorded as `WeakRef`s, to allow them to be garbage collected.
// The entry is also added to a `FinalizationRegistry`, which removes the ref from the set
// when entry object is garbage collected. This should prevent escalating memory usage
// if lots of entries.
const ref = new WeakRef(entry);
entry._ref = ref;
this._uncertainUncompressedSizeEntryRefs.add(ref);
uncertainUncompressedSizeEntriesRegistry.register(entry, {zip: this, ref}, ref);
}
}
// Return `Entry` object
return entry;
}
/**
* Determine actual compressed size of entry.
* Update `compressedSize` if it's not what was reported.
* Return whether *all future* entries have certain compressed size.
*
* This method should only be called if this is a Mac ZIP, or possibly a Mac ZIP.
* i.e. Compressed sizes are not certain to be as reported in the ZIP.
*
* First attempt to prove that size can be known with certainty without reading from ZIP file.
* If that's not possible, search ZIP file for the Data Descriptor which follows file data.
*
* Care has to be taken to avoid data races, because this function contains async IO calls,
* and possible for user to call `openReadStream()` on another Entry, or an event on a stream
* already in process to cause the ZIP to be identified as definitely Mac or definitely not Mac
* during this function's async calls.
*
* @param {Object} entryProps - Entry properties
* @param {number} fileDataOffsetIfMac - If ZIP is a Mac OS ZIP, offset file data will start at
* @returns {boolean} - `true` if all later entry compressed sizes must be certain
*/
async _determineCompressedSize(entryProps, fileDataOffsetIfMac) {
// ZIP may only be a suspected Mac OS ZIP, rather than definitely one.
// However, we can assume it is a Mac ZIP for purposes of calculations here,
// as if actually it's not, compressed size of all entries is certain anyway.
//
// In a Mac ZIP:
// - Files (unless empty) are compressed and have Data Descriptor and Extra Fields.
// Size may be incorrect - truncated to lower 32 bits.
// - Folders and empty files are not compressed and have no Data Descriptor,
// but do have Extra Fields.
// Size = 0.
// - Symlinks are not compressed and have no Data Descriptor or Extra Fields.
// Size assumed under 4GiB as file content is just path to linked file.
//
// So we can know exact end point of this entry's data section (unless it's 4 GiB larger),
// and all other entries yet to come must use 30 bytes each at minimum.
let numEntriesRemaining = this.entryCount - this.numEntriesRead - 1;
let dataSpaceRemaining = this.centralDirectoryOffset - fileDataOffsetIfMac
- entryProps.compressedSize - (entryProps.compressionMethod === 8) * 16;
// Check if not enough data space left for this entry or any later entry
// to be 4 GiB larger than reported
if (dataSpaceRemaining - numEntriesRemaining * 30 < FOUR_GIB) return true;
if (this.isMacArchive && numEntriesRemaining === 0) {
// Last entry in Mac ZIP - must use all remaining space.
// We can trust `entryCount` at this point, as it would have been increased
// if there was excess space in the Central Directory.
// We cannot assume file takes up all remaining space if we don't know for sure that
// this is a Mac ZIP, because if it's not, it would be legitimate as per the ZIP spec
// to have unused space between end of file data and the Central Directory.
assert(
dataSpaceRemaining % FOUR_GIB === 0,
'Invalid ZIP structure for Mac OS Archive Utility ZIP'
);
entryProps.compressedSize += dataSpaceRemaining;
return true;
}
if (entryProps.compressionMethod === 0) {
// If this is a Mac ZIP, entry is a folder, empty file, or symlink (see `entryMaybeMac()` below).
// Folders and empty files definitely have 0 size.
// We have to assume symlinks are under 4 GiB because they have no data descriptor after to
// search for (and what kind of maniac uses a symlink bigger than 4 GiB anyway?).
// If it's not a Mac ZIP, reported compressed size will be accurate.
// So either way, we know size is correct.
// Return `false`, because compressed size of later files may still be larger than reported.
return false;
}
// Compressed size is not certain.
// Search for Data Descriptor after file data.
// It could be where it's reported to be, or anywhere after that in 4 GiB jumps.
let fileDataEnd = fileDataOffsetIfMac + entryProps.compressedSize;
while (true) { // eslint-disable-line no-constant-condition
const buffer = await this.reader.read(fileDataEnd, 20);
if (
buffer.readUInt32LE(0) === 0x08074b50 // Data Descriptor signature
&& buffer.readUInt32LE(4) === entryProps.crc32
&& buffer.readUInt32LE(8) === entryProps.compressedSize
&& buffer.readUInt32LE(12) === entryProps.uncompressedSize
&& (
buffer.readUInt32LE(16) === 0x04034b50 // Local File Header signature
|| fileDataEnd + 16 === this.centralDirectoryOffset // Last entry
)
) break;
// During async `read()` call above, if user called `openReadStream()` on another entry,
// it could have discovered this isn't a Mac ZIP after all.
// If so, stop searching for data descriptor.
if (this.compressedSizesAreCertain) {
fileDataEnd = null;
break;
}
fileDataEnd += FOUR_GIB;
if (fileDataEnd + 16 > this.centralDirectoryOffset) {
// Data Descriptor not found
fileDataEnd = null;
break;
}
}
if (fileDataEnd === null) {
// Could not find Data Descriptor, so this can't be a Mac ZIP
assert(!this.isMacArchive, 'Cannot locate file Data Descriptor');
// Have to check `isMaybeMacArchive` again, as could have changed during async calls
// to `read()` above, if `openReadStream()` was called and found this isn't a Mac ZIP after all
if (this.isMaybeMacArchive) this._setAsNotMacArchive();
return true;
}
if (fileDataEnd === fileDataOffsetIfMac + entryProps.compressedSize) {
// Compressed size is what was stated. So size of later entries is still uncertain.
return false;
}
// Size is larger than stated, so this must be Mac ZIP
if (!this.isMacArchive) {
// Have to check `isMaybeMacArchive` again, as could have changed during async calls
// to `read()` above, if `openReadStream()` was called and found this isn't a Mac ZIP after all
assert(this.isMaybeMacArchive, 'Cannot locate file Data Descriptor');
this._setAsMacArchive(this.numEntriesRead + 1, entryProps.entryEnd);
}
entryProps.compressedSize = fileDataEnd - fileDataOffsetIfMac;
// Check if there's now not enough data space left after this entry for any later entry
// to be 4 GiB larger than reported.
// Need to recalculate `numEntriesRemaining` as `entryCount` could have changed.
// That could happen in `_setAsMacArchive()` call above. Or there's also a possible race
// if another entry is being streamed at the moment, and that stream happened to exceed
// its reported uncompressed size. That could happen during async `read()` calls above,
// and would also cause a call to `_setAsMacArchive()`.
// More obviously, `dataSpaceRemaining` has to be recalculated too,
// as initial `fileDataEnd` may have been found to be inaccurate.
numEntriesRemaining = this.entryCount - this.numEntriesRead - 1;
dataSpaceRemaining = this.centralDirectoryOffset - fileDataEnd - 16;
return dataSpaceRemaining - numEntriesRemaining * 30 < FOUR_GIB;
}
/**
* Attempt to read Central Directory Header at offset.
* Returns properties of entry. Does not decode strings or validate file sizes.
* @async
* @param {number} offset - Offset to parse CDH at
* @returns {Object|null} - Entry properties or `null` if no Central Directory File Header found
*/
async _readEntryAt(offset) {
// Bytes 0-3: Central Directory File Header signature
assert(offset + CDH_MIN_LENGTH <= this.footerOffset, 'Invalid Central Directory File Header');
const entryBuffer = await this.reader.read(offset, CDH_MIN_LENGTH);
if (entryBuffer.readUInt32LE(0) !== 0x02014b50) return null;
// Bytes 4-5: Version made by
const versionMadeBy = entryBuffer.readUInt16LE(4);
// Bytes 6-7: Version needed to extract (minimum)
const versionNeededToExtract = entryBuffer.readUInt16LE(6);
// Bytes 8-9: General Purpose Bit Flag
const generalPurposeBitFlag = entryBuffer.readUInt16LE(8);
// Bytes 10-11: Compression method
const compressionMethod = entryBuffer.readUInt16LE(10);
// Bytes 12-13: File last modification time
const lastModTime = entryBuffer.readUInt16LE(12);
// Bytes 14-15: File last modification date
const lastModDate = entryBuffer.readUInt16LE(14);
// Bytes 16-17: CRC32
const crc32 = entryBuffer.readUInt32LE(16);
// Bytes 20-23: Compressed size
let compressedSize = entryBuffer.readUInt32LE(20);
// Bytes 24-27: Uncompressed size
let uncompressedSize = entryBuffer.readUInt32LE(24);
// Bytes 28-29: Filename length
const filenameLength = entryBuffer.readUInt16LE(28);
// Bytes 30-31: Extra field length
const extraFieldLength = entryBuffer.readUInt16LE(30);
// Bytes 32-33: File comment length
const commentLength = entryBuffer.readUInt16LE(32);
// Bytes 34-35: Disk number where file starts
// Bytes 36-37: Internal file attributes
const internalFileAttributes = entryBuffer.readUInt16LE(36);
// Bytes 38-41: External file attributes
const externalFileAttributes = entryBuffer.readUInt32LE(38);
// Bytes 42-45: Relative offset of Local File Header
let fileHeaderOffset = entryBuffer.readUInt32LE(42);
// eslint-disable-next-line no-bitwise
assert((generalPurposeBitFlag & 0x40) === 0, 'Strong encryption is not supported');
// Get filename
const extraDataOffset = offset + CDH_MIN_LENGTH,
extraDataSize = filenameLength + extraFieldLength + commentLength,
entryEnd = extraDataOffset + extraDataSize;
assert(entryEnd <= this.footerOffset, 'Invalid Central Directory File Header');
const extraBuffer = await this.reader.read(extraDataOffset, extraDataSize);
const filename = extraBuffer.subarray(0, filenameLength);
// Get extra fields
const commentStart = filenameLength + extraFieldLength;
const extraFieldBuffer = extraBuffer.subarray(filenameLength, commentStart);
let i = 0;
const extraFields = [];
let zip64EiefBuffer;
while (i < extraFieldBuffer.length - 3) {
const headerId = extraFieldBuffer.readUInt16LE(i + 0),
dataSize = extraFieldBuffer.readUInt16LE(i + 2),
dataStart = i + 4,
dataEnd = dataStart + dataSize;
assert(dataEnd <= extraFieldBuffer.length, 'Extra field length exceeds extra field buffer size');
const dataBuffer = extraFieldBuffer.subarray(dataStart, dataEnd);
extraFields.push({id: headerId, data: dataBuffer});
i = dataEnd;
if (headerId === 1) zip64EiefBuffer = dataBuffer;
}
// Get file comment
const comment = extraBuffer.subarray(commentStart, extraDataSize);
// Handle ZIP64
const isZip64 = uncompressedSize === 0xFFFFFFFF || compressedSize === 0xFFFFFFFF
|| fileHeaderOffset === 0xFFFFFFFF;
if (isZip64) {
assert(zip64EiefBuffer, 'Expected ZIP64 Extended Information Extra Field');
// @overlookmotel: According to the spec, I'd expect all 3 of these fields to be present,
// but Yauzl's implementation makes them optional.
// There may be a good reason for this, so leaving it as in Yauzl's implementation.
let index = 0;
// 8 bytes: Uncompressed size
if (uncompressedSize === 0xFFFFFFFF) {
assert(
index + 8 <= zip64EiefBuffer.length,
'ZIP64 Extended Information Extra Field does not include uncompressed size'
);
uncompressedSize = readUInt64LE(zip64EiefBuffer, index);
index += 8;
}
// 8 bytes: Compressed size
if (compressedSize === 0xFFFFFFFF) {
assert(
index + 8 <= zip64EiefBuffer.length,
'ZIP64 Extended Information Extra Field does not include compressed size'
);
compressedSize = readUInt64LE(zip64EiefBuffer, index);
index += 8;
}
// 8 bytes: Local File Header offset
if (fileHeaderOffset === 0xFFFFFFFF) {
assert(
index + 8 <= zip64EiefBuffer.length,
'ZIP64 Extended Information Extra Field does not include relative header offset'
);
fileHeaderOffset = readUInt64LE(zip64EiefBuffer, index);
index += 8;
}
// 4 bytes: Disk Start Number
}
// Minimum length of Local File Header = 30
assert(fileHeaderOffset + 30 <= this.footerOffset, 'Invalid location for file data');
// Return entry properties
return {
filename,
compressedSize,
uncompressedSize,
uncompressedSizeIsCertain: true, // May not be correct - may be set to `false` in `readEntry()`
compressionMethod,
fileHeaderOffset,
fileDataOffset: null,
isZip64,
crc32,
lastModTime,
lastModDate,
comment,
extraFields,
versionMadeBy,
versionNeededToExtract,
generalPurposeBitFlag,
internalFileAttributes,
externalFileAttributes,
filenameLength,
entryEnd
};
}
/**
* Update `entryCount` if it's lower than is possible for it to be.
* @param {number} numEntriesRead - Number of entries read so far
* @param {number} entryCursor - Current position in Central Directory
* @returns {boolean} - `true` if entry count was increased
*/
_recalculateEntryCount(numEntriesRead, entryCursor) {
const numEntriesRemaining = this.entryCount - numEntriesRead,
centralDirectoryRemaining = this.centralDirectoryOffset + this.centralDirectorySize - entryCursor,
entryMaxLen = this.isMacArchive ? CDH_MAX_LENGTH_MAC : CDH_MAX_LENGTH;
if (numEntriesRemaining * entryMaxLen >= centralDirectoryRemaining) return false;
// Entry count can't be right.
// This must be a Mac Archive, so we calculate minimum entry count based on
// max length of entries in Mac OS ZIPs (which is less than for non-Mac entries).
const minEntriesRemaining = Math.ceil(centralDirectoryRemaining / CDH_MAX_LENGTH_MAC);
// eslint-disable-next-line no-bitwise
this.entryCount += (minEntriesRemaining - numEntriesRemaining + 0xFFFF) & 0x10000;
return true;
}
/**
* Update `entryCountIsCertain` if it's impossible for entry count to be 65536 larger than
* current `entryCount` without exceeding bounds of Central Directory.
* This calculation is only valid if size of Central Directory is certain,
* so must only be called if `centralDirectorySizeIsCertain` is `true`.
* @param {number} numEntriesRead - Number of entries read so far
* @param {number} entryCursor - Current position in Central Directory
* @returns {undefined}
*/
_recalculateEntryCountIsCertain(numEntriesRead, entryCursor) {
const numEntriesRemaining = this.entryCount - numEntriesRead,
centralDirectoryRemaining = this.centralDirectoryOffset + this.centralDirectorySize - entryCursor;
if (entryCountIsCertain(numEntriesRemaining, centralDirectoryRemaining)) {
this.entryCountIsCertain = true;
}
}
/**
* Suspected Mac OS Archive Utility ZIP has turned out to definitely be one.
* Flag as Mac ZIP and calculate Central Directory size if it was ambiguous previously.
* Recalculate minimum entry count and whether it's now certain.
* @param {number} numEntriesRead - Number of entries read so far
* @param {number} entryCursor - Current position in Central Directory
* @returns {undefined}
*/
_setAsMacArchive(numEntriesRead, entryCursor) {
this.isMacArchive = true;
this.isMaybeMacArchive = false;
if (!this.centralDirectorySizeIsCertain) {
this.centralDirectorySize = this.footerOffset - this.centralDirectoryOffset;
this.centralDirectorySizeIsCertain = true;
}
// Recalculate minimum entry count + whether entry count is certain
if (!this.entryCountIsCertain) {
this._recalculateEntryCount(numEntriesRead, entryCursor);
this._recalculateEntryCountIsCertain(numEntriesRead, entryCursor);
}
// Clear set of uncertain uncompressed size entries
for (const ref of this._uncertainUncompressedSizeEntryRefs) {
uncertainUncompressedSizeEntriesRegistry.unregister(ref);
const entry = ref.deref();
if (entry) entry._ref = null;
}
this._uncertainUncompressedSizeEntryRefs = null;
}
/**
* Suspected Mac OS Archive Utility ZIP has turned out not to be one.
* Reset flags.
* @returns {undefined}
*/
_setAsNotMacArchive() {
this.isMaybeMacArchive = false;
this.entryCountIsCertain = true;
this.centralDirectorySizeIsCertain = true;
this.compressedSizesAreCertain = true;
this.uncompressedSizesAreCertain = true;
this._fileCursor = null;
// Flag all entries flagged as having uncertain uncompressed size as now having certain size
for (const ref of this._uncertainUncompressedSizeEntryRefs) {
uncertainUncompressedSizeEntriesRegistry.unregister(ref);
const entry = ref.deref();
if (entry) {
entry._ref = null;
entry.uncompressedSizeIsCertain = true;
}
}
this._uncertainUncompressedSizeEntryRefs = null;
}
/**
* Convert entry properties returned from `_readEntryAt()` to a full `Entry` object.
* Decode strings and validate entry size according to options.
* @param {Object} entry - Entry properties returned by `_readEntryAt()`
* @returns {Entry} - `Entry` object
*/
_validateAndDecodeEntry(entry) {
if (this.decodeStrings) {
// Check for Info-ZIP Unicode Path Extra Field (0x7075).
// See: https://github.com/thejoshwolfe/yauzl/issues/33
let filename;
for (const extraField of entry.extraFields) {
if (extraField.id !== 0x7075) continue;
if (extraField.data.length < 6) continue; // Too short to be meaningful
// Check version is 1. "Changes may not be backward compatible so this extra
// field should not be used if the version is not recognized."
if (extraField.data[0] !== 1) continue;
// Check CRC32 matches original filename.
// "The NameCRC32 is the standard zip CRC32 checksum of the File Name
// field in the header. This is used to verify that the header
// File Name field has not changed since the Unicode Path extra field
// was created. This can happen if a utility renames the File Name but
// does not update the UTF-8 path extra field. If the CRC check fails,
// this UTF-8 Path Extra Field SHOULD be ignored and the File Name field
// in the header SHOULD be used instead."
const oldNameCrc32 = extraField.data.readUInt32LE(1);
if (calculateCrc32(entry.filename) !== oldNameCrc32) continue;
filename = decodeBuffer(extraField.data, 5, true);
break;
}
// Decode filename
const isUtf8 = (entry.generalPurposeBitFlag & 0x800) !== 0; // eslint-disable-line no-bitwise
if (filename === undefined) filename = decodeBuffer(entry.filename, 0, isUtf8);
// Validate filename
if (this.validateFilenames) {
// Allow backslash if `strictFilenames` option disabled
if (!this.strictFilenames) filename = filename.replace(/\\/g, '/');
validateFilename(filename);
}
entry.filename = filename;
// Clone Extra Fields buffers, so rest of buffer that they're sliced from
// (which also contains strings which are now decoded) can be garbage collected
for (const extraField of entry.extraFields) {
extraField.data = Buffer.from(extraField.data);
}
// Decode comment
entry.comment = decodeBuffer(entry.comment, 0, isUtf8);
}
// Validate file size
if (this.validateEntrySizes && entry.compressionMethod === 0) {
// Lowest bit of General Purpose Bit Flag is for traditional encryption.
// Traditional encryption prefixes the file data with a header.
// eslint-disable-next-line no-bitwise
const expectedCompressedSize = (entry.generalPurposeBitFlag & 0x1)
? entry.uncompressedSize + 12
: entry.uncompressedSize;
assert(
entry.compressedSize === expectedCompressedSize,
'Compressed/uncompressed size mismatch for stored file: '
+ `${entry.compressedSize} !== ${expectedCompressedSize}`
);
}
// Create `Entry` object
let entryEnd;
({entryEnd, ...entry} = entry); // eslint-disable-line prefer-const
return new Entry(INTERNAL_SYMBOL, {...entry, zip: this, _ref: null});
}
/**
* Read multiple entries.
* If `numEntries` is provided, will read at maximum that number of entries.
* Otherwise, reads all entries.
* @async
* @param {number} [numEntries] - Number of entries to read
* @returns {Array<Entry>} - Array of entries
*/
async readEntries(numEntries) {
if (numEntries != null) {
assert(isPositiveIntegerOrZero(numEntries), '`numEntries` must be a positive integer if provided');
} else {
numEntries = Infinity;
}
const entries = [];
for (let i = 0; i < numEntries; i++) {
const entry = await this.readEntry();
if (!entry) break;
entries.push(entry);
}
return entries;
}
/**
* Get async iterator for entries.
* Usage: `for await (const entry of zip) { ... }`
* @returns {Object} - Async iterator
*/
[Symbol.asyncIterator]() {
return {
next: async () => {
const entry = await this.readEntry();
return {value: entry, done: entry === null};
}
};
}
/**
* Get readable stream for file data.
* @async
* @param {Entry} entry - `Entry` object
* @param {Object} [options] - Options
* @param {boolean} [options.decompress] - `false` to output raw data without decompression
* @param {boolean} [options.decrypt] - `true` to decrypt if is encrypted
* @param {number} [options.start] - Start offset (only valid if not decompressing)
* @param {number} [options.end] - End offset (only valid if not decompressing)
* @returns {Object} - Readable stream
*/
async openReadStream(entry, options) {
assert(entry instanceof Entry, '`entry` must be an instance of `Entry`');
assert(entry.zip === this, '`entry` must be an `Entry` from this ZIP file');
return await entry.openReadStream(options);
}
}
module.exports = Zip;
/**
* Determine if entry count is certain.
* i.e. `centralDirectorySize` bytes could not fit 65536 more entries than stated.
* @param {number} entryCount - Number of entries expected (may be under-estimate)
* @param {number} centralDirectorySize - Size of Central Directory space to store entries
* @returns {boolean} - `true` if entry count is certain
*/
function entryCountIsCertain(entryCount, centralDirectorySize) {
return (entryCount + 0x10000) * CDH_MIN_LENGTH > centralDirectorySize;
}
/**
* Check if first entry may be a Mac OS Archive Utility entry,
* according to various distinguishing characteristics.
* @param {Object} entry - Entry props from `_readEntryAt()`
* @returns {boolean} - `true` if matches signature of a Mac OS ZIP first entry
*/
function firstEntryMaybeMac(entry) {
// First file always starts at byte 0
if (entry.fileHeaderOffset !== 0) return false;
return entryMaybeMac(entry);
}
/**
* Check if entry may be a Mac OS Archive Utility entry,
* according to various distinguishing characteristics.
* @param {Object} entry - Entry props from `_readEntryAt()`
* @returns {boolean} - `true` if matches signature of a Mac OS ZIP entry
*/
function entryMaybeMac(entry) {
// Entries always have this `versionMadeBy` value
if (entry.versionMadeBy !== 789) return false;
// Entries never have comments
if (entry.comment.length !== 0) return false;
// Entries never have ZIP64 headers
if (entry.isZip64) return false;
// Check various attributes for files,