Comparing version 0.0.0 to 1.0.0
205
index.js
var fs = require("fs"); | ||
var zlib = require("zlib"); | ||
var FdSlicer = require("fd-slicer"); | ||
var util = require("util"); | ||
var EventEmitter = require("events").EventEmitter; | ||
var Iconv = require("iconv").Iconv; | ||
@@ -14,7 +17,12 @@ exports.open = open; | ||
function open(path, callback) { | ||
function open(path, options, callback) { | ||
if (typeof options === "function") { | ||
callback = options; | ||
options = null; | ||
} | ||
if (options == null) options = {autoClose: true}; | ||
if (callback == null) callback = defaultCallback; | ||
fs.open(path, "r", function(err, fd) { | ||
if (err) return callback(err); | ||
fopen(fd, function(err, zipfile) { | ||
fopen(fd, options, function(err, zipfile) { | ||
if (err) fs.close(fd, defaultCallback); | ||
@@ -26,3 +34,8 @@ callback(err, zipfile); | ||
function fopen(fd, callback) { | ||
function fopen(fd, options, callback) { | ||
if (typeof options === "function") { | ||
callback = options; | ||
options = null; | ||
} | ||
if (options == null) options = {autoClose: false}; | ||
if (callback == null) callback = defaultCallback; | ||
@@ -66,7 +79,5 @@ fs.fstat(fd, function(err, stats) { | ||
// 22 - Comment | ||
var comment = new Buffer(commentLength); | ||
// the comment length is typcially 0. | ||
// copy from the original buffer to make sure we're not pinning it from being GC'ed. | ||
eocdrBuffer.copy(comment, 0, 22, eocdrBuffer.length); | ||
return callback(null, new ZipFile(fd, cdOffset, entryCount, comment)); | ||
// the encoding is always cp437. | ||
var comment = bufferToString(eocdrBuffer, 22, eocdrBuffer.length, false); | ||
return callback(null, new ZipFile(fd, cdOffset, entryCount, comment, options.autoClose)); | ||
} | ||
@@ -78,44 +89,49 @@ callback(new Error("end of central directory record signature not found")); | ||
function ZipFile(fd, cdOffset, entryCount, comment) { | ||
this.fdSlicer = new FdSlicer(fd); | ||
this.readEntryCursor = cdOffset; | ||
this.entryCount = entryCount; | ||
this.comment = comment; | ||
this.entriesRead = 0; | ||
this.isReadingEntry = false; | ||
util.inherits(ZipFile, EventEmitter); | ||
function ZipFile(fd, cdOffset, entryCount, comment, autoClose) { | ||
var self = this; | ||
EventEmitter.call(self); | ||
self.fdSlicer = new FdSlicer(fd, {autoClose: true}); | ||
self.fdSlicer.ref(); | ||
// forward close events | ||
self.fdSlicer.on("error", function(err) { | ||
// error closing the fd | ||
self.emit("error", err); | ||
}); | ||
self.fdSlicer.once("close", function() { | ||
self.emit("close"); | ||
}); | ||
self.readEntryCursor = cdOffset; | ||
self.entryCount = entryCount; | ||
self.comment = comment; | ||
self.entriesRead = 0; | ||
self.autoClose = !!autoClose; | ||
self.isOpen = true; | ||
// make sure events don't fire outta here until the client has a chance to attach listeners | ||
setImmediate(function() { readEntries(self); }); | ||
} | ||
ZipFile.prototype.close = function(callback) { | ||
if (callback == null) callback = defaultCallback; | ||
fs.close(this.fdSlicer.fd, callback); | ||
ZipFile.prototype.close = function() { | ||
if (!this.isOpen) return; | ||
this.isOpen = false; | ||
this.fdSlicer.unref(); | ||
}; | ||
ZipFile.prototype.readEntries = function(callback) { | ||
var self = this; | ||
if (callback == null) callback = defaultCallback; | ||
self.entries = []; | ||
// setImmediate here to make sure callback is called asynchronously even if there are 0 entries left. | ||
setImmediate(keepReading); | ||
function keepReading() { | ||
if (self.entriesRemaining() === 0) return callback(null, self.entries); | ||
self.readEntry(function(err, entry) { | ||
if (err) return callback(err); | ||
self.entries.push(entry); | ||
keepReading(); | ||
}); | ||
function emitErrorAndAutoClose(self, err) { | ||
if (self.autoClose) self.close(); | ||
self.emit("error", err); | ||
} | ||
function readEntries(self) { | ||
if (self.entryCount === self.entriesRead) { | ||
// done with metadata | ||
if (self.autoClose) self.close(); | ||
return self.emit("end"); | ||
} | ||
}; | ||
ZipFile.prototype.entriesRemaining = function() { | ||
return this.entryCount - this.entriesRead; | ||
}; | ||
ZipFile.prototype.readEntry = function(callback) { | ||
var self = this; | ||
if (self.isReadingEntry) throw new Error("readEntry already in progress"); | ||
self.isReadingEntry = true; | ||
if (callback == null) callback = defaultCallback; | ||
var buffer = new Buffer(46); | ||
readFdSlicerNoEof(this.fdSlicer, buffer, 0, buffer.length, this.readEntryCursor, function(err) { | ||
if (err) return callback(err); | ||
readFdSlicerNoEof(self.fdSlicer, buffer, 0, buffer.length, self.readEntryCursor, function(err) { | ||
if (err) return emitErrorAndAutoClose(self, err); | ||
var entry = new Entry(); | ||
// 0 - Central directory file header signature | ||
var signature = buffer.readUInt32LE(0); | ||
if (signature !== 0x02014b50) return callback(new Error("invalid central directory file header signature: 0x" + signature.toString(16))); | ||
if (signature !== 0x02014b50) return emitErrorAndAutoClose(self, new Error("invalid central directory file header signature: 0x" + signature.toString(16))); | ||
// 4 - Version made by | ||
@@ -157,7 +173,6 @@ entry.versionMadeBy = buffer.readUInt16LE(4); | ||
readFdSlicerNoEof(self.fdSlicer, buffer, 0, buffer.length, self.readEntryCursor, function(err) { | ||
if (err) return callback(err); | ||
if (err) return emitErrorAndAutoClose(self, err); | ||
// 46 - File name | ||
var encoding = entry.generalPurposeBitFlag & 0x800 ? "utf8" : "ascii"; | ||
// TODO: replace ascii with CP437 using https://github.com/bnoordhuis/node-iconv | ||
entry.fileName = buffer.toString(encoding, 0, entry.fileNameLength); | ||
var isUtf8 = entry.generalPurposeBitFlag & 0x800 | ||
entry.fileName = bufferToString(buffer, 0, entry.fileNameLength); | ||
@@ -184,53 +199,63 @@ // 46+n - Extra field | ||
// 46+n+m - File comment | ||
entry.fileComment = buffer.toString(encoding, fileCommentStart, fileCommentStart + entry.fileCommentLength); | ||
entry.fileComment = bufferToString(buffer, fileCommentStart, fileCommentStart + entry.fileCommentLength); | ||
self.readEntryCursor += buffer.length; | ||
self.entriesRead += 1; | ||
self.isReadingEntry = false; | ||
callback(null, entry); | ||
// validate file name | ||
if (entry.fileName.indexOf("\\") !== -1) return emitErrorAndAutoClose(self, new Error("invalid characters in fileName: " + entry.fileName)); | ||
if (/^[a-zA-Z]:/.exec(entry.fileName) || /^\//.exec(entry.fileName)) return emitErrorAndAutoClose(self, new Error("absolute path: " + entry.fileName)); | ||
self.emit("entry", entry); | ||
readEntries(self); | ||
}); | ||
}); | ||
}; | ||
} | ||
ZipFile.prototype.openReadStream = function(entry, callback) { | ||
var self = this; | ||
if (!self.isOpen) return self.emit("error", new Error("closed")); | ||
// make sure we don't lose the fd before we open the actual read stream | ||
self.fdSlicer.ref(); | ||
var buffer = new Buffer(30); | ||
readFdSlicerNoEof(self.fdSlicer, buffer, 0, buffer.length, entry.relativeOffsetOfLocalHeader, function(err) { | ||
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 filterStream = null; | ||
if (entry.compressionMethod === 0) { | ||
// 0 - The file is stored (no compression) | ||
} else if (entry.compressionMethod === 8) { | ||
// 8 - The file is Deflated | ||
filterStream = zlib.createInflateRaw(); | ||
} else { | ||
return callback(new Error("unsupported compression method: " + entry.compressionMethod)); | ||
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 filterStream = null; | ||
if (entry.compressionMethod === 0) { | ||
// 0 - The file is stored (no compression) | ||
} else if (entry.compressionMethod === 8) { | ||
// 8 - The file is Deflated | ||
filterStream = zlib.createInflateRaw(); | ||
} else { | ||
return callback(new Error("unsupported compression method: " + entry.compressionMethod)); | ||
} | ||
var fileDataStart = localFileHeaderEnd; | ||
var fileDataEnd = fileDataStart + entry.compressedSize; | ||
var stream = self.fdSlicer.createReadStream({start: fileDataStart, end: fileDataEnd}); | ||
if (filterStream != null) { | ||
stream = stream.pipe(filterStream); | ||
} | ||
callback(null, stream); | ||
} finally { | ||
self.fdSlicer.unref(); | ||
} | ||
var fileDataStart = localFileHeaderEnd; | ||
var fileDataEnd = fileDataStart + entry.compressedSize; | ||
var stream = self.fdSlicer.createReadStream({start: fileDataStart, end: fileDataEnd}); | ||
if (filterStream != null) { | ||
stream = stream.pipe(filterStream); | ||
} | ||
callback(null, stream); | ||
}); | ||
@@ -256,4 +281,12 @@ }; | ||
} | ||
var cp437_to_utf8 = new Iconv("cp437", "utf8"); | ||
function bufferToString(buffer, start, end, isUtf8) { | ||
if (isUtf8) { | ||
return buffer.toString("utf8", start, end); | ||
} else { | ||
return cp437_to_utf8.convert(buffer.slice(start, end)).toString("utf8"); | ||
} | ||
} | ||
function defaultCallback(err) { | ||
if (err) throw err; | ||
} |
{ | ||
"name": "yauzl", | ||
"version": "0.0.0", | ||
"version": "1.0.0", | ||
"description": "yet another unzip library for node", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "node test/test.js" | ||
}, | ||
"repository": { | ||
@@ -20,4 +23,6 @@ "type": "git", | ||
"dependencies": { | ||
"fd-slicer": "~0.2.1" | ||
"fd-slicer": "~0.2.1", | ||
"iconv": "~2.1.4", | ||
"pend": "~1.1.3" | ||
} | ||
} |
@@ -11,3 +11,3 @@ # yauzl | ||
* Don't block the JavaScript thread. | ||
Use and provide async apis. | ||
Use and provide async APIs. | ||
* Keep memory usage under control. | ||
@@ -20,13 +20,15 @@ Don't attempt to buffer entire files in RAM at once. | ||
var yauzl = require("yauzl"); | ||
var fs = require('fs'); | ||
var fs = require("fs"); | ||
yauzl.open("path/to/file.zip", function(err, zipfile) { | ||
if (err) throw err; | ||
zipfile.readEntries(function(err, entries) { | ||
if (err) throw err; | ||
entries.forEach(function(entry) { | ||
zipfile.openReadStream(entry, function(err, readStream) { | ||
if (err) throw err; | ||
readStream.pipe(fs.createWriteStream(entry.fileName)); | ||
}); | ||
zipfile.on("entry", function(entry) { | ||
if (/\/$/.exec(entry.fileName)) { | ||
// directory file names end with '/' | ||
return; | ||
} | ||
zipfile.openReadStream(entry, function(err, readStream) { | ||
if (err) throw err; | ||
// ensure parent directory exists, and then: | ||
readStream.pipe(fs.createWriteStream(entry.fileName)); | ||
}); | ||
@@ -47,8 +49,10 @@ }); | ||
### open(path, [callback]) | ||
### open(path, [options], [callback]) | ||
Calls `fs.open(path, "r")` and gives the `fd` and `callback` to `fopen` below. | ||
Calls `fs.open(path, "r")` and gives the `fd`, `options`, and `callback` to `fopen` below. | ||
### fopen(fd, [callback]) | ||
`options` may be omitted or `null` and defaults to `{autoClose: true}`. | ||
### fopen(fd, [options], [callback]) | ||
Reads from the fd, which is presumed to be an open .zip file. | ||
@@ -63,15 +67,30 @@ Note that random access is required by the zip file specification, | ||
### class ZipFile | ||
`options` may be omitted or `null` and defaults to `{autoClose: false}`. | ||
`autoClose` is effectively equivalent to: | ||
```js | ||
zipfile.once("end", function() { | ||
zipfile.close(); | ||
}); | ||
``` | ||
### Class: ZipFile | ||
The constructor for the class is not part of the public API. | ||
Use `open` or `fopen` instead. | ||
#### close([callback]) | ||
#### Event: "entry" | ||
Calls `fs.close(fd, callback)`. | ||
Callback gets `(entry)`, which is an `Entry`. | ||
#### readEntries([callback]) | ||
#### Event: "end" | ||
`callback` gets `(err, entries)`, where `entries` is an `Array` of `Entry` objects. | ||
Emitted after the last `entry` event has been emitted. | ||
#### Event: "close" | ||
Emitted after the fd is actually closed. | ||
This is after calling `close` (or after the `end` event when `autoClose` is `true`), | ||
and after all streams created from `openReadStream` have emitted their `end` events. | ||
#### openReadStream(entry, [callback]) | ||
@@ -83,18 +102,17 @@ | ||
the read stream provides the decompressed data. | ||
If this zipfile is already closed (see `close`), the `callback` will receive an `err`. | ||
#### entriesRemaining() | ||
#### close([callback]) | ||
Returns the number of entries in this `ZipFile` that have not yet been returned by `readEntry`. | ||
Causes all future calls to `openReadStream` to fail, | ||
and calls `fs.close(fd, callback)` after all streams created by `openReadStream` have emitted their `end` events. | ||
If this object's `end` event has not been emitted yet, this function causes undefined behavior. | ||
#### readEntry([callback]) | ||
If `autoClose` is `true` in the original `open` or `fopen` call, | ||
this function will be called automatically effectively in response to this object's `end` event. | ||
Most clients should use the `readEntries` function. | ||
`readEntry` and `entriesRemaining` provide low-level access for reading one entry at a time. | ||
This can be useful if the index were very large, and you wanted to start reading entries right away. | ||
#### isOpen | ||
Calling this function directly sabotages the `readEntries` function. | ||
You must not call this function before any previous call to this function completes by calling its `callback`. | ||
`Boolean`. `true` until `close` is called; then it's `false`. | ||
TODO: really? This API is super sketchy. | ||
#### entryCount | ||
@@ -106,5 +124,5 @@ | ||
`Buffer`. TODO: decode with `cp473`. | ||
`String`. Always decoded with `CP437` per the spec. | ||
### class Entry | ||
### Class: Entry | ||
@@ -114,3 +132,3 @@ Objects of this class represent Central Directory Records. | ||
These fields are numbers: | ||
These fields are of type `Number`: | ||
@@ -126,5 +144,5 @@ * `versionMadeBy` | ||
* `uncompressedSize` | ||
* `fileNameLength` | ||
* `extraFieldLength` | ||
* `fileCommentLength` | ||
* `fileNameLength` (bytes) | ||
* `extraFieldLength` (bytes) | ||
* `fileCommentLength` (bytes) | ||
* `internalFileAttributes` | ||
@@ -137,9 +155,9 @@ * `externalFileAttributes` | ||
`String`. | ||
The bytes in the file are decoded with `utf8` if `generalPurposeBitFlag & 0x800`, as per the spec. | ||
Otherwise, the file name is decoded with `ascii`, which is technically not correct. | ||
The correct default encoding is `cp473`. | ||
Following the spec, the bytes for the file name are decoded with | ||
`utf8` if `generalPurposeBitFlag & 0x800`, otherwise with `CP437`. | ||
#### extraFields | ||
`Array` with each entry in the form `{id: id, data: data}`, where `id` is a `Number` and `data` is a `Buffer`. | ||
`Array` with each entry in the form `{id: id, data: data}`, | ||
where `id` is a `Number` and `data` is a `Buffer`. | ||
None of the extra fields are considered significant by this library. | ||
@@ -153,10 +171,7 @@ | ||
### Default Charset Should Be CP437 | ||
Currently, the default charset is `ascii`, but it should be `cp473`. | ||
### No Multi-Disk Archive Support | ||
This library does not support multi-disk zip files. | ||
This API was intended for a zip file to span multiple floppy disks, which probably never happens now. | ||
The multi-disk fields in the zipfile spec were intended for a zip file to span multiple floppy disks, | ||
which probably never happens now. | ||
If the "number of this disk" field in the End of Central Directory Record is not `0`, | ||
@@ -163,0 +178,0 @@ the `open` or `fopen` `callback` will receive an `err`. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
268
0
230
20236
3
5
+ Addediconv@~2.1.4
+ Addedpend@~1.1.3
+ Addediconv@2.1.11(transitive)
+ Addednan@2.0.9(transitive)