Skip to content

Commit

Permalink
add decompress and decrypt options
Browse files Browse the repository at this point in the history
closes #11
closes #39
see also #38
  • Loading branch information
thejoshwolfe committed Apr 19, 2017
1 parent 4702ad8 commit 3fdd6e4
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 14 deletions.
71 changes: 65 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,43 @@ After calling this method, calling this method again before the response event h
Calling this method after the `end` event has been emitted will cause undefined behavior.
Calling this method after calling `close()` will cause undefined behavior.

#### openReadStream(entry, callback)
#### openReadStream(entry, [options], callback)

`entry` must be an `Entry` object from this `ZipFile`.
`callback` gets `(err, readStream)`, where `readStream` is a `Readable Stream`.
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,
}
```

If the entry is compressed (with a supported compression method),
and the `decompress` option is `true` (or omitted),
the read stream provides the decompressed data.
If this zipfile is already closed (see `close()`), the `callback` will receive an `err`.
Omitting the `decompress` option is what most clients should do.

It's possible for the `readStream` it to emit errors for several reasons.
The `decompress` option must be `null` (or omitted) when the entry is not compressed (see `isCompressed()`),
and either `true` (or omitted) or `false` when the entry is compressed.
Specifying `decompress: false` for a compressed entry causes the read stream
to provide the raw compressed file data without going through zlib.

If the entry is encrypted (see `isEncrypted()`), clients may want to avoid calling `openReadStream()` on the entry entirely.
Alternatively, clients may call `openReadStream()` for encrypted entries and specify `decrypt: false`.
If the entry is also compressed, clients must *also* specify `decompress: false`, or else the `callback` will receive an `err`.
Specifying `decrypt: false` for an encrypted entry causes the read stream to provide the raw encrypted file data.
(This data includes the 12-byte header described in the spec.)

The `decrypt` option must be `null` (or omitted) for non-encrypted entries, and `false` for encrypted entries.
Omitting the `decrypt` option (or specifying it as `null`) for an encrypted entry
will result in the `callback` receiving an `err`.
This default behavior is so that clients not accounting for encrypted files aren't surprised by bogus file data.

It's also possible for the `readStream` to emit errors for several reasons.
For example, if zlib cannot decompress the data, the zlib error will be emitted from the `readStream`.
Two more error cases (when `validateEntrySizes` is `true`) are if the decompressed data has too many
or too few actual bytes compared to the reported byte count from the entry's `uncompressedSize` field.
Expand Down Expand Up @@ -352,6 +380,29 @@ Effectively implemented as:
return dosDateTimeToDate(this.lastModFileDate, this.lastModFileTime);
```

#### isEncrypted()

Returns is this entry encrypted with "Traditional Encryption".
Effectively implemented as:

```js
return (this.generalPurposeBitFlag & 0x1) !== 0;
```

See `openReadStream()` for the implications of this value.

Note that "Strong Encryption" is not supported, and will result in an `"error"` event emitted from the `ZipFile`.

#### isCompressed()

Effectively implemented as:

```js
return this.compressionMethod === 8;
```

See `openReadStream()` for the implications of this value.

### Class: RandomAccessReader

This class is meant to be subclassed by clients and instantiated for the `fromRandomAccessReader()` function.
Expand Down Expand Up @@ -411,6 +462,8 @@ be sure to do the following:
* Attach a listener for the `error` event on any `ZipFile` object you get from `open()`, `fromFd()`, `fromBuffer()`, or `fromRandomAccessReader()`.
* Attach a listener for the `error` event on any stream you get from `openReadStream()`.

Minor version updates to yauzl will not add any additional requirements to this list.

## Limitations

### No Streaming Unzip API
Expand Down Expand Up @@ -454,10 +507,15 @@ By extension the following zip file fields are ignored by this library and not p
* Number of central directory records on this disk
* Disk number where file starts

### No Encryption Support
### Limited Encryption Handling

You can detect when a file entry is encrypted with "Traditional Encryption" via `isEncrypted()`,
but yauzl will not help you decrypt it.
See `openReadStream()`.

If a zip file contains file entries encrypted with "Strong Encryption", yauzl emits an error.

Currently, the presence of encryption is not even checked,
and encrypted zip files will cause undefined behavior.
If the central directory is encrypted or compressed, yauzl emits an error.

### Local File Headers Are Ignored

Expand Down Expand Up @@ -504,6 +562,7 @@ This library makes no attempt to interpret the Language Encoding Flag.
* 2.8.0
* Added option `validateEntrySizes`. [issue #53](https://github.com/thejoshwolfe/yauzl/issues/53)
* Added `examples/promises.js`
* Added ability to read raw file data via `decompress` and `decrypt` options. [issue #11](https://github.com/thejoshwolfe/yauzl/issues/11), [issue #38](https://github.com/thejoshwolfe/yauzl/issues/38), [pull #39](https://github.com/thejoshwolfe/yauzl/pull/39)
* 2.7.0
* Added option `decodeStrings`. [issue #42](https://github.com/thejoshwolfe/yauzl/issues/42)
* Fixed documentation for `entry.fileComment` and added compatibility alias. [issue #47](https://github.com/thejoshwolfe/yauzl/issues/47)
Expand Down
52 changes: 46 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ ZipFile.prototype.readEntry = function() {
// 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);
Expand Down Expand Up @@ -395,7 +397,12 @@ ZipFile.prototype.readEntry = function() {

// validate file size
if (self.validateEntrySizes && entry.compressionMethod === 0) {
if (entry.compressedSize !== entry.uncompressedSize) {
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));
}
Expand All @@ -412,9 +419,36 @@ ZipFile.prototype.readEntry = function() {
});
};

ZipFile.prototype.openReadStream = function(entry, callback) {
ZipFile.prototype.openReadStream = function(entry, options, callback) {
var self = this;
// parameter validation
if (callback == null) {
callback = options;
options = {};
} else {
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 (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);
}
}
}
// any further errors can be caused by the zipfile, 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"));
if (entry.isCompressed()) {
if (options.decompress !== false) return callback(new Error("entry is encrypted and compressed, and options.decompress !== false"));
}
}
// make sure we don't lose the fd before we open the actual read stream
self.reader.ref();
var buffer = new Buffer(30);
Expand Down Expand Up @@ -442,13 +476,13 @@ ZipFile.prototype.openReadStream = function(entry, callback) {
// 30 - File name
// 30+n - Extra field
var localFileHeaderEnd = entry.relativeOffsetOfLocalHeader + buffer.length + fileNameLength + extraFieldLength;
var compressed;
var decompress;
if (entry.compressionMethod === 0) {
// 0 - The file is stored (no compression)
compressed = false;
decompress = false;
} else if (entry.compressionMethod === 8) {
// 8 - The file is Deflated
compressed = true;
decompress = options.decompress != null ? options.decompress : true;
} else {
return callback(new Error("unsupported compression method: " + entry.compressionMethod));
}
Expand All @@ -465,7 +499,7 @@ ZipFile.prototype.openReadStream = function(entry, callback) {
}
var readStream = self.reader.createReadStream({start: fileDataStart, end: fileDataEnd});
var endpointStream = readStream;
if (compressed) {
if (decompress) {
var destroyed = false;
var inflateFilter = zlib.createInflateRaw();
readStream.on("error", function(err) {
Expand Down Expand Up @@ -510,6 +544,12 @@ function Entry() {
Entry.prototype.getLastModDate = function() {
return dosDateTimeToDate(this.lastModFileDate, this.lastModFileTime);
};
Entry.prototype.isEncrypted = function() {
return (this.generalPurposeBitFlag & 0x1) !== 0;
};
Entry.prototype.isCompressed = function() {
return this.compressionMethod === 8;
};

function dosDateTimeToDate(date, time) {
var day = date & 0x1f; // 1-31
Expand Down
Binary file added test/failure/strong encryption is not supported.zip
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions test/success/traditional-encryption-and-compression/a.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
��c.��,��"�s� IX�}Y`
Binary file added test/success/traditional-encryption.zip
Binary file not shown.
1 change: 1 addition & 0 deletions test/success/traditional-encryption/a.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
�(=�0�����X��&]b\
20 changes: 18 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,23 @@ listZipFiles([path.join(__dirname, "success"), path.join(__dirname, "wrong-entry
console.log(messagePrefix + "pass");
zipfile.readEntry();
} else {
zipfile.openReadStream(entry, function(err, readStream) {
var isEncrypted = entry.isEncrypted();
var isCompressed = entry.isCompressed();
if (/traditional-encryption/.test(zipfilePath) !== isEncrypted) {
throw new Error("expected traditional encryption in the traditional encryption test cases");
if (/traditional-encryption-and-compression/.test(zipfilePath) !== isCompressed) {
throw new Error("expected traditional encryption and compression in the traditional encryption and compression test case");
}
}
if (isEncrypted) {
zipfile.openReadStream(entry, {
decrypt: false,
decompress: isCompressed ? false : null,
}, onReadStream);
} else {
zipfile.openReadStream(entry, onReadStream);
}
function onReadStream(err, readStream) {
if (err) throw err;
var buffers = [];
readStream.on("data", function(data) {
Expand All @@ -112,7 +128,7 @@ listZipFiles([path.join(__dirname, "success"), path.join(__dirname, "wrong-entry
readStream.on("error", function(err) {
throw err;
});
});
}
}
});
zipfile.on("end", function() {
Expand Down

0 comments on commit 3fdd6e4

Please sign in to comment.