function Entry() { }
n/a
function RandomAccessReader() { EventEmitter.call(this); this.refCount = 0; }
n/a
function ZipFile(reader, centralDirectoryOffset, fileSize, entryCount, comment, autoClose, lazyEntries, decodeStrings, validateEntrySizes ) { var self = this; EventEmitter.call(self); self.reader = reader; // forward close events self.reader.on("error", function(err) { // error closing the fd emitError(self, err); }); self.reader.once("close", function() { self.emit("close"); }); self.readEntryCursor = centralDirectoryOffset; self.fileSize = fileSize; self.entryCount = entryCount; self.comment = comment; self.entriesRead = 0; self.autoClose = !!autoClose; self.lazyEntries = !!lazyEntries; self.decodeStrings = !!decodeStrings; self.validateEntrySizes = !!validateEntrySizes; self.isOpen = true; self.emittedError = false; if (!self.lazyEntries) self.readEntry(); }
n/a
function dosDateTimeToDate(date, time) { var day = date & 0x1f; // 1-31 var month = (date >> 5 & 0xf) - 1; // 1-12, 0-11 var year = (date >> 9 & 0x7f) + 1980; // 0-128, 1980-2108 var millisecond = 0; var second = (time & 0x1f) * 2; // 0-29, 0-58 (even numbers) var minute = time >> 5 & 0x3f; // 0-59 var hour = time >> 11 & 0x1f; // 0-23 return new Date(year, month, day, hour, minute, second, millisecond); }
n/a
function fromBuffer(buffer, options, callback) { if (typeof options === "function") { callback = options; options = null; } if (options == null) options = {}; options.autoClose = false; if (options.lazyEntries == null) options.lazyEntries = false; if (options.decodeStrings == null) options.decodeStrings = true; if (options.validateEntrySizes == null) options.validateEntrySizes = true; // i got your open file right here. var reader = fd_slicer.createFromBuffer(buffer); fromRandomAccessReader(reader, buffer.length, options, callback); }
n/a
function fromFd(fd, options, callback) { if (typeof options === "function") { callback = options; options = null; } if (options == null) options = {}; if (options.autoClose == null) options.autoClose = false; if (options.lazyEntries == null) options.lazyEntries = false; if (options.decodeStrings == null) options.decodeStrings = true; if (options.validateEntrySizes == null) options.validateEntrySizes = true; if (callback == null) callback = defaultCallback; fs.fstat(fd, function(err, stats) { if (err) return callback(err); var reader = fd_slicer.createFromFd(fd, {autoClose: true}); fromRandomAccessReader(reader, stats.size, options, callback); }); }
n/a
function fromRandomAccessReader(reader, totalSize, options, callback) { if (typeof options === "function") { callback = options; options = null; } if (options == null) options = {}; if (options.autoClose == null) options.autoClose = true; if (options.lazyEntries == null) options.lazyEntries = false; if (options.decodeStrings == null) options.decodeStrings = true; var decodeStrings = !!options.decodeStrings; if (options.validateEntrySizes == null) options.validateEntrySizes = true; if (callback == null) callback = defaultCallback; if (typeof totalSize !== "number") throw new Error("expected totalSize parameter to be a number"); if (totalSize > Number.MAX_SAFE_INTEGER) { throw new Error("zip file too large. only file sizes up to 2^52 are supported due to JavaScript's Number type being an IEEE 754 double."); } // the matching unref() call is in zipfile.close() reader.ref(); // eocdr means End of Central Directory Record. // search backwards for the eocdr signature. // the last field of the eocdr 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. // we search backwards for a eocdr signature, and hope that whoever made the zip file was smart enough to forbid the eocdr signature in the comment. var eocdrWithoutCommentSize = 22; var maxCommentSize = 0xffff; // 2-byte size var bufferSize = Math.min(eocdrWithoutCommentSize + maxCommentSize, totalSize); var buffer = new Buffer(bufferSize); var bufferReadStart = totalSize - buffer.length; readAndAssertNoEof(reader, buffer, 0, bufferSize, bufferReadStart, function(err) { if (err) return callback(err); for (var i = bufferSize - eocdrWithoutCommentSize; i >= 0; i -= 1) { if (buffer.readUInt32LE(i) !== 0x06054b50) continue; // found eocdr var eocdrBuffer = buffer.slice(i); // 0 - End of central directory signature = 0x06054b50 // 4 - Number of this disk var diskNumber = eocdrBuffer.readUInt16LE(4); if (diskNumber !== 0) { return callback(new Error("multi-disk zip files are not supported: found disk number: " + diskNumber)); } // 6 - Disk where central directory starts // 8 - Number of central directory records on this disk // 10 - Total number of central directory records var entryCount = eocdrBuffer.readUInt16LE(10); // 12 - Size of central directory (bytes) // 16 - Offset of start of central directory, relative to start of archive var centralDirectoryOffset = eocdrBuffer.readUInt32LE(16); // 20 - Comment length var commentLength = eocdrBuffer.readUInt16LE(20); var expectedCommentLength = eocdrBuffer.length - eocdrWithoutCommentSize; if (commentLength !== expectedCommentLength) { return callback(new Error("invalid comment length. expected: " + expectedCommentLength + ". found: " + commentLength)); } // 22 - Comment // the encoding is always cp437. var comment = decodeStrings ? decodeBuffer(eocdrBuffer, 22, eocdrBuffer.length, false) : eocdrBuffer.slice(22); if (!(entryCount === 0xffff || centralDirectoryOffset === 0xffffffff)) { return callback(null, new ZipFile(reader, centralDirectoryOffset, totalSize, entryCount, comment, options.autoClose, options .lazyEntries, decodeStrings, options.validateEntrySizes)); } // ZIP64 format // ZIP64 Zip64 end of central directory locator var zip64EocdlBuffer = new Buffer(20); var zip64EocdlOffset = bufferReadStart + i - zip64EocdlBuffer.length; readAndAssertNoEof(reader, zip64EocdlBuffer, 0, zip64EocdlBuffer.length, zip64EocdlOffset, function(err) { if (err) return callback(err); // 0 - zip64 end of central dir locator signature = 0x07064b50 if (zip64EocdlBuf ...
n/a
function open(path, options, callback) { if (typeof options === "function") { callback = options; options = null; } if (options == null) options = {}; if (options.autoClose == null) options.autoClose = true; if (options.lazyEntries == null) options.lazyEntries = false; if (options.decodeStrings == null) options.decodeStrings = true; if (options.validateEntrySizes == null) options.validateEntrySizes = true; if (callback == null) callback = defaultCallback; fs.open(path, "r", function(err, fd) { if (err) return callback(err); fromFd(fd, options, function(err, zipfile) { if (err) fs.close(fd, defaultCallback); callback(err, zipfile); }); }); }
...
See `validateFileName()`.
## Usage
```js
var yauzl = require("yauzl");
yauzl.open("path/to/file.zip", {lazyEntries: true}, function(err, zipfile) {
if (err) throw err;
zipfile.readEntry();
zipfile.on("entry", function(entry) {
if (/\/$/.test(entry.fileName)) {
// Directory file names end with '/'.
// Note that entires for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
...
function validateFileName(fileName) { if (fileName.indexOf("\\") !== -1) { return "invalid characters in fileName: " + fileName; } if (/^[a-zA-Z]:/.test(fileName) || /^\//.test(fileName)) { return "absolute path: " + fileName; } if (fileName.split("/").indexOf("..") !== -1) { return "invalid relative path: " + fileName; } // all good return null; }
...
### validateFileName(fileName)
Returns `null` or a `String` error message depending on the validity of `fileName`.
If `fileName` starts with `"/"` or `/[A-Za-z]:\//` or if it contains `".."` path segments or `"\\"`,
this function returns an error message appropriate for use like this:
```js
var errorMessage = yauzl.validateFileName(fileName);
if (errorMessage != null) throw new Error(errorMessage);
```
This function is automatically run for each entry, as long as `decodeStrings` is `true`.
See `open()` and `Event: "entry"` for more information.
### Class: ZipFile
...
function Entry() { }
n/a
getLastModDate = function () { return dosDateTimeToDate(this.lastModFileDate, this.lastModFileTime); }
n/a
isCompressed = function () { return this.compressionMethod === 8; }
...
`callback` gets `(err, readStream)`, where `readStream` is a `Readable Stream` that provides the file data for this entry.
If this zipfile is already closed (see `close()`), the `callback` will receive an `err`.
`options` may be omitted or `null`, and has the following defaults:
```js
{
decompress: entry.isCompressed() ? true : null,
decrypt: null,
start: 0, // actually the default is null, see below
end: entry.compressedSize, // actually the default is null, see below
}
```
If the entry is compressed (with a supported compression method),
...
isEncrypted = function () { return (this.generalPurposeBitFlag & 0x1) !== 0; }
...
}
}
}
// validate file size
if (self.validateEntrySizes && entry.compressionMethod === 0) {
var expectedCompressedSize = entry.uncompressedSize;
if (entry.isEncrypted()) {
// traditional encryption prefixes the file data with a header
expectedCompressedSize += 12;
}
if (entry.compressedSize !== expectedCompressedSize) {
var msg = "compressed/uncompressed size mismatch for stored file: " + entry.compressedSize + " != " + entry
.uncompressedSize;
return emitErrorAndAutoClose(self, new Error(msg));
}
...
function RandomAccessReader() { EventEmitter.call(this); this.refCount = 0; }
n/a
function EventEmitter() { EventEmitter.init.call(this); }
n/a
_readStreamForRange = function (start, end) { throw new Error("not implemented"); }
...
Such heuristics are outside the scope of this library,
but enforcing the `uncompressedSize` is implemented here as a security feature.
It is possible to destroy the `readStream` before it has piped all of its data.
To do this, call `readStream.destroy()`.
You must `unpipe()` the `readStream` from any destination before calling `readStream.destroy()`.
If this zipfile was created using `fromRandomAccessReader()`, the `RandomAccessReader` implementation
must provide readable streams that implement a `.destroy()` method (see `randomAccessReader.
_readStreamForRange()`)
in order for calls to `readStream.destroy()` to work in this context.
#### close()
Causes all future calls to `openReadStream()` to fail,
and closes the fd after all streams created by `openReadStream()` have emitted their `end` events.
...
close = function (callback) { setImmediate(callback); }
...
`options` may be omitted or `null`. The defaults are `{autoClose: true, lazyEntries: false, decodeStrings: true, validateEntrySizes
: true}`.
`autoClose` is effectively equivalent to:
```js
zipfile.once("end", function() {
zipfile.close();
});
```
`lazyEntries` indicates that entries should be read only when `readEntry()` is called.
If `lazyEntries` is `false`, `entry` events will be emitted as fast as possible to allow `pipe()`ing
file data from all entries in parallel.
This is not recommended, as it can lead to out of control memory usage for zip files with many entries.
...
createReadStream = function (options) { var start = options.start; var end = options.end; if (start === end) { var emptyStream = new PassThrough(); setImmediate(function() { emptyStream.end(); }); return emptyStream; } var stream = this._readStreamForRange(start, end); var destroyed = false; var refUnrefFilter = new RefUnrefFilter(this); stream.on("error", function(err) { setImmediate(function() { if (!destroyed) refUnrefFilter.emit("error", err); }); }); refUnrefFilter.destroy = function() { stream.unpipe(refUnrefFilter); refUnrefFilter.unref(); stream.destroy(); }; var byteCounter = new AssertByteCountStream(end - start); refUnrefFilter.on("error", function(err) { setImmediate(function() { if (!destroyed) byteCounter.emit("error", err); }); }); byteCounter.destroy = function() { destroyed = true; refUnrefFilter.unpipe(byteCounter); refUnrefFilter.destroy(); }; return stream.pipe(refUnrefFilter).pipe(byteCounter); }
...
// since we're dealing with an unsigned offset plus an unsigned size,
// we only have 1 thing to check for.
if (fileDataEnd > self.fileSize) {
return callback(new Error("file data overflows file bounds: " +
fileDataStart + " + " + entry.compressedSize + " > " + self.fileSize));
}
}
var readStream = self.reader.createReadStream({
start: fileDataStart + relativeStart,
end: fileDataStart + relativeEnd,
});
var endpointStream = readStream;
if (decompress) {
var destroyed = false;
var inflateFilter = zlib.createInflateRaw();
...
read = function (buffer, offset, length, position, callback) { var readStream = this.createReadStream({start: position, end: position + length}); var writeStream = new Writable(); var written = 0; writeStream._write = function(chunk, encoding, cb) { chunk.copy(buffer, offset + written, 0, chunk.length); written += chunk.length; cb(); }; writeStream.on("finish", callback); readStream.on("error", function(error) { callback(error); }); readStream.pipe(writeStream); }
...
If you never call `readStream.destroy()`, then streams returned from this method do not need to implement a method `.destroy()`.
`.destroy()` should abort any streaming that is in progress and clean up any associated resources.
`.destroy()` will only be called after the stream has been `unpipe()`d from its destination.
Note that the stream returned from this method might not be the same object that is provided by `openReadStream()`.
The stream returned from this method might be `pipe()`d through one or more filter streams (for example, a zlib inflate stream).
#### randomAccessReader.read(buffer, offset, length, position, callback)
Subclasses may implement this method.
The default implementation uses `createReadStream()` to fill the `buffer`.
This method should behave like `fs.read()`.
#### randomAccessReader.close(callback)
...
ref = function () { this.refCount += 1; }
...
if (callback == null) callback = defaultCallback;
if (typeof totalSize !== "number") throw new Error("expected totalSize parameter to be a number");
if (totalSize > Number.MAX_SAFE_INTEGER) {
throw new Error("zip file too large. only file sizes up to 2^52 are supported due to JavaScript's Number type being
an IEEE 754 double.");
}
// the matching unref() call is in zipfile.close()
reader.ref();
// eocdr means End of Central Directory Record.
// search backwards for the eocdr signature.
// the last field of the eocdr 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.
// we search backwards for a eocdr signature, and hope that whoever made the zip file was smart enough to forbid the eocdr signature
in the comment.
...
unref = function () { var self = this; self.refCount -= 1; if (self.refCount > 0) return; if (self.refCount < 0) throw new Error("invalid unref"); self.close(onCloseDone); function onCloseDone(err) { if (err) return self.emit('error', err); self.emit('close'); } }
...
self.emittedError = false;
if (!self.lazyEntries) self.readEntry();
}
ZipFile.prototype.close = function() {
if (!this.isOpen) return;
this.isOpen = false;
this.reader.unref();
};
function emitErrorAndAutoClose(self, err) {
if (self.autoClose) self.close();
emitError(self, err);
}
function emitError(self, err) {
...
function ZipFile(reader, centralDirectoryOffset, fileSize, entryCount, comment, autoClose, lazyEntries, decodeStrings, validateEntrySizes ) { var self = this; EventEmitter.call(self); self.reader = reader; // forward close events self.reader.on("error", function(err) { // error closing the fd emitError(self, err); }); self.reader.once("close", function() { self.emit("close"); }); self.readEntryCursor = centralDirectoryOffset; self.fileSize = fileSize; self.entryCount = entryCount; self.comment = comment; self.entriesRead = 0; self.autoClose = !!autoClose; self.lazyEntries = !!lazyEntries; self.decodeStrings = !!decodeStrings; self.validateEntrySizes = !!validateEntrySizes; self.isOpen = true; self.emittedError = false; if (!self.lazyEntries) self.readEntry(); }
n/a
function EventEmitter() { EventEmitter.init.call(this); }
n/a
close = function () { if (!this.isOpen) return; this.isOpen = false; this.reader.unref(); }
...
`options` may be omitted or `null`. The defaults are `{autoClose: true, lazyEntries: false, decodeStrings: true, validateEntrySizes
: true}`.
`autoClose` is effectively equivalent to:
```js
zipfile.once("end", function() {
zipfile.close();
});
```
`lazyEntries` indicates that entries should be read only when `readEntry()` is called.
If `lazyEntries` is `false`, `entry` events will be emitted as fast as possible to allow `pipe()`ing
file data from all entries in parallel.
This is not recommended, as it can lead to out of control memory usage for zip files with many entries.
...
openReadStream = function (entry, options, callback) { var self = this; // parameter validation var relativeStart = 0; var relativeEnd = entry.compressedSize; if (callback == null) { callback = options; options = {}; } else { // validate options that the caller has no excuse to get wrong if (options.decrypt != null) { if (!entry.isEncrypted()) { throw new Error("options.decrypt can only be specified for encrypted entries"); } if (options.decrypt !== false) throw new Error("invalid options.decrypt value: " + options.decrypt); if (entry.isCompressed()) { if (options.decompress !== false) throw new Error("entry is encrypted and compressed, and options.decompress !== false"); } } if (options.decompress != null) { if (!entry.isCompressed()) { throw new Error("options.decompress can only be specified for compressed entries"); } if (!(options.decompress === false || options.decompress === true)) { throw new Error("invalid options.decompress value: " + options.decompress); } } if (options.start != null || options.end != null) { if (entry.isCompressed() && options.decompress !== false) { throw new Error("start/end range not allowed for compressed entry without options.decompress === false"); } if (entry.isEncrypted() && options.decrypt !== false) { throw new Error("start/end range not allowed for encrypted entry without options.decrypt === false"); } } if (options.start != null) { relativeStart = options.start; if (relativeStart < 0) throw new Error("options.start < 0"); if (relativeStart > entry.compressedSize) throw new Error("options.start > entry.compressedSize"); } if (options.end != null) { relativeEnd = options.end; if (relativeEnd < 0) throw new Error("options.end < 0"); if (relativeEnd > entry.compressedSize) throw new Error("options.end > entry.compressedSize"); if (relativeEnd < relativeStart) throw new Error("options.end < options.start"); } } // any further errors can either be caused by the zipfile, // or were introduced in a minor version of yauzl, // so should be passed to the client rather than thrown. if (!self.isOpen) return callback(new Error("closed")); if (entry.isEncrypted()) { if (options.decrypt !== false) return callback(new Error("entry is encrypted, and options.decrypt !== false")); } // make sure we don't lose the fd before we open the actual read stream self.reader.ref(); var buffer = new Buffer(30); readAndAssertNoEof(self.reader, buffer, 0, buffer.length, entry.relativeOffsetOfLocalHeader, function(err) { try { if (err) return callback(err); // 0 - Local file header signature = 0x04034b50 var signature = buffer.readUInt32LE(0); if (signature !== 0x04034b50) { return callback(new Error("invalid local file header signature: 0x" + signature.toString(16))); } // all this should be redundant // 4 - Version needed to extract (minimum) // 6 - General purpose bit flag // 8 - Compression method // 10 - File last modification time // 12 - File last modification date // 14 - CRC-32 // 18 - Compressed size // 22 - Uncompressed size // 26 - File name length (n) var fileNameLength = buffer.readUInt16LE(26); // 28 - Extra field length (m) var extraFieldLength = buffer.readUInt16LE(28); // 30 - File name // 30+n - Extra field var localFileHeaderEnd = entry.relativeOffsetOfLocalHeader + buffer.length + fileNameLength + extraFieldLength; var decompress; if (entry.compressionMethod === 0) { // 0 - The file is stored (no compression) decompress = false; } else if (entry.compressionMethod === 8) { // 8 - The file is Deflated decompress = options.decompress != null ? options.decompress : true; } else { return callback(new Error("unsupported compression method: " + entry.compressionMeth ...
...
zipfile.on("entry", function(entry) {
if (/\/$/.test(entry.fileName)) {
// Directory file names end with '/'.
// Note that entires for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
} else {
// file entry
zipfile.openReadStream(entry, function(err, readStream) {
if (err) throw err;
readStream.on("end", function() {
zipfile.readEntry();
});
readStream.pipe(somewhere);
});
}
...
readEntry = function () { var self = this; if (self.entryCount === self.entriesRead) { // done with metadata setImmediate(function() { if (self.autoClose) self.close(); if (self.emittedError) return; self.emit("end"); }); return; } if (self.emittedError) return; var buffer = new Buffer(46); readAndAssertNoEof(self.reader, buffer, 0, buffer.length, self.readEntryCursor, function(err) { if (err) return emitErrorAndAutoClose(self, err); if (self.emittedError) return; var entry = new Entry(); // 0 - Central directory file header signature var signature = buffer.readUInt32LE(0); if (signature !== 0x02014b50) return emitErrorAndAutoClose(self, new Error("invalid central directory file header signature: 0x" + signature.toString(16))); // 4 - Version made by entry.versionMadeBy = buffer.readUInt16LE(4); // 6 - Version needed to extract (minimum) entry.versionNeededToExtract = buffer.readUInt16LE(6); // 8 - General purpose bit flag entry.generalPurposeBitFlag = buffer.readUInt16LE(8); // 10 - Compression method entry.compressionMethod = buffer.readUInt16LE(10); // 12 - File last modification time entry.lastModFileTime = buffer.readUInt16LE(12); // 14 - File last modification date entry.lastModFileDate = buffer.readUInt16LE(14); // 16 - CRC-32 entry.crc32 = buffer.readUInt32LE(16); // 20 - Compressed size entry.compressedSize = buffer.readUInt32LE(20); // 24 - Uncompressed size entry.uncompressedSize = buffer.readUInt32LE(24); // 28 - File name length (n) entry.fileNameLength = buffer.readUInt16LE(28); // 30 - Extra field length (m) entry.extraFieldLength = buffer.readUInt16LE(30); // 32 - File comment length (k) entry.fileCommentLength = buffer.readUInt16LE(32); // 34 - Disk number where file starts // 36 - Internal file attributes entry.internalFileAttributes = buffer.readUInt16LE(36); // 38 - External file attributes entry.externalFileAttributes = buffer.readUInt32LE(38); // 42 - Relative offset of local file header entry.relativeOffsetOfLocalHeader = buffer.readUInt32LE(42); if (entry.generalPurposeBitFlag & 0x40) return emitErrorAndAutoClose(self, new Error("strong encryption is not supported")); self.readEntryCursor += 46; buffer = new Buffer(entry.fileNameLength + entry.extraFieldLength + entry.fileCommentLength); readAndAssertNoEof(self.reader, buffer, 0, buffer.length, self.readEntryCursor, function(err) { if (err) return emitErrorAndAutoClose(self, err); if (self.emittedError) return; // 46 - File name var isUtf8 = (entry.generalPurposeBitFlag & 0x800) !== 0; entry.fileName = self.decodeStrings ? decodeBuffer(buffer, 0, entry.fileNameLength, isUtf8) : buffer.slice(0, entry.fileNameLength); // 46+n - Extra field var fileCommentStart = entry.fileNameLength + entry.extraFieldLength; var extraFieldBuffer = buffer.slice(entry.fileNameLength, fileCommentStart); entry.extraFields = []; var i = 0; while (i < extraFieldBuffer.length - 3) { var headerId = extraFieldBuffer.readUInt16LE(i + 0); var dataSize = extraFieldBuffer.readUInt16LE(i + 2); var dataStart = i + 4; var dataEnd = dataStart + dataSize; if (dataEnd > extraFieldBuffer.length) return emitErrorAndAutoClose(self, new Error("extra field length exceeds extra field buffer size")); var dataBuffer = new Buffer(dataSize); extraFieldBuffer.copy(dataBuffer, 0, dataStart, dataEnd); entry.extraFields.push({ id: headerId, data: dataBuffer, }); i = dataEnd; } // 46+n+m - File comment entry.fileComment = self.decodeStrings ? decodeBuffer(buffer, fileCommentStart, fileCommentStart + entry.fileCommentLength , isUtf8) : buffer.slice(fileCommentStart, fileCommentStart + entry.fileCommentLength); // compatibility ...
...
## Usage
```js
var yauzl = require("yauzl");
yauzl.open("path/to/file.zip", {lazyEntries: true}, function(err, zipfile) {
if (err) throw err;
zipfile.readEntry();
zipfile.on("entry", function(entry) {
if (/\/$/.test(entry.fileName)) {
// Directory file names end with '/'.
// Note that entires for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
} else {
// file entry
...