315 lines
8.5 KiB
JavaScript
315 lines
8.5 KiB
JavaScript
'use strict';
|
|
|
|
module.exports = Parse.create = Parse;
|
|
|
|
require("setimmediate");
|
|
var Transform = require('readable-stream/transform');
|
|
var inherits = require('util').inherits;
|
|
var zlib = require('zlib');
|
|
var binary = require('binary');
|
|
var PullStream = require('pullstream');
|
|
var MatchStream = require('match-stream');
|
|
var Entry = require('./entry');
|
|
|
|
inherits(Parse, Transform);
|
|
|
|
function Parse(opts) {
|
|
var self = this;
|
|
if (!(this instanceof Parse)) {
|
|
return new Parse(opts);
|
|
}
|
|
|
|
Transform.call(this, { lowWaterMark: 0 });
|
|
this._opts = opts || { verbose: false };
|
|
this._hasEntryListener = false;
|
|
|
|
this._pullStream = new PullStream();
|
|
this._pullStream.on("error", function (e) {
|
|
self.emit('error', e);
|
|
});
|
|
this._pullStream.once("end", function () {
|
|
self._streamEnd = true;
|
|
});
|
|
this._pullStream.once("finish", function () {
|
|
self._streamFinish = true;
|
|
});
|
|
|
|
this._readRecord();
|
|
}
|
|
|
|
Parse.prototype._readRecord = function () {
|
|
var self = this;
|
|
this._pullStream.pull(4, function (err, data) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var signature = data.readUInt32LE(0);
|
|
if (signature === 0x04034b50) {
|
|
self._readFile();
|
|
} else if (signature === 0x02014b50) {
|
|
self._readCentralDirectoryFileHeader();
|
|
} else if (signature === 0x06054b50) {
|
|
self._readEndOfCentralDirectoryRecord();
|
|
} else {
|
|
err = new Error('invalid signature: 0x' + signature.toString(16));
|
|
self.emit('error', err);
|
|
}
|
|
});
|
|
};
|
|
|
|
Parse.prototype._readFile = function () {
|
|
var self = this;
|
|
this._pullStream.pull(26, function (err, data) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
var vars = binary.parse(data)
|
|
.word16lu('versionsNeededToExtract')
|
|
.word16lu('flags')
|
|
.word16lu('compressionMethod')
|
|
.word16lu('lastModifiedTime')
|
|
.word16lu('lastModifiedDate')
|
|
.word32lu('crc32')
|
|
.word32lu('compressedSize')
|
|
.word32lu('uncompressedSize')
|
|
.word16lu('fileNameLength')
|
|
.word16lu('extraFieldLength')
|
|
.vars;
|
|
|
|
return self._pullStream.pull(vars.fileNameLength, function (err, fileName) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
fileName = fileName.toString('utf8');
|
|
var entry = new Entry();
|
|
entry.path = fileName;
|
|
entry.props.path = fileName;
|
|
entry.type = (vars.compressedSize === 0 && /[\/\\]$/.test(fileName)) ? 'Directory' : 'File';
|
|
|
|
if (self._opts.verbose) {
|
|
if (entry.type === 'Directory') {
|
|
console.log(' creating:', fileName);
|
|
} else if (entry.type === 'File') {
|
|
if (vars.compressionMethod === 0) {
|
|
console.log(' extracting:', fileName);
|
|
} else {
|
|
console.log(' inflating:', fileName);
|
|
}
|
|
}
|
|
}
|
|
|
|
var hasEntryListener = self._hasEntryListener;
|
|
if (hasEntryListener) {
|
|
self.emit('entry', entry);
|
|
}
|
|
|
|
self._pullStream.pull(vars.extraFieldLength, function (err, extraField) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
if (vars.compressionMethod === 0) {
|
|
self._pullStream.pull(vars.compressedSize, function (err, compressedData) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
if (hasEntryListener) {
|
|
entry.write(compressedData);
|
|
entry.end();
|
|
}
|
|
|
|
return self._readRecord();
|
|
});
|
|
} else {
|
|
var fileSizeKnown = !(vars.flags & 0x08);
|
|
|
|
var inflater = zlib.createInflateRaw();
|
|
inflater.on('error', function (err) {
|
|
self.emit('error', err);
|
|
});
|
|
|
|
if (fileSizeKnown) {
|
|
entry.size = vars.uncompressedSize;
|
|
if (hasEntryListener) {
|
|
entry.on('finish', self._readRecord.bind(self));
|
|
self._pullStream.pipe(vars.compressedSize, inflater).pipe(entry);
|
|
} else {
|
|
self._pullStream.drain(vars.compressedSize, function (err) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
self._readRecord();
|
|
});
|
|
}
|
|
} else {
|
|
var descriptorSig = new Buffer(4);
|
|
descriptorSig.writeUInt32LE(0x08074b50, 0);
|
|
|
|
var matchStream = new MatchStream({ pattern: descriptorSig }, function (buf, matched, extra) {
|
|
if (hasEntryListener) {
|
|
if (!matched) {
|
|
return this.push(buf);
|
|
}
|
|
this.push(buf);
|
|
}
|
|
setImmediate(function() {
|
|
self._pullStream.unpipe();
|
|
self._pullStream.prepend(extra);
|
|
self._processDataDescriptor(entry);
|
|
});
|
|
return this.push(null);
|
|
});
|
|
|
|
self._pullStream.pipe(matchStream);
|
|
if (hasEntryListener) {
|
|
matchStream.pipe(inflater).pipe(entry);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
Parse.prototype._processDataDescriptor = function (entry) {
|
|
var self = this;
|
|
this._pullStream.pull(16, function (err, data) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
var vars = binary.parse(data)
|
|
.word32lu('dataDescriptorSignature')
|
|
.word32lu('crc32')
|
|
.word32lu('compressedSize')
|
|
.word32lu('uncompressedSize')
|
|
.vars;
|
|
|
|
entry.size = vars.uncompressedSize;
|
|
self._readRecord();
|
|
});
|
|
};
|
|
|
|
Parse.prototype._readCentralDirectoryFileHeader = function () {
|
|
var self = this;
|
|
this._pullStream.pull(42, function (err, data) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
var vars = binary.parse(data)
|
|
.word16lu('versionMadeBy')
|
|
.word16lu('versionsNeededToExtract')
|
|
.word16lu('flags')
|
|
.word16lu('compressionMethod')
|
|
.word16lu('lastModifiedTime')
|
|
.word16lu('lastModifiedDate')
|
|
.word32lu('crc32')
|
|
.word32lu('compressedSize')
|
|
.word32lu('uncompressedSize')
|
|
.word16lu('fileNameLength')
|
|
.word16lu('extraFieldLength')
|
|
.word16lu('fileCommentLength')
|
|
.word16lu('diskNumber')
|
|
.word16lu('internalFileAttributes')
|
|
.word32lu('externalFileAttributes')
|
|
.word32lu('offsetToLocalFileHeader')
|
|
.vars;
|
|
|
|
return self._pullStream.pull(vars.fileNameLength, function (err, fileName) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
fileName = fileName.toString('utf8');
|
|
|
|
self._pullStream.pull(vars.extraFieldLength, function (err, extraField) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
self._pullStream.pull(vars.fileCommentLength, function (err, fileComment) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
return self._readRecord();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
Parse.prototype._readEndOfCentralDirectoryRecord = function () {
|
|
var self = this;
|
|
this._pullStream.pull(18, function (err, data) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
|
|
var vars = binary.parse(data)
|
|
.word16lu('diskNumber')
|
|
.word16lu('diskStart')
|
|
.word16lu('numberOfRecordsOnDisk')
|
|
.word16lu('numberOfRecords')
|
|
.word32lu('sizeOfCentralDirectory')
|
|
.word32lu('offsetToStartOfCentralDirectory')
|
|
.word16lu('commentLength')
|
|
.vars;
|
|
|
|
if (vars.commentLength) {
|
|
setImmediate(function() {
|
|
self._pullStream.pull(vars.commentLength, function (err, comment) {
|
|
if (err) {
|
|
return self.emit('error', err);
|
|
}
|
|
comment = comment.toString('utf8');
|
|
return self._pullStream.end();
|
|
});
|
|
});
|
|
|
|
} else {
|
|
self._pullStream.end();
|
|
}
|
|
});
|
|
};
|
|
|
|
Parse.prototype._transform = function (chunk, encoding, callback) {
|
|
if (this._pullStream.write(chunk)) {
|
|
return callback();
|
|
}
|
|
|
|
this._pullStream.once('drain', callback);
|
|
};
|
|
|
|
Parse.prototype.pipe = function (dest, opts) {
|
|
var self = this;
|
|
if (typeof dest.add === "function") {
|
|
self.on("entry", function (entry) {
|
|
dest.add(entry);
|
|
})
|
|
}
|
|
return Transform.prototype.pipe.apply(this, arguments);
|
|
};
|
|
|
|
Parse.prototype._flush = function (callback) {
|
|
if (!this._streamEnd || !this._streamFinish) {
|
|
return setImmediate(this._flush.bind(this, callback));
|
|
}
|
|
|
|
this.emit('close');
|
|
return callback();
|
|
};
|
|
|
|
Parse.prototype.addListener = function(type, listener) {
|
|
if ('entry' === type) {
|
|
this._hasEntryListener = true;
|
|
}
|
|
return Transform.prototype.addListener.call(this, type, listener);
|
|
};
|
|
|
|
Parse.prototype.on = Parse.prototype.addListener;
|