diff --git a/server/core/Zip/ZipReader.js b/server/core/Zip/ZipReader.js new file mode 100644 index 00000000..2ce35c18 --- /dev/null +++ b/server/core/Zip/ZipReader.js @@ -0,0 +1,59 @@ +const StreamUnzip = require('./node_stream_zip_changed'); +//const StreamUnzip = require('node-stream-zip'); + +class ZipReader { + constructor() { + this.zip = null; + } + + checkState() { + if (!this.zip) + throw new Error('Zip closed'); + } + + async open(zipFile, zipEntries = true) { + if (this.zip) + throw new Error('Zip file is already open'); + + const zip = new StreamUnzip.async({file: zipFile, skipEntryNameValidation: true}); + + if (zipEntries) + this.zipEntries = await zip.entries(); + + this.zip = zip; + } + + get entries() { + this.checkState(); + + return this.zipEntries; + } + + async extractToBuf(entryFilePath) { + this.checkState(); + + return await this.zip.entryData(entryFilePath); + } + + async extractToFile(entryFilePath, outputFile) { + this.checkState(); + + await this.zip.extract(entryFilePath, outputFile); + } + + async extractAllToDir(outputDir) { + this.checkState(); + + await this.zip.extract(null, outputDir); + } + + async close() { + if (this.zip) { + await this.zip.close(); + this.zip = null; + this.zipEntries = undefined; + } + } +} + +module.exports = ZipReader; \ No newline at end of file diff --git a/server/core/Zip/ZipStreamer.js b/server/core/Zip/ZipStreamer.js index 94e3d375..c4a83fa5 100644 --- a/server/core/Zip/ZipStreamer.js +++ b/server/core/Zip/ZipStreamer.js @@ -2,7 +2,7 @@ const path = require('path'); const zipStream = require('zip-stream');*/ -const unzipStream = require('./node_stream_zip'); +const StreamUnzip = require('./node_stream_zip_changed'); class ZipStreamer { constructor() { @@ -63,7 +63,7 @@ class ZipStreamer { decodeEntryNameCallback = false, } = options; - const unzip = new unzipStream({file: zipFile}); + const unzip = new StreamUnzip({file: zipFile, skipEntryNameValidation: true}); unzip.on('error', reject); diff --git a/server/core/Zip/node_stream_zip.js b/server/core/Zip/node_stream_zip.js index 81a40fc2..d95bbefb 100644 --- a/server/core/Zip/node_stream_zip.js +++ b/server/core/Zip/node_stream_zip.js @@ -1,194 +1,191 @@ /** - * @license node-stream-zip | (c) 2015 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE + * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE */ -// region Deps +let fs = require('fs'); +const util = require('util'); +const path = require('path'); +const events = require('events'); +const zlib = require('zlib'); +const stream = require('stream'); -var - util = require('util'), - fs = require('fs'), - path = require('path'), - events = require('events'), - zlib = require('zlib'), - stream = require('stream'); - -// endregion - -// region Constants - -var consts = { +const consts = { /* The local file header */ - LOCHDR : 30, // LOC header size - LOCSIG : 0x04034b50, // "PK\003\004" - LOCVER : 4, // version needed to extract - LOCFLG : 6, // general purpose bit flag - LOCHOW : 8, // compression method - LOCTIM : 10, // modification time (2 bytes time, 2 bytes date) - LOCCRC : 14, // uncompressed file crc-32 value - LOCSIZ : 18, // compressed size - LOCLEN : 22, // uncompressed size - LOCNAM : 26, // filename length - LOCEXT : 28, // extra field length + LOCHDR: 30, // LOC header size + LOCSIG: 0x04034b50, // "PK\003\004" + LOCVER: 4, // version needed to extract + LOCFLG: 6, // general purpose bit flag + LOCHOW: 8, // compression method + LOCTIM: 10, // modification time (2 bytes time, 2 bytes date) + LOCCRC: 14, // uncompressed file crc-32 value + LOCSIZ: 18, // compressed size + LOCLEN: 22, // uncompressed size + LOCNAM: 26, // filename length + LOCEXT: 28, // extra field length /* The Data descriptor */ - EXTSIG : 0x08074b50, // "PK\007\008" - EXTHDR : 16, // EXT header size - EXTCRC : 4, // uncompressed file crc-32 value - EXTSIZ : 8, // compressed size - EXTLEN : 12, // uncompressed size + EXTSIG: 0x08074b50, // "PK\007\008" + EXTHDR: 16, // EXT header size + EXTCRC: 4, // uncompressed file crc-32 value + EXTSIZ: 8, // compressed size + EXTLEN: 12, // uncompressed size /* The central directory file header */ - CENHDR : 46, // CEN header size - CENSIG : 0x02014b50, // "PK\001\002" - CENVEM : 4, // version made by - CENVER : 6, // version needed to extract - CENFLG : 8, // encrypt, decrypt flags - CENHOW : 10, // compression method - CENTIM : 12, // modification time (2 bytes time, 2 bytes date) - CENCRC : 16, // uncompressed file crc-32 value - CENSIZ : 20, // compressed size - CENLEN : 24, // uncompressed size - CENNAM : 28, // filename length - CENEXT : 30, // extra field length - CENCOM : 32, // file comment length - CENDSK : 34, // volume number start - CENATT : 36, // internal file attributes - CENATX : 38, // external file attributes (host system dependent) - CENOFF : 42, // LOC header offset + CENHDR: 46, // CEN header size + CENSIG: 0x02014b50, // "PK\001\002" + CENVEM: 4, // version made by + CENVER: 6, // version needed to extract + CENFLG: 8, // encrypt, decrypt flags + CENHOW: 10, // compression method + CENTIM: 12, // modification time (2 bytes time, 2 bytes date) + CENCRC: 16, // uncompressed file crc-32 value + CENSIZ: 20, // compressed size + CENLEN: 24, // uncompressed size + CENNAM: 28, // filename length + CENEXT: 30, // extra field length + CENCOM: 32, // file comment length + CENDSK: 34, // volume number start + CENATT: 36, // internal file attributes + CENATX: 38, // external file attributes (host system dependent) + CENOFF: 42, // LOC header offset /* The entries in the end of central directory */ - ENDHDR : 22, // END header size - ENDSIG : 0x06054b50, // "PK\005\006" - ENDSIGFIRST : 0x50, - ENDSUB : 8, // number of entries on this disk - ENDTOT : 10, // total number of entries - ENDSIZ : 12, // central directory size in bytes - ENDOFF : 16, // offset of first CEN header - ENDCOM : 20, // zip file comment length - MAXFILECOMMENT : 0xFFFF, + ENDHDR: 22, // END header size + ENDSIG: 0x06054b50, // "PK\005\006" + ENDSIGFIRST: 0x50, + ENDSUB: 8, // number of entries on this disk + ENDTOT: 10, // total number of entries + ENDSIZ: 12, // central directory size in bytes + ENDOFF: 16, // offset of first CEN header + ENDCOM: 20, // zip file comment length + MAXFILECOMMENT: 0xffff, /* The entries in the end of ZIP64 central directory locator */ - ENDL64HDR : 20, // ZIP64 end of central directory locator header size - ENDL64SIG : 0x07064b50, // ZIP64 end of central directory locator signature - ENDL64SIGFIRST : 0x50, - ENDL64OFS : 8, // ZIP64 end of central directory offset + ENDL64HDR: 20, // ZIP64 end of central directory locator header size + ENDL64SIG: 0x07064b50, // ZIP64 end of central directory locator signature + ENDL64SIGFIRST: 0x50, + ENDL64OFS: 8, // ZIP64 end of central directory offset /* The entries in the end of ZIP64 central directory */ - END64HDR : 56, // ZIP64 end of central directory header size - END64SIG : 0x06064b50, // ZIP64 end of central directory signature - END64SIGFIRST : 0x50, - END64SUB : 24, // number of entries on this disk - END64TOT : 32, // total number of entries - END64SIZ : 40, - END64OFF : 48, + END64HDR: 56, // ZIP64 end of central directory header size + END64SIG: 0x06064b50, // ZIP64 end of central directory signature + END64SIGFIRST: 0x50, + END64SUB: 24, // number of entries on this disk + END64TOT: 32, // total number of entries + END64SIZ: 40, + END64OFF: 48, /* Compression methods */ - STORED : 0, // no compression - SHRUNK : 1, // shrunk - REDUCED1 : 2, // reduced with compression factor 1 - REDUCED2 : 3, // reduced with compression factor 2 - REDUCED3 : 4, // reduced with compression factor 3 - REDUCED4 : 5, // reduced with compression factor 4 - IMPLODED : 6, // imploded + STORED: 0, // no compression + SHRUNK: 1, // shrunk + REDUCED1: 2, // reduced with compression factor 1 + REDUCED2: 3, // reduced with compression factor 2 + REDUCED3: 4, // reduced with compression factor 3 + REDUCED4: 5, // reduced with compression factor 4 + IMPLODED: 6, // imploded // 7 reserved - DEFLATED : 8, // deflated - ENHANCED_DEFLATED: 9, // enhanced deflated - PKWARE : 10,// PKWare DCL imploded + DEFLATED: 8, // deflated + ENHANCED_DEFLATED: 9, // deflate64 + PKWARE: 10, // PKWare DCL imploded // 11 reserved - BZIP2 : 12, // compressed using BZIP2 + BZIP2: 12, // compressed using BZIP2 // 13 reserved - LZMA : 14, // LZMA + LZMA: 14, // LZMA // 15-17 reserved - IBM_TERSE : 18, // compressed using IBM TERSE - IBM_LZ77 : 19, //IBM LZ77 z + IBM_TERSE: 18, // compressed using IBM TERSE + IBM_LZ77: 19, //IBM LZ77 z /* General purpose bit flag */ - FLG_ENC : 0, // encrypted file - FLG_COMP1 : 1, // compression option - FLG_COMP2 : 2, // compression option - FLG_DESC : 4, // data descriptor - FLG_ENH : 8, // enhanced deflation - FLG_STR : 16, // strong encryption - FLG_LNG : 1024, // language encoding - FLG_MSK : 4096, // mask header values - FLG_ENTRY_ENC : 1, + FLG_ENC: 0, // encrypted file + FLG_COMP1: 1, // compression option + FLG_COMP2: 2, // compression option + FLG_DESC: 4, // data descriptor + FLG_ENH: 8, // enhanced deflation + FLG_STR: 16, // strong encryption + FLG_LNG: 1024, // language encoding + FLG_MSK: 4096, // mask header values + FLG_ENTRY_ENC: 1, /* 4.5 Extensible data fields */ - EF_ID : 0, - EF_SIZE : 2, + EF_ID: 0, + EF_SIZE: 2, /* Header IDs */ - ID_ZIP64 : 0x0001, - ID_AVINFO : 0x0007, - ID_PFS : 0x0008, - ID_OS2 : 0x0009, - ID_NTFS : 0x000a, - ID_OPENVMS : 0x000c, - ID_UNIX : 0x000d, - ID_FORK : 0x000e, - ID_PATCH : 0x000f, - ID_X509_PKCS7 : 0x0014, - ID_X509_CERTID_F : 0x0015, - ID_X509_CERTID_C : 0x0016, - ID_STRONGENC : 0x0017, - ID_RECORD_MGT : 0x0018, - ID_X509_PKCS7_RL : 0x0019, - ID_IBM1 : 0x0065, - ID_IBM2 : 0x0066, - ID_POSZIP : 0x4690, + ID_ZIP64: 0x0001, + ID_AVINFO: 0x0007, + ID_PFS: 0x0008, + ID_OS2: 0x0009, + ID_NTFS: 0x000a, + ID_OPENVMS: 0x000c, + ID_UNIX: 0x000d, + ID_FORK: 0x000e, + ID_PATCH: 0x000f, + ID_X509_PKCS7: 0x0014, + ID_X509_CERTID_F: 0x0015, + ID_X509_CERTID_C: 0x0016, + ID_STRONGENC: 0x0017, + ID_RECORD_MGT: 0x0018, + ID_X509_PKCS7_RL: 0x0019, + ID_IBM1: 0x0065, + ID_IBM2: 0x0066, + ID_POSZIP: 0x4690, - EF_ZIP64_OR_32 : 0xffffffff, - EF_ZIP64_OR_16 : 0xffff + EF_ZIP64_OR_32: 0xffffffff, + EF_ZIP64_OR_16: 0xffff, }; -// endregion - -// region StreamZip - -var StreamZip = function(config) { - var - fd, - fileSize, - chunkSize, - ready = false, +const StreamZip = function (config) { + let fd, fileSize, chunkSize, op, centralDirectory, closed; + const ready = false, that = this, - op, - centralDirectory, - closed, - entries = config.storeEntries !== false ? {} : null, - fileName = config.file; + fileName = config.file, + textDecoder = config.nameEncoding ? new TextDecoder(config.nameEncoding) : null; open(); function open() { - fs.open(fileName, 'r', function(err, f) { - if (err) - return that.emit('error', err); - fd = f; - fs.fstat(fd, function(err, stat) { - if (err) + if (config.fd) { + fd = config.fd; + readFile(); + } else { + fs.open(fileName, 'r', (err, f) => { + if (err) { return that.emit('error', err); - fileSize = stat.size; - chunkSize = config.chunkSize || Math.round(fileSize / 1000); - chunkSize = Math.max(Math.min(chunkSize, Math.min(128*1024, fileSize)), Math.min(1024, fileSize)); - readCentralDirectory(); + } + fd = f; + readFile(); }); + } + } + + function readFile() { + fs.fstat(fd, (err, stat) => { + if (err) { + return that.emit('error', err); + } + fileSize = stat.size; + chunkSize = config.chunkSize || Math.round(fileSize / 1000); + chunkSize = Math.max( + Math.min(chunkSize, Math.min(128 * 1024, fileSize)), + Math.min(1024, fileSize) + ); + readCentralDirectory(); }); } function readUntilFoundCallback(err, bytesRead) { - if (err || !bytesRead) - return that.emit('error', err || 'Archive read error'); - var - buffer = op.win.buffer, - pos = op.lastPos, - bufferPosition = pos - op.win.position, - minPos = op.minPos; + if (err || !bytesRead) { + return that.emit('error', err || new Error('Archive read error')); + } + let pos = op.lastPos; + let bufferPosition = pos - op.win.position; + const buffer = op.win.buffer; + const minPos = op.minPos; while (--pos >= minPos && --bufferPosition >= 0) { - if (buffer.length - bufferPosition >= 4 && - buffer[bufferPosition] === op.firstByte) { // quick check first signature byte + if (buffer.length - bufferPosition >= 4 && buffer[bufferPosition] === op.firstByte) { + // quick check first signature byte if (buffer.readUInt32LE(bufferPosition) === op.sig) { op.lastBufferPosition = bufferPosition; op.lastBytesRead = bytesRead; @@ -198,48 +195,57 @@ var StreamZip = function(config) { } } if (pos === minPos) { - return that.emit('error', 'Bad archive'); + return that.emit('error', new Error('Bad archive')); } op.lastPos = pos + 1; op.chunkSize *= 2; - if (pos <= minPos) - return that.emit('error', 'Bad archive'); - var expandLength = Math.min(op.chunkSize, pos - minPos); + if (pos <= minPos) { + return that.emit('error', new Error('Bad archive')); + } + const expandLength = Math.min(op.chunkSize, pos - minPos); op.win.expandLeft(expandLength, readUntilFoundCallback); - } function readCentralDirectory() { - var totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, fileSize); + const totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, fileSize); op = { win: new FileWindowBuffer(fd), - totalReadLength: totalReadLength, + totalReadLength, minPos: fileSize - totalReadLength, lastPos: fileSize, chunkSize: Math.min(1024, chunkSize), firstByte: consts.ENDSIGFIRST, sig: consts.ENDSIG, - complete: readCentralDirectoryComplete + complete: readCentralDirectoryComplete, }; op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback); } function readCentralDirectoryComplete() { - var buffer = op.win.buffer; - var pos = op.lastBufferPosition; + const buffer = op.win.buffer; + const pos = op.lastBufferPosition; try { centralDirectory = new CentralDirectoryHeader(); centralDirectory.read(buffer.slice(pos, pos + consts.ENDHDR)); centralDirectory.headerOffset = op.win.position + pos; - if (centralDirectory.commentLength) - that.comment = buffer.slice(pos + consts.ENDHDR, - pos + consts.ENDHDR + centralDirectory.commentLength).toString(); - else + if (centralDirectory.commentLength) { + that.comment = buffer + .slice( + pos + consts.ENDHDR, + pos + consts.ENDHDR + centralDirectory.commentLength + ) + .toString(); + } else { that.comment = null; + } that.entriesCount = centralDirectory.volumeEntries; that.centralDirectory = centralDirectory; - if (centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 && centralDirectory.totalEntries === consts.EF_ZIP64_OR_16 - || centralDirectory.size === consts.EF_ZIP64_OR_32 || centralDirectory.offset === consts.EF_ZIP64_OR_32) { + if ( + (centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 && + centralDirectory.totalEntries === consts.EF_ZIP64_OR_16) || + centralDirectory.size === consts.EF_ZIP64_OR_32 || + centralDirectory.offset === consts.EF_ZIP64_OR_32 + ) { readZip64CentralDirectoryLocator(); } else { op = {}; @@ -251,7 +257,7 @@ var StreamZip = function(config) { } function readZip64CentralDirectoryLocator() { - var length = consts.ENDL64HDR; + const length = consts.ENDL64HDR; if (op.lastBufferPosition > length) { op.lastBufferPosition -= length; readZip64CentralDirectoryLocatorComplete(); @@ -264,17 +270,19 @@ var StreamZip = function(config) { chunkSize: op.chunkSize, firstByte: consts.ENDL64SIGFIRST, sig: consts.ENDL64SIG, - complete: readZip64CentralDirectoryLocatorComplete + complete: readZip64CentralDirectoryLocatorComplete, }; op.win.read(op.lastPos - op.chunkSize, op.chunkSize, readUntilFoundCallback); } } function readZip64CentralDirectoryLocatorComplete() { - var buffer = op.win.buffer; - var locHeader = new CentralDirectoryLoc64Header(); - locHeader.read(buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.ENDL64HDR)); - var readLength = fileSize - locHeader.headerOffset; + const buffer = op.win.buffer; + const locHeader = new CentralDirectoryLoc64Header(); + locHeader.read( + buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.ENDL64HDR) + ); + const readLength = fileSize - locHeader.headerOffset; op = { win: op.win, totalReadLength: readLength, @@ -283,14 +291,14 @@ var StreamZip = function(config) { chunkSize: op.chunkSize, firstByte: consts.END64SIGFIRST, sig: consts.END64SIG, - complete: readZip64CentralDirectoryComplete + complete: readZip64CentralDirectoryComplete, }; op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback); } function readZip64CentralDirectoryComplete() { - var buffer = op.win.buffer; - var zip64cd = new CentralDirectoryZip64Header(); + const buffer = op.win.buffer; + const zip64cd = new CentralDirectoryZip64Header(); zip64cd.read(buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.END64HDR)); that.centralDirectory.volumeEntries = zip64cd.volumeEntries; that.centralDirectory.totalEntries = zip64cd.totalEntries; @@ -305,20 +313,20 @@ var StreamZip = function(config) { op = { win: new FileWindowBuffer(fd), pos: centralDirectory.offset, - chunkSize: chunkSize, - entriesLeft: centralDirectory.volumeEntries + chunkSize, + entriesLeft: centralDirectory.volumeEntries, }; op.win.read(op.pos, Math.min(chunkSize, fileSize - op.pos), readEntriesCallback); } function readEntriesCallback(err, bytesRead) { - if (err || !bytesRead) - return that.emit('error', err || 'Entries read error'); - var - buffer = op.win.buffer, - bufferPos = op.pos - op.win.position, - bufferLength = buffer.length, - entry = op.entry; + if (err || !bytesRead) { + return that.emit('error', err || new Error('Entries read error')); + } + let bufferPos = op.pos - op.win.position; + let entry = op.entry; + const buffer = op.win.buffer; + const bufferLength = buffer.length; try { while (op.entriesLeft > 0) { if (!entry) { @@ -329,19 +337,20 @@ var StreamZip = function(config) { op.pos += consts.CENHDR; bufferPos += consts.CENHDR; } - var entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen; - var advanceBytes = entryHeaderSize + (op.entriesLeft > 1 ? consts.CENHDR : 0); + const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen; + const advanceBytes = entryHeaderSize + (op.entriesLeft > 1 ? consts.CENHDR : 0); if (bufferLength - bufferPos < advanceBytes) { op.win.moveRight(chunkSize, readEntriesCallback, bufferPos); op.move = true; return; } - entry.read(buffer, bufferPos); + entry.read(buffer, bufferPos, textDecoder); if (!config.skipEntryNameValidation) { entry.validateName(); } - if (entries) + if (entries) { entries[entry.name] = entry; + } that.emit('entry', entry); op.entry = entry = null; op.entriesLeft--; @@ -355,95 +364,118 @@ var StreamZip = function(config) { } function checkEntriesExist() { - if (!entries) + if (!entries) { throw new Error('storeEntries disabled'); + } } - Object.defineProperty(this, 'ready', { get: function() { return ready; } }); + Object.defineProperty(this, 'ready', { + get() { + return ready; + }, + }); - this.entry = function(name) { + this.entry = function (name) { checkEntriesExist(); return entries[name]; }; - this.entries = function() { + this.entries = function () { checkEntriesExist(); return entries; }; - this.stream = function(entry, callback) { - return this.openEntry(entry, function(err, entry) { - if (err) - return callback(err); - var offset = dataOffset(entry); - var entryStream = new EntryDataReaderStream(fd, offset, entry.compressedSize); - if (entry.method === consts.STORED) { - } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) { - entryStream = entryStream.pipe(zlib.createInflateRaw()); - } else { - return callback('Unknown compression method: ' + entry.method); - } - if (canVerifyCrc(entry)) - entryStream = entryStream.pipe(new EntryVerifyStream(entryStream, entry.crc, entry.size)); - callback(null, entryStream); - }, false); + this.stream = function (entry, callback) { + return this.openEntry( + entry, + (err, entry) => { + if (err) { + return callback(err); + } + const offset = dataOffset(entry); + let entryStream = new EntryDataReaderStream(fd, offset, entry.compressedSize); + if (entry.method === consts.STORED) { + // nothing to do + } else if (entry.method === consts.DEFLATED) { + entryStream = entryStream.pipe(zlib.createInflateRaw()); + } else { + return callback(new Error('Unknown compression method: ' + entry.method)); + } + if (canVerifyCrc(entry)) { + entryStream = entryStream.pipe( + new EntryVerifyStream(entryStream, entry.crc, entry.size) + ); + } + callback(null, entryStream); + }, + false + ); }; - this.entryDataSync = function(entry) { - var err = null; - this.openEntry(entry, function(e, en) { - err = e; - entry = en; - }, true); - if (err) + this.entryDataSync = function (entry) { + let err = null; + this.openEntry( + entry, + (e, en) => { + err = e; + entry = en; + }, + true + ); + if (err) { throw err; - var - data = Buffer.alloc(entry.compressedSize), - bytesRead; - new FsRead(fd, data, 0, entry.compressedSize, dataOffset(entry), function(e, br) { + } + let data = Buffer.alloc(entry.compressedSize); + new FsRead(fd, data, 0, entry.compressedSize, dataOffset(entry), (e) => { err = e; - bytesRead = br; }).read(true); - if (err) + if (err) { throw err; + } if (entry.method === consts.STORED) { + // nothing to do } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) { data = zlib.inflateRawSync(data); } else { throw new Error('Unknown compression method: ' + entry.method); } - if (data.length !== entry.size) + if (data.length !== entry.size) { throw new Error('Invalid size'); + } if (canVerifyCrc(entry)) { - var verify = new CrcVerify(entry.crc, entry.size); + const verify = new CrcVerify(entry.crc, entry.size); verify.data(data); } return data; }; - this.openEntry = function(entry, callback, sync) { + this.openEntry = function (entry, callback, sync) { if (typeof entry === 'string') { checkEntriesExist(); entry = entries[entry]; - if (!entry) - return callback('Entry not found'); + if (!entry) { + return callback(new Error('Entry not found')); + } } - if (!entry.isFile) - return callback('Entry is not file'); - if (!fd) - return callback('Archive closed'); - var buffer = Buffer.alloc(consts.LOCHDR); - new FsRead(fd, buffer, 0, buffer.length, entry.offset, function(err) { - if (err) + if (!entry.isFile) { + return callback(new Error('Entry is not file')); + } + if (!fd) { + return callback(new Error('Archive closed')); + } + const buffer = Buffer.alloc(consts.LOCHDR); + new FsRead(fd, buffer, 0, buffer.length, entry.offset, (err) => { + if (err) { return callback(err); - var readEx; + } + let readEx; try { entry.readDataHeader(buffer); if (entry.encrypted) { - readEx = 'Entry encrypted'; + readEx = new Error('Entry encrypted'); } } catch (ex) { - readEx = ex + readEx = ex; } callback(readEx, entry); }).read(sync); @@ -455,38 +487,40 @@ var StreamZip = function(config) { function canVerifyCrc(entry) { // if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written - return (entry.flags & 0x8) != 0x8; + return (entry.flags & 0x8) !== 0x8; } function extract(entry, outPath, callback) { - that.stream(entry, function (err, stm) { + that.stream(entry, (err, stm) => { if (err) { callback(err); } else { - var fsStm, errThrown; - stm.on('error', function(err) { + let fsStm, errThrown; + stm.on('error', (err) => { errThrown = err; if (fsStm) { stm.unpipe(fsStm); - fsStm.close(function () { + fsStm.close(() => { callback(err); }); } }); - fs.open(outPath, 'w', function(err, fdFile) { - if (err) - return callback(err || errThrown); + fs.open(outPath, 'w', (err, fdFile) => { + if (err) { + return callback(err); + } if (errThrown) { - fs.close(fd, function() { + fs.close(fd, () => { callback(errThrown); }); return; } fsStm = fs.createWriteStream(outPath, { fd: fdFile }); - fsStm.on('finish', function() { + fsStm.on('finish', () => { that.emit('extract', entry, outPath); - if (!errThrown) + if (!errThrown) { callback(); + } }); stm.pipe(fsStm); }); @@ -495,58 +529,71 @@ var StreamZip = function(config) { } function createDirectories(baseDir, dirs, callback) { - if (!dirs.length) + if (!dirs.length) { return callback(); - var dir = dirs.shift(); - dir = path.join(baseDir, path.join.apply(path, dir)); - fs.mkdir(dir, function(err) { - if (err && err.code !== 'EEXIST') + } + let dir = dirs.shift(); + dir = path.join(baseDir, path.join(...dir)); + fs.mkdir(dir, { recursive: true }, (err) => { + if (err && err.code !== 'EEXIST') { return callback(err); + } createDirectories(baseDir, dirs, callback); }); } function extractFiles(baseDir, baseRelPath, files, callback, extractedCount) { - if (!files.length) + if (!files.length) { return callback(null, extractedCount); - var file = files.shift(); - var targetPath = path.join(baseDir, file.name.replace(baseRelPath, '')); - extract(file, targetPath, function (err) { - if (err) + } + const file = files.shift(); + const targetPath = path.join(baseDir, file.name.replace(baseRelPath, '')); + extract(file, targetPath, (err) => { + if (err) { return callback(err, extractedCount); + } extractFiles(baseDir, baseRelPath, files, callback, extractedCount + 1); }); } - this.extract = function(entry, outPath, callback) { - var entryName = entry || ''; + this.extract = function (entry, outPath, callback) { + let entryName = entry || ''; if (typeof entry === 'string') { entry = this.entry(entry); if (entry) { entryName = entry.name; } else { - if (entryName.length && entryName[entryName.length - 1] !== '/') + if (entryName.length && entryName[entryName.length - 1] !== '/') { entryName += '/'; + } } } if (!entry || entry.isDirectory) { - var files = [], dirs = [], allDirs = {}; - for (var e in entries) { - if (Object.prototype.hasOwnProperty.call(entries, e) && e.lastIndexOf(entryName, 0) === 0) { - var relPath = e.replace(entryName, ''); - var childEntry = entries[e]; + const files = [], + dirs = [], + allDirs = {}; + for (const e in entries) { + if ( + Object.prototype.hasOwnProperty.call(entries, e) && + e.lastIndexOf(entryName, 0) === 0 + ) { + let relPath = e.replace(entryName, ''); + const childEntry = entries[e]; if (childEntry.isFile) { files.push(childEntry); relPath = path.dirname(relPath); } if (relPath && !allDirs[relPath] && relPath !== '.') { allDirs[relPath] = true; - var parts = relPath.split('/').filter(function (f) { return f; }); - if (parts.length) + let parts = relPath.split('/').filter((f) => { + return f; + }); + if (parts.length) { dirs.push(parts); + } while (parts.length > 1) { parts = parts.slice(0, parts.length - 1); - var partsPath = parts.join('/'); + const partsPath = parts.join('/'); if (allDirs[partsPath] || partsPath === '.') { break; } @@ -556,362 +603,451 @@ var StreamZip = function(config) { } } } - dirs.sort(function(x, y) { return x.length - y.length; }); + dirs.sort((x, y) => { + return x.length - y.length; + }); if (dirs.length) { - createDirectories(outPath, dirs, function (err) { - if (err) + createDirectories(outPath, dirs, (err) => { + if (err) { callback(err); - else + } else { extractFiles(outPath, entryName, files, callback, 0); + } }); } else { extractFiles(outPath, entryName, files, callback, 0); } } else { - fs.stat(outPath, function(err, stat) { - if (stat && stat.isDirectory()) + fs.stat(outPath, (err, stat) => { + if (stat && stat.isDirectory()) { extract(entry, path.join(outPath, path.basename(entry.name)), callback); - else + } else { extract(entry, outPath, callback); + } }); } }; - this.close = function(callback) { - if (closed) { - if (callback) + this.close = function (callback) { + if (closed || !fd) { + closed = true; + if (callback) { callback(); + } } else { closed = true; - fs.close(fd, function(err) { + fs.close(fd, (err) => { fd = null; - if (callback) + if (callback) { callback(err); + } }); } }; - var originalEmit = events.EventEmitter.prototype.emit; - this.emit = function() { + const originalEmit = events.EventEmitter.prototype.emit; + this.emit = function (...args) { if (!closed) { - return originalEmit.apply(this, arguments); + return originalEmit.call(this, ...args); } }; }; -StreamZip.setFs = function(customFs) { +StreamZip.setFs = function (customFs) { fs = customFs; }; +StreamZip.debugLog = (...args) => { + if (StreamZip.debug) { + // eslint-disable-next-line no-console + console.log(...args); + } +}; + util.inherits(StreamZip, events.EventEmitter); -// endregion +const propZip = Symbol('zip'); -// region CentralDirectoryHeader +StreamZip.async = class StreamZipAsync extends events.EventEmitter { + constructor(config) { + super(); -var CentralDirectoryHeader = function() { + const zip = new StreamZip(config); + + zip.on('entry', (entry) => this.emit('entry', entry)); + zip.on('extract', (entry, outPath) => this.emit('extract', entry, outPath)); + + this[propZip] = new Promise((resolve, reject) => { + zip.on('ready', () => { + zip.removeListener('error', reject); + resolve(zip); + }); + zip.on('error', reject); + }); + } + + get entriesCount() { + return this[propZip].then((zip) => zip.entriesCount); + } + + get comment() { + return this[propZip].then((zip) => zip.comment); + } + + async entry(name) { + const zip = await this[propZip]; + return zip.entry(name); + } + + async entries() { + const zip = await this[propZip]; + return zip.entries(); + } + + async stream(entry) { + const zip = await this[propZip]; + return new Promise((resolve, reject) => { + zip.stream(entry, (err, stm) => { + if (err) { + reject(err); + } else { + resolve(stm); + } + }); + }); + } + + async entryData(entry) { + const stm = await this.stream(entry); + return new Promise((resolve, reject) => { + const data = []; + stm.on('data', (chunk) => data.push(chunk)); + stm.on('end', () => { + resolve(Buffer.concat(data)); + }); + stm.on('error', (err) => { + stm.removeAllListeners('end'); + reject(err); + }); + }); + } + + async extract(entry, outPath) { + const zip = await this[propZip]; + return new Promise((resolve, reject) => { + zip.extract(entry, outPath, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); + } + + async close() { + const zip = await this[propZip]; + return new Promise((resolve, reject) => { + zip.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } }; -CentralDirectoryHeader.prototype.read = function(data) { - if (data.length != consts.ENDHDR || data.readUInt32LE(0) != consts.ENDSIG) - throw new Error('Invalid central directory'); - // number of entries on this volume - this.volumeEntries = data.readUInt16LE(consts.ENDSUB); - // total number of entries - this.totalEntries = data.readUInt16LE(consts.ENDTOT); - // central directory size in bytes - this.size = data.readUInt32LE(consts.ENDSIZ); - // offset of first CEN header - this.offset = data.readUInt32LE(consts.ENDOFF); - // zip file comment length - this.commentLength = data.readUInt16LE(consts.ENDCOM); -}; - -// endregion - -// region CentralDirectoryLoc64Header - -var CentralDirectoryLoc64Header = function() { -}; - -CentralDirectoryLoc64Header.prototype.read = function(data) { - if (data.length != consts.ENDL64HDR || data.readUInt32LE(0) != consts.ENDL64SIG) - throw new Error('Invalid zip64 central directory locator'); - // ZIP64 EOCD header offset - this.headerOffset = Util.readUInt64LE(data, consts.ENDSUB); -}; - -// endregion - -// region CentralDirectoryZip64Header - -var CentralDirectoryZip64Header = function() { -}; - -CentralDirectoryZip64Header.prototype.read = function(data) { - if (data.length != consts.END64HDR || data.readUInt32LE(0) != consts.END64SIG) - throw new Error('Invalid central directory'); - // number of entries on this volume - this.volumeEntries = Util.readUInt64LE(data, consts.END64SUB); - // total number of entries - this.totalEntries = Util.readUInt64LE(data, consts.END64TOT); - // central directory size in bytes - this.size = Util.readUInt64LE(data, consts.END64SIZ); - // offset of first CEN header - this.offset = Util.readUInt64LE(data, consts.END64OFF); -}; - -// endregion - -// region ZipEntry - -var ZipEntry = function() { -}; - -function toBits(dec, size) { - var b = (dec >>> 0).toString(2); - while (b.length < size) - b = '0' + b; - return b.split(''); +class CentralDirectoryHeader { + read(data) { + if (data.length !== consts.ENDHDR || data.readUInt32LE(0) !== consts.ENDSIG) { + throw new Error('Invalid central directory'); + } + // number of entries on this volume + this.volumeEntries = data.readUInt16LE(consts.ENDSUB); + // total number of entries + this.totalEntries = data.readUInt16LE(consts.ENDTOT); + // central directory size in bytes + this.size = data.readUInt32LE(consts.ENDSIZ); + // offset of first CEN header + this.offset = data.readUInt32LE(consts.ENDOFF); + // zip file comment length + this.commentLength = data.readUInt16LE(consts.ENDCOM); + } } -function parseZipTime(timebytes, datebytes) { - var timebits = toBits(timebytes, 16); - var datebits = toBits(datebytes, 16); - - var mt = { - h: parseInt(timebits.slice(0,5).join(''), 2), - m: parseInt(timebits.slice(5,11).join(''), 2), - s: parseInt(timebits.slice(11,16).join(''), 2) * 2, - Y: parseInt(datebits.slice(0,7).join(''), 2) + 1980, - M: parseInt(datebits.slice(7,11).join(''), 2), - D: parseInt(datebits.slice(11,16).join(''), 2), - }; - var dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0'; - return new Date(dt_str).getTime(); +class CentralDirectoryLoc64Header { + read(data) { + if (data.length !== consts.ENDL64HDR || data.readUInt32LE(0) !== consts.ENDL64SIG) { + throw new Error('Invalid zip64 central directory locator'); + } + // ZIP64 EOCD header offset + this.headerOffset = readUInt64LE(data, consts.ENDSUB); + } } -ZipEntry.prototype.readHeader = function(data, offset) { - // data should be 46 bytes and start with "PK 01 02" - if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) != consts.CENSIG) { - throw new Error('Invalid entry header'); - } - // version made by - this.verMade = data.readUInt16LE(offset + consts.CENVEM); - // version needed to extract - this.version = data.readUInt16LE(offset + consts.CENVER); - // encrypt, decrypt flags - this.flags = data.readUInt16LE(offset + consts.CENFLG); - // compression method - this.method = data.readUInt16LE(offset + consts.CENHOW); - // modification time (2 bytes time, 2 bytes date) - var timebytes = data.readUInt16LE(offset + consts.CENTIM); - var datebytes = data.readUInt16LE(offset + consts.CENTIM + 2); - this.time = parseZipTime(timebytes, datebytes); - - // uncompressed file crc-32 value - this.crc = data.readUInt32LE(offset + consts.CENCRC); - // compressed size - this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ); - // uncompressed size - this.size = data.readUInt32LE(offset + consts.CENLEN); - // filename length - this.fnameLen = data.readUInt16LE(offset + consts.CENNAM); - // extra field length - this.extraLen = data.readUInt16LE(offset + consts.CENEXT); - // file comment length - this.comLen = data.readUInt16LE(offset + consts.CENCOM); - // volume number start - this.diskStart = data.readUInt16LE(offset + consts.CENDSK); - // internal file attributes - this.inattr = data.readUInt16LE(offset + consts.CENATT); - // external file attributes - this.attr = data.readUInt32LE(offset + consts.CENATX); - // LOC header offset - this.offset = data.readUInt32LE(offset + consts.CENOFF); -}; - -ZipEntry.prototype.readDataHeader = function(data) { - // 30 bytes and should start with "PK\003\004" - if (data.readUInt32LE(0) != consts.LOCSIG) { - throw new Error('Invalid local header'); - } - // version needed to extract - this.version = data.readUInt16LE(consts.LOCVER); - // general purpose bit flag - this.flags = data.readUInt16LE(consts.LOCFLG); - // compression method - this.method = data.readUInt16LE(consts.LOCHOW); - // modification time (2 bytes time ; 2 bytes date) - var timebytes = data.readUInt16LE(consts.LOCTIM); - var datebytes = data.readUInt16LE(consts.LOCTIM + 2); - this.time = parseZipTime(timebytes, datebytes); - - // uncompressed file crc-32 value - this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc; - // compressed size - var compressedSize = data.readUInt32LE(consts.LOCSIZ); - if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) { - this.compressedSize = compressedSize; - } - // uncompressed size - var size = data.readUInt32LE(consts.LOCLEN); - if (size && size !== consts.EF_ZIP64_OR_32) { - this.size = size; - } - // filename length - this.fnameLen = data.readUInt16LE(consts.LOCNAM); - // extra field length - this.extraLen = data.readUInt16LE(consts.LOCEXT); -}; - -ZipEntry.prototype.read = function(data, offset) { - this.nameRaw = Buffer.from(data.slice(offset, offset += this.fnameLen)); - this.name = this.nameRaw.toString(); - var lastChar = data[offset - 1]; - this.isDirectory = (lastChar == 47) || (lastChar == 92); - - if (this.extraLen) { - this.readExtra(data, offset); - offset += this.extraLen; - } - this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null; -}; - -ZipEntry.prototype.validateName = function() { - if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) { - throw new Error('Malicious entry: ' + this.name); - } -}; - -ZipEntry.prototype.readExtra = function(data, offset) { - var signature, size, maxPos = offset + this.extraLen; - while (offset < maxPos) { - signature = data.readUInt16LE(offset); - offset += 2; - size = data.readUInt16LE(offset); - offset += 2; - if (consts.ID_ZIP64 === signature) { - this.parseZip64Extra(data, offset, size); +class CentralDirectoryZip64Header { + read(data) { + if (data.length !== consts.END64HDR || data.readUInt32LE(0) !== consts.END64SIG) { + throw new Error('Invalid central directory'); } - offset += size; + // number of entries on this volume + this.volumeEntries = readUInt64LE(data, consts.END64SUB); + // total number of entries + this.totalEntries = readUInt64LE(data, consts.END64TOT); + // central directory size in bytes + this.size = readUInt64LE(data, consts.END64SIZ); + // offset of first CEN header + this.offset = readUInt64LE(data, consts.END64OFF); } -}; +} -ZipEntry.prototype.parseZip64Extra = function(data, offset, length) { - if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) { - this.size = Util.readUInt64LE(data, offset); - offset += 8; length -= 8; - } - if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) { - this.compressedSize = Util.readUInt64LE(data, offset); - offset += 8; length -= 8; - } - if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) { - this.offset = Util.readUInt64LE(data, offset); - offset += 8; length -= 8; - } - if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) { - this.diskStart = data.readUInt32LE(offset); - // offset += 4; length -= 4; - } -}; - -Object.defineProperty(ZipEntry.prototype, 'encrypted', { - get: function() { return (this.flags & consts.FLG_ENTRY_ENC) == consts.FLG_ENTRY_ENC; } -}); - -Object.defineProperty(ZipEntry.prototype, 'isFile', { - get: function() { return !this.isDirectory; } -}); - -// endregion - -// region FsRead - -var FsRead = function(fd, buffer, offset, length, position, callback) { - this.fd = fd; - this.buffer = buffer; - this.offset = offset; - this.length = length; - this.position = position; - this.callback = callback; - this.bytesRead = 0; - this.waiting = false; -}; - -FsRead.prototype.read = function(sync) { - if (StreamZip.debug) { - console.log('read', this.position, this.bytesRead, this.length, this.offset); - } - this.waiting = true; - var err; - if (sync) { - try { - var bytesRead = fs.readSync(this.fd, this.buffer, this.offset + this.bytesRead, - this.length - this.bytesRead, this.position + this.bytesRead); - } catch (e) { - err = e; +class ZipEntry { + readHeader(data, offset) { + // data should be 46 bytes and start with "PK 01 02" + if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) !== consts.CENSIG) { + throw new Error('Invalid entry header'); } - this.readCallback(sync, err, err ? bytesRead : null); - } else { - fs.read(this.fd, this.buffer, this.offset + this.bytesRead, - this.length - this.bytesRead, this.position + this.bytesRead, - this.readCallback.bind(this, sync)); - } -}; + // version made by + this.verMade = data.readUInt16LE(offset + consts.CENVEM); + // version needed to extract + this.version = data.readUInt16LE(offset + consts.CENVER); + // encrypt, decrypt flags + this.flags = data.readUInt16LE(offset + consts.CENFLG); + // compression method + this.method = data.readUInt16LE(offset + consts.CENHOW); + // modification time (2 bytes time, 2 bytes date) + const timebytes = data.readUInt16LE(offset + consts.CENTIM); + const datebytes = data.readUInt16LE(offset + consts.CENTIM + 2); + this.time = parseZipTime(timebytes, datebytes); -FsRead.prototype.readCallback = function(sync, err, bytesRead) { - if (typeof bytesRead === 'number') - this.bytesRead += bytesRead; - if (err || !bytesRead || this.bytesRead === this.length) { + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(offset + consts.CENCRC); + // compressed size + this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ); + // uncompressed size + this.size = data.readUInt32LE(offset + consts.CENLEN); + // filename length + this.fnameLen = data.readUInt16LE(offset + consts.CENNAM); + // extra field length + this.extraLen = data.readUInt16LE(offset + consts.CENEXT); + // file comment length + this.comLen = data.readUInt16LE(offset + consts.CENCOM); + // volume number start + this.diskStart = data.readUInt16LE(offset + consts.CENDSK); + // internal file attributes + this.inattr = data.readUInt16LE(offset + consts.CENATT); + // external file attributes + this.attr = data.readUInt32LE(offset + consts.CENATX); + // LOC header offset + this.offset = data.readUInt32LE(offset + consts.CENOFF); + } + + readDataHeader(data) { + // 30 bytes and should start with "PK\003\004" + if (data.readUInt32LE(0) !== consts.LOCSIG) { + throw new Error('Invalid local header'); + } + // version needed to extract + this.version = data.readUInt16LE(consts.LOCVER); + // general purpose bit flag + this.flags = data.readUInt16LE(consts.LOCFLG); + // compression method + this.method = data.readUInt16LE(consts.LOCHOW); + // modification time (2 bytes time ; 2 bytes date) + const timebytes = data.readUInt16LE(consts.LOCTIM); + const datebytes = data.readUInt16LE(consts.LOCTIM + 2); + this.time = parseZipTime(timebytes, datebytes); + + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc; + // compressed size + const compressedSize = data.readUInt32LE(consts.LOCSIZ); + if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) { + this.compressedSize = compressedSize; + } + // uncompressed size + const size = data.readUInt32LE(consts.LOCLEN); + if (size && size !== consts.EF_ZIP64_OR_32) { + this.size = size; + } + // filename length + this.fnameLen = data.readUInt16LE(consts.LOCNAM); + // extra field length + this.extraLen = data.readUInt16LE(consts.LOCEXT); + } + + read(data, offset, textDecoder) { + const nameData = data.slice(offset, (offset += this.fnameLen)); + this.name = textDecoder + ? textDecoder.decode(new Uint8Array(nameData)) + : nameData.toString('utf8'); + const lastChar = data[offset - 1]; + this.isDirectory = lastChar === 47 || lastChar === 92; + + if (this.extraLen) { + this.readExtra(data, offset); + offset += this.extraLen; + } + this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null; + } + + validateName() { + if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) { + throw new Error('Malicious entry: ' + this.name); + } + } + + readExtra(data, offset) { + let signature, size; + const maxPos = offset + this.extraLen; + while (offset < maxPos) { + signature = data.readUInt16LE(offset); + offset += 2; + size = data.readUInt16LE(offset); + offset += 2; + if (consts.ID_ZIP64 === signature) { + this.parseZip64Extra(data, offset, size); + } + offset += size; + } + } + + parseZip64Extra(data, offset, length) { + if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) { + this.size = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) { + this.compressedSize = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) { + this.offset = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) { + this.diskStart = data.readUInt32LE(offset); + // offset += 4; length -= 4; + } + } + + get encrypted() { + return (this.flags & consts.FLG_ENTRY_ENC) === consts.FLG_ENTRY_ENC; + } + + get isFile() { + return !this.isDirectory; + } +} + +class FsRead { + constructor(fd, buffer, offset, length, position, callback) { + this.fd = fd; + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.position = position; + this.callback = callback; + this.bytesRead = 0; this.waiting = false; - return this.callback(err, this.bytesRead); - } else { - this.read(sync); } -}; -// endregion + read(sync) { + StreamZip.debugLog('read', this.position, this.bytesRead, this.length, this.offset); + this.waiting = true; + let err; + if (sync) { + let bytesRead = 0; + try { + bytesRead = fs.readSync( + this.fd, + this.buffer, + this.offset + this.bytesRead, + this.length - this.bytesRead, + this.position + this.bytesRead + ); + } catch (e) { + err = e; + } + this.readCallback(sync, err, err ? bytesRead : null); + } else { + fs.read( + this.fd, + this.buffer, + this.offset + this.bytesRead, + this.length - this.bytesRead, + this.position + this.bytesRead, + this.readCallback.bind(this, sync) + ); + } + } -// region FileWindowBuffer + readCallback(sync, err, bytesRead) { + if (typeof bytesRead === 'number') { + this.bytesRead += bytesRead; + } + if (err || !bytesRead || this.bytesRead === this.length) { + this.waiting = false; + return this.callback(err, this.bytesRead); + } else { + this.read(sync); + } + } +} -var FileWindowBuffer = function(fd) { - this.position = 0; - this.buffer = Buffer.alloc(0); +class FileWindowBuffer { + constructor(fd) { + this.position = 0; + this.buffer = Buffer.alloc(0); + this.fd = fd; + this.fsOp = null; + } - var fsOp = null; - - this.checkOp = function() { - if (fsOp && fsOp.waiting) + checkOp() { + if (this.fsOp && this.fsOp.waiting) { throw new Error('Operation in progress'); - }; + } + } - this.read = function(pos, length, callback) { + read(pos, length, callback) { this.checkOp(); - if (this.buffer.length < length) + if (this.buffer.length < length) { this.buffer = Buffer.alloc(length); + } this.position = pos; - fsOp = new FsRead(fd, this.buffer, 0, length, this.position, callback).read(); - }; + this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read(); + } - this.expandLeft = function(length, callback) { + expandLeft(length, callback) { this.checkOp(); this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]); this.position -= length; - if (this.position < 0) + if (this.position < 0) { this.position = 0; - fsOp = new FsRead(fd, this.buffer, 0, length, this.position, callback).read(); - }; + } + this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read(); + } - this.expandRight = function(length, callback) { + expandRight(length, callback) { this.checkOp(); - var offset = this.buffer.length; + const offset = this.buffer.length; this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]); - fsOp = new FsRead(fd, this.buffer, offset, length, this.position + offset, callback).read(); - }; + this.fsOp = new FsRead( + this.fd, + this.buffer, + offset, + length, + this.position + offset, + callback + ).read(); + } - this.moveRight = function(length, callback, shift) { + moveRight(length, callback, shift) { this.checkOp(); if (shift) { this.buffer.copy(this.buffer, 0, shift); @@ -919,137 +1055,156 @@ var FileWindowBuffer = function(fd) { shift = 0; } this.position += shift; - fsOp = new FsRead(fd, this.buffer, this.buffer.length - shift, shift, this.position + this.buffer.length - shift, callback).read(); - }; -}; - -// endregion - -// region EntryDataReaderStream - -var EntryDataReaderStream = function(fd, offset, length) { - stream.Readable.prototype.constructor.call(this); - this.fd = fd; - this.offset = offset; - this.length = length; - this.pos = 0; - this.readCallback = this.readCallback.bind(this); -}; - -util.inherits(EntryDataReaderStream, stream.Readable); - -EntryDataReaderStream.prototype._read = function(n) { - var buffer = Buffer.alloc(Math.min(n, this.length - this.pos)); - if (buffer.length) { - fs.read(this.fd, buffer, 0, buffer.length, this.offset + this.pos, this.readCallback); - } else { - this.push(null); + this.fsOp = new FsRead( + this.fd, + this.buffer, + this.buffer.length - shift, + shift, + this.position + this.buffer.length - shift, + callback + ).read(); } -}; +} -EntryDataReaderStream.prototype.readCallback = function(err, bytesRead, buffer) { - this.pos += bytesRead; - if (err) { - this.emit('error', err); - this.push(null); - } else if (!bytesRead) { - this.push(null); - } else { - if (bytesRead !== buffer.length) - buffer = buffer.slice(0, bytesRead); - this.push(buffer); +class EntryDataReaderStream extends stream.Readable { + constructor(fd, offset, length) { + super(); + this.fd = fd; + this.offset = offset; + this.length = length; + this.pos = 0; + this.readCallback = this.readCallback.bind(this); } -}; -// endregion - -// region EntryVerifyStream - -var EntryVerifyStream = function(baseStm, crc, size) { - stream.Transform.prototype.constructor.call(this); - this.verify = new CrcVerify(crc, size); - var that = this; - baseStm.on('error', function(e) { - that.emit('error', e); - }); -}; - -util.inherits(EntryVerifyStream, stream.Transform); - -EntryVerifyStream.prototype._transform = function(data, encoding, callback) { - var err; - try { - this.verify.data(data); - } catch (e) { - err = e; - } - callback(err, data); -}; - -// endregion - -// region CrcVerify - -var CrcVerify = function(crc, size) { - this.crc = crc; - this.size = size; - this.state = { - crc: ~0, - size: 0 - }; -}; - -CrcVerify.prototype.data = function(data) { - var crcTable = CrcVerify.getCrcTable(); - var crc = this.state.crc, off = 0, len = data.length; - while (--len >= 0) - crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8); - this.state.crc = crc; - this.state.size += data.length; - if (this.state.size >= this.size) { - var buf = Buffer.alloc(4); - buf.writeInt32LE(~this.state.crc & 0xffffffff, 0); - crc = buf.readUInt32LE(0); - if (crc !== this.crc) - throw new Error('Invalid CRC'); - if (this.state.size !== this.size) - throw new Error('Invalid size'); - } -}; - -CrcVerify.getCrcTable = function() { - var crcTable = CrcVerify.crcTable; - if (!crcTable) { - CrcVerify.crcTable = crcTable = []; - var b = Buffer.alloc(4); - for (var n = 0; n < 256; n++) { - var c = n; - for (var k = 8; --k >= 0; ) - if ((c & 1) != 0) { c = 0xedb88320 ^ (c >>> 1); } else { c = c >>> 1; } - if (c < 0) { - b.writeInt32LE(c, 0); - c = b.readUInt32LE(0); - } - crcTable[n] = c; + _read(n) { + const buffer = Buffer.alloc(Math.min(n, this.length - this.pos)); + if (buffer.length) { + fs.read(this.fd, buffer, 0, buffer.length, this.offset + this.pos, this.readCallback); + } else { + this.push(null); } } - return crcTable; -}; -// endregion - -// region Util - -var Util = { - readUInt64LE: function(buffer, offset) { - return (buffer.readUInt32LE(offset + 4) * 0x0000000100000000) + buffer.readUInt32LE(offset); + readCallback(err, bytesRead, buffer) { + this.pos += bytesRead; + if (err) { + this.emit('error', err); + this.push(null); + } else if (!bytesRead) { + this.push(null); + } else { + if (bytesRead !== buffer.length) { + buffer = buffer.slice(0, bytesRead); + } + this.push(buffer); + } } -}; +} -// endregion +class EntryVerifyStream extends stream.Transform { + constructor(baseStm, crc, size) { + super(); + this.verify = new CrcVerify(crc, size); + baseStm.on('error', (e) => { + this.emit('error', e); + }); + } -// region exports + _transform(data, encoding, callback) { + let err; + try { + this.verify.data(data); + } catch (e) { + err = e; + } + callback(err, data); + } +} + +class CrcVerify { + constructor(crc, size) { + this.crc = crc; + this.size = size; + this.state = { + crc: ~0, + size: 0, + }; + } + + data(data) { + const crcTable = CrcVerify.getCrcTable(); + let crc = this.state.crc; + let off = 0; + let len = data.length; + while (--len >= 0) { + crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8); + } + this.state.crc = crc; + this.state.size += data.length; + if (this.state.size >= this.size) { + const buf = Buffer.alloc(4); + buf.writeInt32LE(~this.state.crc & 0xffffffff, 0); + crc = buf.readUInt32LE(0); + if (crc !== this.crc) { + throw new Error('Invalid CRC'); + } + if (this.state.size !== this.size) { + throw new Error('Invalid size'); + } + } + } + + static getCrcTable() { + let crcTable = CrcVerify.crcTable; + if (!crcTable) { + CrcVerify.crcTable = crcTable = []; + const b = Buffer.alloc(4); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 8; --k >= 0; ) { + if ((c & 1) !== 0) { + c = 0xedb88320 ^ (c >>> 1); + } else { + c = c >>> 1; + } + } + if (c < 0) { + b.writeInt32LE(c, 0); + c = b.readUInt32LE(0); + } + crcTable[n] = c; + } + } + return crcTable; + } +} + +function parseZipTime(timebytes, datebytes) { + const timebits = toBits(timebytes, 16); + const datebits = toBits(datebytes, 16); + + const mt = { + h: parseInt(timebits.slice(0, 5).join(''), 2), + m: parseInt(timebits.slice(5, 11).join(''), 2), + s: parseInt(timebits.slice(11, 16).join(''), 2) * 2, + Y: parseInt(datebits.slice(0, 7).join(''), 2) + 1980, + M: parseInt(datebits.slice(7, 11).join(''), 2), + D: parseInt(datebits.slice(11, 16).join(''), 2), + }; + const dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0'; + return new Date(dt_str).getTime(); +} + +function toBits(dec, size) { + let b = (dec >>> 0).toString(2); + while (b.length < size) { + b = '0' + b; + } + return b.split(''); +} + +function readUInt64LE(buffer, offset) { + return buffer.readUInt32LE(offset + 4) * 0x0000000100000000 + buffer.readUInt32LE(offset); +} module.exports = StreamZip; - -// endregion diff --git a/server/core/Zip/node_stream_zip_changed.js b/server/core/Zip/node_stream_zip_changed.js new file mode 100644 index 00000000..ab6c4c13 --- /dev/null +++ b/server/core/Zip/node_stream_zip_changed.js @@ -0,0 +1,1211 @@ +/** + * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE + * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE + */ + +let fs = require('fs'); +const util = require('util'); +const path = require('path'); +const events = require('events'); +const zlib = require('zlib'); +const stream = require('stream'); + +const consts = { + /* The local file header */ + LOCHDR: 30, // LOC header size + LOCSIG: 0x04034b50, // "PK\003\004" + LOCVER: 4, // version needed to extract + LOCFLG: 6, // general purpose bit flag + LOCHOW: 8, // compression method + LOCTIM: 10, // modification time (2 bytes time, 2 bytes date) + LOCCRC: 14, // uncompressed file crc-32 value + LOCSIZ: 18, // compressed size + LOCLEN: 22, // uncompressed size + LOCNAM: 26, // filename length + LOCEXT: 28, // extra field length + + /* The Data descriptor */ + EXTSIG: 0x08074b50, // "PK\007\008" + EXTHDR: 16, // EXT header size + EXTCRC: 4, // uncompressed file crc-32 value + EXTSIZ: 8, // compressed size + EXTLEN: 12, // uncompressed size + + /* The central directory file header */ + CENHDR: 46, // CEN header size + CENSIG: 0x02014b50, // "PK\001\002" + CENVEM: 4, // version made by + CENVER: 6, // version needed to extract + CENFLG: 8, // encrypt, decrypt flags + CENHOW: 10, // compression method + CENTIM: 12, // modification time (2 bytes time, 2 bytes date) + CENCRC: 16, // uncompressed file crc-32 value + CENSIZ: 20, // compressed size + CENLEN: 24, // uncompressed size + CENNAM: 28, // filename length + CENEXT: 30, // extra field length + CENCOM: 32, // file comment length + CENDSK: 34, // volume number start + CENATT: 36, // internal file attributes + CENATX: 38, // external file attributes (host system dependent) + CENOFF: 42, // LOC header offset + + /* The entries in the end of central directory */ + ENDHDR: 22, // END header size + ENDSIG: 0x06054b50, // "PK\005\006" + ENDSIGFIRST: 0x50, + ENDSUB: 8, // number of entries on this disk + ENDTOT: 10, // total number of entries + ENDSIZ: 12, // central directory size in bytes + ENDOFF: 16, // offset of first CEN header + ENDCOM: 20, // zip file comment length + MAXFILECOMMENT: 0xffff, + + /* The entries in the end of ZIP64 central directory locator */ + ENDL64HDR: 20, // ZIP64 end of central directory locator header size + ENDL64SIG: 0x07064b50, // ZIP64 end of central directory locator signature + ENDL64SIGFIRST: 0x50, + ENDL64OFS: 8, // ZIP64 end of central directory offset + + /* The entries in the end of ZIP64 central directory */ + END64HDR: 56, // ZIP64 end of central directory header size + END64SIG: 0x06064b50, // ZIP64 end of central directory signature + END64SIGFIRST: 0x50, + END64SUB: 24, // number of entries on this disk + END64TOT: 32, // total number of entries + END64SIZ: 40, + END64OFF: 48, + + /* Compression methods */ + STORED: 0, // no compression + SHRUNK: 1, // shrunk + REDUCED1: 2, // reduced with compression factor 1 + REDUCED2: 3, // reduced with compression factor 2 + REDUCED3: 4, // reduced with compression factor 3 + REDUCED4: 5, // reduced with compression factor 4 + IMPLODED: 6, // imploded + // 7 reserved + DEFLATED: 8, // deflated + ENHANCED_DEFLATED: 9, // deflate64 + PKWARE: 10, // PKWare DCL imploded + // 11 reserved + BZIP2: 12, // compressed using BZIP2 + // 13 reserved + LZMA: 14, // LZMA + // 15-17 reserved + IBM_TERSE: 18, // compressed using IBM TERSE + IBM_LZ77: 19, //IBM LZ77 z + + /* General purpose bit flag */ + FLG_ENC: 0, // encrypted file + FLG_COMP1: 1, // compression option + FLG_COMP2: 2, // compression option + FLG_DESC: 4, // data descriptor + FLG_ENH: 8, // enhanced deflation + FLG_STR: 16, // strong encryption + FLG_LNG: 1024, // language encoding + FLG_MSK: 4096, // mask header values + FLG_ENTRY_ENC: 1, + + /* 4.5 Extensible data fields */ + EF_ID: 0, + EF_SIZE: 2, + + /* Header IDs */ + ID_ZIP64: 0x0001, + ID_AVINFO: 0x0007, + ID_PFS: 0x0008, + ID_OS2: 0x0009, + ID_NTFS: 0x000a, + ID_OPENVMS: 0x000c, + ID_UNIX: 0x000d, + ID_FORK: 0x000e, + ID_PATCH: 0x000f, + ID_X509_PKCS7: 0x0014, + ID_X509_CERTID_F: 0x0015, + ID_X509_CERTID_C: 0x0016, + ID_STRONGENC: 0x0017, + ID_RECORD_MGT: 0x0018, + ID_X509_PKCS7_RL: 0x0019, + ID_IBM1: 0x0065, + ID_IBM2: 0x0066, + ID_POSZIP: 0x4690, + + EF_ZIP64_OR_32: 0xffffffff, + EF_ZIP64_OR_16: 0xffff, +}; + +const StreamZip = function (config) { + let fd, fileSize, chunkSize, op, centralDirectory, closed; + const ready = false, + that = this, + entries = config.storeEntries !== false ? {} : null, + fileName = config.file, + textDecoder = config.nameEncoding ? new TextDecoder(config.nameEncoding) : null; + + open(); + + function open() { + if (config.fd) { + fd = config.fd; + readFile(); + } else { + fs.open(fileName, 'r', (err, f) => { + if (err) { + return that.emit('error', err); + } + fd = f; + readFile(); + }); + } + } + + function readFile() { + fs.fstat(fd, (err, stat) => { + if (err) { + return that.emit('error', err); + } + fileSize = stat.size; + chunkSize = config.chunkSize || Math.round(fileSize / 1000); + chunkSize = Math.max( + Math.min(chunkSize, Math.min(128 * 1024, fileSize)), + Math.min(1024, fileSize) + ); + readCentralDirectory(); + }); + } + + function readUntilFoundCallback(err, bytesRead) { + if (err || !bytesRead) { + return that.emit('error', err || new Error('Archive read error')); + } + let pos = op.lastPos; + let bufferPosition = pos - op.win.position; + const buffer = op.win.buffer; + const minPos = op.minPos; + while (--pos >= minPos && --bufferPosition >= 0) { + if (buffer.length - bufferPosition >= 4 && buffer[bufferPosition] === op.firstByte) { + // quick check first signature byte + if (buffer.readUInt32LE(bufferPosition) === op.sig) { + op.lastBufferPosition = bufferPosition; + op.lastBytesRead = bytesRead; + op.complete(); + return; + } + } + } + if (pos === minPos) { + return that.emit('error', new Error('Bad archive')); + } + op.lastPos = pos + 1; + op.chunkSize *= 2; + if (pos <= minPos) { + return that.emit('error', new Error('Bad archive')); + } + const expandLength = Math.min(op.chunkSize, pos - minPos); + op.win.expandLeft(expandLength, readUntilFoundCallback); + } + + function readCentralDirectory() { + const totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, fileSize); + op = { + win: new FileWindowBuffer(fd), + totalReadLength, + minPos: fileSize - totalReadLength, + lastPos: fileSize, + chunkSize: Math.min(1024, chunkSize), + firstByte: consts.ENDSIGFIRST, + sig: consts.ENDSIG, + complete: readCentralDirectoryComplete, + }; + op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback); + } + + function readCentralDirectoryComplete() { + const buffer = op.win.buffer; + const pos = op.lastBufferPosition; + try { + centralDirectory = new CentralDirectoryHeader(); + centralDirectory.read(buffer.slice(pos, pos + consts.ENDHDR)); + centralDirectory.headerOffset = op.win.position + pos; + if (centralDirectory.commentLength) { + that.comment = buffer + .slice( + pos + consts.ENDHDR, + pos + consts.ENDHDR + centralDirectory.commentLength + ) + .toString(); + } else { + that.comment = null; + } + that.entriesCount = centralDirectory.volumeEntries; + that.centralDirectory = centralDirectory; + if ( + (centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 && + centralDirectory.totalEntries === consts.EF_ZIP64_OR_16) || + centralDirectory.size === consts.EF_ZIP64_OR_32 || + centralDirectory.offset === consts.EF_ZIP64_OR_32 + ) { + readZip64CentralDirectoryLocator(); + } else { + op = {}; + readEntries(); + } + } catch (err) { + that.emit('error', err); + } + } + + function readZip64CentralDirectoryLocator() { + const length = consts.ENDL64HDR; + if (op.lastBufferPosition > length) { + op.lastBufferPosition -= length; + readZip64CentralDirectoryLocatorComplete(); + } else { + op = { + win: op.win, + totalReadLength: length, + minPos: op.win.position - length, + lastPos: op.win.position, + chunkSize: op.chunkSize, + firstByte: consts.ENDL64SIGFIRST, + sig: consts.ENDL64SIG, + complete: readZip64CentralDirectoryLocatorComplete, + }; + op.win.read(op.lastPos - op.chunkSize, op.chunkSize, readUntilFoundCallback); + } + } + + function readZip64CentralDirectoryLocatorComplete() { + const buffer = op.win.buffer; + const locHeader = new CentralDirectoryLoc64Header(); + locHeader.read( + buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.ENDL64HDR) + ); + const readLength = fileSize - locHeader.headerOffset; + op = { + win: op.win, + totalReadLength: readLength, + minPos: locHeader.headerOffset, + lastPos: op.lastPos, + chunkSize: op.chunkSize, + firstByte: consts.END64SIGFIRST, + sig: consts.END64SIG, + complete: readZip64CentralDirectoryComplete, + }; + op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback); + } + + function readZip64CentralDirectoryComplete() { + const buffer = op.win.buffer; + const zip64cd = new CentralDirectoryZip64Header(); + zip64cd.read(buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.END64HDR)); + that.centralDirectory.volumeEntries = zip64cd.volumeEntries; + that.centralDirectory.totalEntries = zip64cd.totalEntries; + that.centralDirectory.size = zip64cd.size; + that.centralDirectory.offset = zip64cd.offset; + that.entriesCount = zip64cd.volumeEntries; + op = {}; + readEntries(); + } + + function readEntries() { + op = { + win: new FileWindowBuffer(fd), + pos: centralDirectory.offset, + chunkSize, + entriesLeft: centralDirectory.volumeEntries, + }; + op.win.read(op.pos, Math.min(chunkSize, fileSize - op.pos), readEntriesCallback); + } + + function readEntriesCallback(err, bytesRead) { + if (err || !bytesRead) { + return that.emit('error', err || new Error('Entries read error')); + } + let bufferPos = op.pos - op.win.position; + let entry = op.entry; + const buffer = op.win.buffer; + const bufferLength = buffer.length; + try { + while (op.entriesLeft > 0) { + if (!entry) { + entry = new ZipEntry(); + entry.readHeader(buffer, bufferPos); + entry.headerOffset = op.win.position + bufferPos; + op.entry = entry; + op.pos += consts.CENHDR; + bufferPos += consts.CENHDR; + } + const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen; + const advanceBytes = entryHeaderSize + (op.entriesLeft > 1 ? consts.CENHDR : 0); + if (bufferLength - bufferPos < advanceBytes) { + op.win.moveRight(chunkSize, readEntriesCallback, bufferPos); + op.move = true; + return; + } + entry.read(buffer, bufferPos, textDecoder); + if (!config.skipEntryNameValidation) { + entry.validateName(); + } + if (entries) { + entries[entry.name] = entry; + } + that.emit('entry', entry); + op.entry = entry = null; + op.entriesLeft--; + op.pos += entryHeaderSize; + bufferPos += entryHeaderSize; + } + that.emit('ready'); + } catch (err) { + that.emit('error', err); + } + } + + function checkEntriesExist() { + if (!entries) { + throw new Error('storeEntries disabled'); + } + } + + Object.defineProperty(this, 'ready', { + get() { + return ready; + }, + }); + + this.entry = function (name) { + checkEntriesExist(); + return entries[name]; + }; + + this.entries = function () { + checkEntriesExist(); + return entries; + }; + + this.stream = function (entry, callback) { + return this.openEntry( + entry, + (err, entry) => { + if (err) { + return callback(err); + } + const offset = dataOffset(entry); + let entryStream = new EntryDataReaderStream(fd, offset, entry.compressedSize); + if (entry.method === consts.STORED) { + // nothing to do + } else if (entry.method === consts.DEFLATED) { + entryStream = entryStream.pipe(zlib.createInflateRaw()); + } else { + return callback(new Error('Unknown compression method: ' + entry.method)); + } + if (canVerifyCrc(entry)) { + entryStream = entryStream.pipe( + new EntryVerifyStream(entryStream, entry.crc, entry.size) + ); + } + callback(null, entryStream); + }, + false + ); + }; + + this.entryDataSync = function (entry) { + let err = null; + this.openEntry( + entry, + (e, en) => { + err = e; + entry = en; + }, + true + ); + if (err) { + throw err; + } + let data = Buffer.alloc(entry.compressedSize); + new FsRead(fd, data, 0, entry.compressedSize, dataOffset(entry), (e) => { + err = e; + }).read(true); + if (err) { + throw err; + } + if (entry.method === consts.STORED) { + // nothing to do + } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) { + data = zlib.inflateRawSync(data); + } else { + throw new Error('Unknown compression method: ' + entry.method); + } + if (data.length !== entry.size) { + throw new Error('Invalid size'); + } + if (canVerifyCrc(entry)) { + const verify = new CrcVerify(entry.crc, entry.size); + verify.data(data); + } + return data; + }; + + this.openEntry = function (entry, callback, sync) { + if (typeof entry === 'string') { + checkEntriesExist(); + entry = entries[entry]; + if (!entry) { + return callback(new Error('Entry not found')); + } + } + if (!entry.isFile) { + return callback(new Error('Entry is not file')); + } + if (!fd) { + return callback(new Error('Archive closed')); + } + const buffer = Buffer.alloc(consts.LOCHDR); + new FsRead(fd, buffer, 0, buffer.length, entry.offset, (err) => { + if (err) { + return callback(err); + } + let readEx; + try { + entry.readDataHeader(buffer); + if (entry.encrypted) { + readEx = new Error('Entry encrypted'); + } + } catch (ex) { + readEx = ex; + } + callback(readEx, entry); + }).read(sync); + }; + + function dataOffset(entry) { + return entry.offset + consts.LOCHDR + entry.fnameLen + entry.extraLen; + } + + function canVerifyCrc(entry) { + // if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written + return (entry.flags & 0x8) !== 0x8; + } + + function extract(entry, outPath, callback) { + that.stream(entry, (err, stm) => { + if (err) { + callback(err); + } else { + let fsStm, errThrown; + stm.on('error', (err) => { + errThrown = err; + if (fsStm) { + stm.unpipe(fsStm); + fsStm.close(() => { + callback(err); + }); + } + }); + fs.open(outPath, 'w', (err, fdFile) => { + if (err) { + return callback(err); + } + if (errThrown) { + fs.close(fd, () => { + callback(errThrown); + }); + return; + } + fsStm = fs.createWriteStream(outPath, { fd: fdFile }); + fsStm.on('finish', () => { + that.emit('extract', entry, outPath); + if (!errThrown) { + callback(); + } + }); + stm.pipe(fsStm); + }); + } + }); + } + + function createDirectories(baseDir, dirs, callback) { + if (!dirs.length) { + return callback(); + } + let dir = dirs.shift(); + dir = path.join(baseDir, path.join(...dir)); + fs.mkdir(dir, { recursive: true }, (err) => { + if (err && err.code !== 'EEXIST') { + return callback(err); + } + createDirectories(baseDir, dirs, callback); + }); + } + + function extractFiles(baseDir, baseRelPath, files, callback, extractedCount) { + if (!files.length) { + return callback(null, extractedCount); + } + const file = files.shift(); + const targetPath = path.join(baseDir, file.name.replace(baseRelPath, '')); + extract(file, targetPath, (err) => { + if (err) { + return callback(err, extractedCount); + } + extractFiles(baseDir, baseRelPath, files, callback, extractedCount + 1); + }); + } + + this.extract = function (entry, outPath, callback) { + let entryName = entry || ''; + if (typeof entry === 'string') { + entry = this.entry(entry); + if (entry) { + entryName = entry.name; + } else { + if (entryName.length && entryName[entryName.length - 1] !== '/') { + entryName += '/'; + } + } + } + if (!entry || entry.isDirectory) { + const files = [], + dirs = [], + allDirs = {}; + for (const e in entries) { + if ( + Object.prototype.hasOwnProperty.call(entries, e) && + e.lastIndexOf(entryName, 0) === 0 + ) { + let relPath = e.replace(entryName, ''); + const childEntry = entries[e]; + if (childEntry.isFile) { + files.push(childEntry); + relPath = path.dirname(relPath); + } + if (relPath && !allDirs[relPath] && relPath !== '.') { + allDirs[relPath] = true; + let parts = relPath.split('/').filter((f) => { + return f; + }); + if (parts.length) { + dirs.push(parts); + } + while (parts.length > 1) { + parts = parts.slice(0, parts.length - 1); + const partsPath = parts.join('/'); + if (allDirs[partsPath] || partsPath === '.') { + break; + } + allDirs[partsPath] = true; + dirs.push(parts); + } + } + } + } + dirs.sort((x, y) => { + return x.length - y.length; + }); + if (dirs.length) { + createDirectories(outPath, dirs, (err) => { + if (err) { + callback(err); + } else { + extractFiles(outPath, entryName, files, callback, 0); + } + }); + } else { + extractFiles(outPath, entryName, files, callback, 0); + } + } else { + fs.stat(outPath, (err, stat) => { + if (stat && stat.isDirectory()) { + extract(entry, path.join(outPath, path.basename(entry.name)), callback); + } else { + extract(entry, outPath, callback); + } + }); + } + }; + + this.close = function (callback) { + if (closed || !fd) { + closed = true; + if (callback) { + callback(); + } + } else { + closed = true; + fs.close(fd, (err) => { + fd = null; + if (callback) { + callback(err); + } + }); + } + }; + + const originalEmit = events.EventEmitter.prototype.emit; + this.emit = function (...args) { + if (!closed) { + return originalEmit.call(this, ...args); + } + }; +}; + +StreamZip.setFs = function (customFs) { + fs = customFs; +}; + +StreamZip.debugLog = (...args) => { + if (StreamZip.debug) { + // eslint-disable-next-line no-console + console.log(...args); + } +}; + +util.inherits(StreamZip, events.EventEmitter); + +const propZip = Symbol('zip'); + +StreamZip.async = class StreamZipAsync extends events.EventEmitter { + constructor(config) { + super(); + + const zip = new StreamZip(config); + + zip.on('entry', (entry) => this.emit('entry', entry)); + zip.on('extract', (entry, outPath) => this.emit('extract', entry, outPath)); + + this[propZip] = new Promise((resolve, reject) => { + zip.on('ready', () => { + zip.removeListener('error', reject); + resolve(zip); + }); + zip.on('error', reject); + }); + } + + get entriesCount() { + return this[propZip].then((zip) => zip.entriesCount); + } + + get comment() { + return this[propZip].then((zip) => zip.comment); + } + + async entry(name) { + const zip = await this[propZip]; + return zip.entry(name); + } + + async entries() { + const zip = await this[propZip]; + return zip.entries(); + } + + async stream(entry) { + const zip = await this[propZip]; + return new Promise((resolve, reject) => { + zip.stream(entry, (err, stm) => { + if (err) { + reject(err); + } else { + resolve(stm); + } + }); + }); + } + + async entryData(entry) { + const stm = await this.stream(entry); + return new Promise((resolve, reject) => { + const data = []; + stm.on('data', (chunk) => data.push(chunk)); + stm.on('end', () => { + resolve(Buffer.concat(data)); + }); + stm.on('error', (err) => { + stm.removeAllListeners('end'); + reject(err); + }); + }); + } + + async extract(entry, outPath) { + const zip = await this[propZip]; + return new Promise((resolve, reject) => { + zip.extract(entry, outPath, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); + } + + async close() { + const zip = await this[propZip]; + return new Promise((resolve, reject) => { + zip.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +}; + +class CentralDirectoryHeader { + read(data) { + if (data.length !== consts.ENDHDR || data.readUInt32LE(0) !== consts.ENDSIG) { + throw new Error('Invalid central directory'); + } + // number of entries on this volume + this.volumeEntries = data.readUInt16LE(consts.ENDSUB); + // total number of entries + this.totalEntries = data.readUInt16LE(consts.ENDTOT); + // central directory size in bytes + this.size = data.readUInt32LE(consts.ENDSIZ); + // offset of first CEN header + this.offset = data.readUInt32LE(consts.ENDOFF); + // zip file comment length + this.commentLength = data.readUInt16LE(consts.ENDCOM); + } +} + +class CentralDirectoryLoc64Header { + read(data) { + if (data.length !== consts.ENDL64HDR || data.readUInt32LE(0) !== consts.ENDL64SIG) { + throw new Error('Invalid zip64 central directory locator'); + } + // ZIP64 EOCD header offset + this.headerOffset = readUInt64LE(data, consts.ENDSUB); + } +} + +class CentralDirectoryZip64Header { + read(data) { + if (data.length !== consts.END64HDR || data.readUInt32LE(0) !== consts.END64SIG) { + throw new Error('Invalid central directory'); + } + // number of entries on this volume + this.volumeEntries = readUInt64LE(data, consts.END64SUB); + // total number of entries + this.totalEntries = readUInt64LE(data, consts.END64TOT); + // central directory size in bytes + this.size = readUInt64LE(data, consts.END64SIZ); + // offset of first CEN header + this.offset = readUInt64LE(data, consts.END64OFF); + } +} + +class ZipEntry { + readHeader(data, offset) { + // data should be 46 bytes and start with "PK 01 02" + if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) !== consts.CENSIG) { + throw new Error('Invalid entry header'); + } + // version made by + this.verMade = data.readUInt16LE(offset + consts.CENVEM); + // version needed to extract + this.version = data.readUInt16LE(offset + consts.CENVER); + // encrypt, decrypt flags + this.flags = data.readUInt16LE(offset + consts.CENFLG); + // compression method + this.method = data.readUInt16LE(offset + consts.CENHOW); + // modification time (2 bytes time, 2 bytes date) + const timebytes = data.readUInt16LE(offset + consts.CENTIM); + const datebytes = data.readUInt16LE(offset + consts.CENTIM + 2); + this.time = parseZipTime(timebytes, datebytes); + + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(offset + consts.CENCRC); + // compressed size + this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ); + // uncompressed size + this.size = data.readUInt32LE(offset + consts.CENLEN); + // filename length + this.fnameLen = data.readUInt16LE(offset + consts.CENNAM); + // extra field length + this.extraLen = data.readUInt16LE(offset + consts.CENEXT); + // file comment length + this.comLen = data.readUInt16LE(offset + consts.CENCOM); + // volume number start + this.diskStart = data.readUInt16LE(offset + consts.CENDSK); + // internal file attributes + this.inattr = data.readUInt16LE(offset + consts.CENATT); + // external file attributes + this.attr = data.readUInt32LE(offset + consts.CENATX); + // LOC header offset + this.offset = data.readUInt32LE(offset + consts.CENOFF); + } + + readDataHeader(data) { + // 30 bytes and should start with "PK\003\004" + if (data.readUInt32LE(0) !== consts.LOCSIG) { + throw new Error('Invalid local header'); + } + // version needed to extract + this.version = data.readUInt16LE(consts.LOCVER); + // general purpose bit flag + this.flags = data.readUInt16LE(consts.LOCFLG); + // compression method + this.method = data.readUInt16LE(consts.LOCHOW); + // modification time (2 bytes time ; 2 bytes date) + const timebytes = data.readUInt16LE(consts.LOCTIM); + const datebytes = data.readUInt16LE(consts.LOCTIM + 2); + this.time = parseZipTime(timebytes, datebytes); + + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc; + // compressed size + const compressedSize = data.readUInt32LE(consts.LOCSIZ); + if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) { + this.compressedSize = compressedSize; + } + // uncompressed size + const size = data.readUInt32LE(consts.LOCLEN); + if (size && size !== consts.EF_ZIP64_OR_32) { + this.size = size; + } + // filename length + this.fnameLen = data.readUInt16LE(consts.LOCNAM); + // extra field length + this.extraLen = data.readUInt16LE(consts.LOCEXT); + } + + read(data, offset, textDecoder) { + const nameData = data.slice(offset, (offset += this.fnameLen)); + this.nameRaw = Buffer.from(nameData); + this.name = textDecoder + ? textDecoder.decode(new Uint8Array(nameData)) + : nameData.toString('utf8'); + const lastChar = data[offset - 1]; + this.isDirectory = lastChar === 47 || lastChar === 92; + + if (this.extraLen) { + this.readExtra(data, offset); + offset += this.extraLen; + } + this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null; + } + + validateName() { + if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) { + throw new Error('Malicious entry: ' + this.name); + } + } + + readExtra(data, offset) { + let signature, size; + const maxPos = offset + this.extraLen; + while (offset < maxPos) { + signature = data.readUInt16LE(offset); + offset += 2; + size = data.readUInt16LE(offset); + offset += 2; + if (consts.ID_ZIP64 === signature) { + this.parseZip64Extra(data, offset, size); + } + offset += size; + } + } + + parseZip64Extra(data, offset, length) { + if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) { + this.size = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) { + this.compressedSize = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) { + this.offset = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) { + this.diskStart = data.readUInt32LE(offset); + // offset += 4; length -= 4; + } + } + + get encrypted() { + return (this.flags & consts.FLG_ENTRY_ENC) === consts.FLG_ENTRY_ENC; + } + + get isFile() { + return !this.isDirectory; + } +} + +class FsRead { + constructor(fd, buffer, offset, length, position, callback) { + this.fd = fd; + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.position = position; + this.callback = callback; + this.bytesRead = 0; + this.waiting = false; + } + + read(sync) { + StreamZip.debugLog('read', this.position, this.bytesRead, this.length, this.offset); + this.waiting = true; + let err; + if (sync) { + let bytesRead = 0; + try { + bytesRead = fs.readSync( + this.fd, + this.buffer, + this.offset + this.bytesRead, + this.length - this.bytesRead, + this.position + this.bytesRead + ); + } catch (e) { + err = e; + } + this.readCallback(sync, err, err ? bytesRead : null); + } else { + fs.read( + this.fd, + this.buffer, + this.offset + this.bytesRead, + this.length - this.bytesRead, + this.position + this.bytesRead, + this.readCallback.bind(this, sync) + ); + } + } + + readCallback(sync, err, bytesRead) { + if (typeof bytesRead === 'number') { + this.bytesRead += bytesRead; + } + if (err || !bytesRead || this.bytesRead === this.length) { + this.waiting = false; + return this.callback(err, this.bytesRead); + } else { + this.read(sync); + } + } +} + +class FileWindowBuffer { + constructor(fd) { + this.position = 0; + this.buffer = Buffer.alloc(0); + this.fd = fd; + this.fsOp = null; + } + + checkOp() { + if (this.fsOp && this.fsOp.waiting) { + throw new Error('Operation in progress'); + } + } + + read(pos, length, callback) { + this.checkOp(); + if (this.buffer.length < length) { + this.buffer = Buffer.alloc(length); + } + this.position = pos; + this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read(); + } + + expandLeft(length, callback) { + this.checkOp(); + this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]); + this.position -= length; + if (this.position < 0) { + this.position = 0; + } + this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read(); + } + + expandRight(length, callback) { + this.checkOp(); + const offset = this.buffer.length; + this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]); + this.fsOp = new FsRead( + this.fd, + this.buffer, + offset, + length, + this.position + offset, + callback + ).read(); + } + + moveRight(length, callback, shift) { + this.checkOp(); + if (shift) { + this.buffer.copy(this.buffer, 0, shift); + } else { + shift = 0; + } + this.position += shift; + this.fsOp = new FsRead( + this.fd, + this.buffer, + this.buffer.length - shift, + shift, + this.position + this.buffer.length - shift, + callback + ).read(); + } +} + +class EntryDataReaderStream extends stream.Readable { + constructor(fd, offset, length) { + super(); + this.fd = fd; + this.offset = offset; + this.length = length; + this.pos = 0; + this.readCallback = this.readCallback.bind(this); + } + + _read(n) { + const buffer = Buffer.alloc(Math.min(n, this.length - this.pos)); + if (buffer.length) { + fs.read(this.fd, buffer, 0, buffer.length, this.offset + this.pos, this.readCallback); + } else { + this.push(null); + } + } + + readCallback(err, bytesRead, buffer) { + this.pos += bytesRead; + if (err) { + this.emit('error', err); + this.push(null); + } else if (!bytesRead) { + this.push(null); + } else { + if (bytesRead !== buffer.length) { + buffer = buffer.slice(0, bytesRead); + } + this.push(buffer); + } + } +} + +class EntryVerifyStream extends stream.Transform { + constructor(baseStm, crc, size) { + super(); + this.verify = new CrcVerify(crc, size); + baseStm.on('error', (e) => { + this.emit('error', e); + }); + } + + _transform(data, encoding, callback) { + let err; + try { + this.verify.data(data); + } catch (e) { + err = e; + } + callback(err, data); + } +} + +class CrcVerify { + constructor(crc, size) { + this.crc = crc; + this.size = size; + this.state = { + crc: ~0, + size: 0, + }; + } + + data(data) { + const crcTable = CrcVerify.getCrcTable(); + let crc = this.state.crc; + let off = 0; + let len = data.length; + while (--len >= 0) { + crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8); + } + this.state.crc = crc; + this.state.size += data.length; + if (this.state.size >= this.size) { + const buf = Buffer.alloc(4); + buf.writeInt32LE(~this.state.crc & 0xffffffff, 0); + crc = buf.readUInt32LE(0); + if (crc !== this.crc) { + throw new Error('Invalid CRC'); + } + if (this.state.size !== this.size) { + throw new Error('Invalid size'); + } + } + } + + static getCrcTable() { + let crcTable = CrcVerify.crcTable; + if (!crcTable) { + CrcVerify.crcTable = crcTable = []; + const b = Buffer.alloc(4); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 8; --k >= 0; ) { + if ((c & 1) !== 0) { + c = 0xedb88320 ^ (c >>> 1); + } else { + c = c >>> 1; + } + } + if (c < 0) { + b.writeInt32LE(c, 0); + c = b.readUInt32LE(0); + } + crcTable[n] = c; + } + } + return crcTable; + } +} + +function parseZipTime(timebytes, datebytes) { + const timebits = toBits(timebytes, 16); + const datebits = toBits(datebytes, 16); + + const mt = { + h: parseInt(timebits.slice(0, 5).join(''), 2), + m: parseInt(timebits.slice(5, 11).join(''), 2), + s: parseInt(timebits.slice(11, 16).join(''), 2) * 2, + Y: parseInt(datebits.slice(0, 7).join(''), 2) + 1980, + M: parseInt(datebits.slice(7, 11).join(''), 2), + D: parseInt(datebits.slice(11, 16).join(''), 2), + }; + const dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0'; + return new Date(dt_str).getTime(); +} + +function toBits(dec, size) { + let b = (dec >>> 0).toString(2); + while (b.length < size) { + b = '0' + b; + } + return b.split(''); +} + +function readUInt64LE(buffer, offset) { + return buffer.readUInt32LE(offset + 4) * 0x0000000100000000 + buffer.readUInt32LE(offset); +} + +module.exports = StreamZip;