@zklogic/unzipper 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,243 @@
1
+ const PullStream = require('../PullStream');
2
+ const unzip = require('./unzip');
3
+ const BufferStream = require('../BufferStream');
4
+ const parseExtraField = require('../parseExtraField');
5
+ const path = require('path');
6
+ const fs = require('fs-extra');
7
+ const parseDateTime = require('../parseDateTime');
8
+ const parseBuffer = require('../parseBuffer');
9
+ const Bluebird = require('bluebird');
10
+
11
+ const signature = Buffer.alloc(4);
12
+ signature.writeUInt32LE(0x06054b50, 0);
13
+
14
+ function getCrxHeader(source) {
15
+ const sourceStream = source.stream(0).pipe(PullStream());
16
+
17
+ return sourceStream.pull(4).then(function(data) {
18
+ const signature = data.readUInt32LE(0);
19
+ if (signature === 0x34327243) {
20
+ let crxHeader;
21
+ return sourceStream.pull(12).then(function(data) {
22
+ crxHeader = parseBuffer.parse(data, [
23
+ ['version', 4],
24
+ ['pubKeyLength', 4],
25
+ ['signatureLength', 4],
26
+ ]);
27
+ }).then(function() {
28
+ return sourceStream.pull(crxHeader.pubKeyLength +crxHeader.signatureLength);
29
+ }).then(function(data) {
30
+ crxHeader.publicKey = data.slice(0, crxHeader.pubKeyLength);
31
+ crxHeader.signature = data.slice(crxHeader.pubKeyLength);
32
+ crxHeader.size = 16 + crxHeader.pubKeyLength +crxHeader.signatureLength;
33
+ return crxHeader;
34
+ });
35
+ }
36
+ });
37
+ }
38
+
39
+ // Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
40
+ function getZip64CentralDirectory(source, zip64CDL) {
41
+ const d64loc = parseBuffer.parse(zip64CDL, [
42
+ ['signature', 4],
43
+ ['diskNumber', 4],
44
+ ['offsetToStartOfCentralDirectory', 8],
45
+ ['numberOfDisks', 4],
46
+ ]);
47
+
48
+ if (d64loc.signature != 0x07064b50) {
49
+ throw new Error('invalid zip64 end of central dir locator signature (0x07064b50): 0x' + d64loc.signature.toString(16));
50
+ }
51
+
52
+ const dir64 = PullStream();
53
+ source.stream(d64loc.offsetToStartOfCentralDirectory).pipe(dir64);
54
+
55
+ return dir64.pull(56);
56
+ }
57
+
58
+ // Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
59
+ function parseZip64DirRecord (dir64record) {
60
+ const vars = parseBuffer.parse(dir64record, [
61
+ ['signature', 4],
62
+ ['sizeOfCentralDirectory', 8],
63
+ ['version', 2],
64
+ ['versionsNeededToExtract', 2],
65
+ ['diskNumber', 4],
66
+ ['diskStart', 4],
67
+ ['numberOfRecordsOnDisk', 8],
68
+ ['numberOfRecords', 8],
69
+ ['sizeOfCentralDirectory', 8],
70
+ ['offsetToStartOfCentralDirectory', 8],
71
+ ]);
72
+
73
+ if (vars.signature != 0x06064b50) {
74
+ throw new Error('invalid zip64 end of central dir locator signature (0x06064b50): 0x0' + vars.signature.toString(16));
75
+ }
76
+
77
+ return vars;
78
+ }
79
+
80
+ module.exports = function centralDirectory(source, options) {
81
+ const endDir = PullStream();
82
+ const records = PullStream();
83
+ const tailSize = (options && options.tailSize) || 80;
84
+ let sourceSize,
85
+ crxHeader,
86
+ startOffset,
87
+ vars;
88
+
89
+ if (options && options.crx)
90
+ crxHeader = getCrxHeader(source);
91
+
92
+ return source.size()
93
+ .then(function(size) {
94
+ sourceSize = size;
95
+
96
+ source.stream(Math.max(0, size-tailSize))
97
+ .on('error', function (error) { endDir.emit('error', error); })
98
+ .pipe(endDir);
99
+
100
+ return endDir.pull(signature);
101
+ })
102
+ .then(function() {
103
+ return Bluebird.props({directory: endDir.pull(22), crxHeader: crxHeader});
104
+ })
105
+ .then(function(d) {
106
+ const data = d.directory;
107
+ startOffset = d.crxHeader && d.crxHeader.size || 0;
108
+
109
+ vars = parseBuffer.parse(data, [
110
+ ['signature', 4],
111
+ ['diskNumber', 2],
112
+ ['diskStart', 2],
113
+ ['numberOfRecordsOnDisk', 2],
114
+ ['numberOfRecords', 2],
115
+ ['sizeOfCentralDirectory', 4],
116
+ ['offsetToStartOfCentralDirectory', 4],
117
+ ['commentLength', 2],
118
+ ]);
119
+
120
+ // Is this zip file using zip64 format? Use same check as Go:
121
+ // https://github.com/golang/go/blob/master/src/archive/zip/reader.go#L503
122
+ // For zip64 files, need to find zip64 central directory locator header to extract
123
+ // relative offset for zip64 central directory record.
124
+ if (vars.diskNumber == 0xffff || vars.numberOfRecords == 0xffff ||
125
+ vars.offsetToStartOfCentralDirectory == 0xffffffff) {
126
+
127
+ // Offset to zip64 CDL is 20 bytes before normal CDR
128
+ const zip64CDLSize = 20;
129
+ const zip64CDLOffset = sourceSize - (tailSize - endDir.match + zip64CDLSize);
130
+ const zip64CDLStream = PullStream();
131
+
132
+ source.stream(zip64CDLOffset).pipe(zip64CDLStream);
133
+
134
+ return zip64CDLStream.pull(zip64CDLSize)
135
+ .then(function (d) { return getZip64CentralDirectory(source, d); })
136
+ .then(function (dir64record) {
137
+ vars = parseZip64DirRecord(dir64record);
138
+ });
139
+ } else {
140
+ vars.offsetToStartOfCentralDirectory += startOffset;
141
+ }
142
+ })
143
+ .then(function() {
144
+ if (vars.commentLength) return endDir.pull(vars.commentLength).then(function(comment) {
145
+ vars.comment = comment.toString('utf8');
146
+ });
147
+ })
148
+ .then(function() {
149
+ source.stream(vars.offsetToStartOfCentralDirectory).pipe(records);
150
+
151
+ vars.extract = function(opts) {
152
+ if (!opts || !opts.path) throw new Error('PATH_MISSING');
153
+ // make sure path is normalized before using it
154
+ opts.path = path.resolve(path.normalize(opts.path));
155
+ return vars.files.then(function(files) {
156
+ return Bluebird.map(files, async function(entry) {
157
+ // to avoid zip slip (writing outside of the destination), we resolve
158
+ // the target path, and make sure it's nested in the intended
159
+ // destination, or not extract it otherwise.
160
+ const extractPath = path.join(opts.path, entry.path);
161
+ if (extractPath.indexOf(opts.path) != 0) {
162
+ return;
163
+ }
164
+
165
+ if (entry.type == 'Directory') {
166
+ await fs.ensureDir(extractPath);
167
+ return;
168
+ }
169
+
170
+ await fs.ensureDir(path.dirname(extractPath));
171
+
172
+ const writer = opts.getWriter ? opts.getWriter({path: extractPath}) : fs.createWriteStream(extractPath);
173
+
174
+ return new Promise(function(resolve, reject) {
175
+ entry.stream(opts.password)
176
+ .on('error', reject)
177
+ .pipe(writer)
178
+ .on('close', resolve)
179
+ .on('error', reject);
180
+ });
181
+ }, { concurrency: opts.concurrency > 1 ? opts.concurrency : 1 });
182
+ });
183
+ };
184
+
185
+ vars.files = Bluebird.mapSeries(Array(vars.numberOfRecords), function() {
186
+ return records.pull(46).then(function(data) {
187
+ const vars = parseBuffer.parse(data, [
188
+ ['signature', 4],
189
+ ['versionMadeBy', 2],
190
+ ['versionsNeededToExtract', 2],
191
+ ['flags', 2],
192
+ ['compressionMethod', 2],
193
+ ['lastModifiedTime', 2],
194
+ ['lastModifiedDate', 2],
195
+ ['crc32', 4],
196
+ ['compressedSize', 4],
197
+ ['uncompressedSize', 4],
198
+ ['fileNameLength', 2],
199
+ ['extraFieldLength', 2],
200
+ ['fileCommentLength', 2],
201
+ ['diskNumber', 2],
202
+ ['internalFileAttributes', 2],
203
+ ['externalFileAttributes', 4],
204
+ ['offsetToLocalFileHeader', 4],
205
+ ]);
206
+
207
+ vars.offsetToLocalFileHeader += startOffset;
208
+ vars.lastModifiedDateTime = parseDateTime(vars.lastModifiedDate, vars.lastModifiedTime);
209
+
210
+ return records.pull(vars.fileNameLength).then(function(fileNameBuffer) {
211
+ vars.pathBuffer = fileNameBuffer;
212
+ vars.path = fileNameBuffer.toString('utf8');
213
+ vars.isUnicode = (vars.flags & 0x800) != 0;
214
+ return records.pull(vars.extraFieldLength);
215
+ })
216
+ .then(function(extraField) {
217
+ vars.extra = parseExtraField(extraField, vars);
218
+ return records.pull(vars.fileCommentLength);
219
+ })
220
+ .then(function(comment) {
221
+ vars.comment = comment;
222
+ vars.type = (vars.uncompressedSize === 0 && /[/\\]$/.test(vars.path)) ? 'Directory' : 'File';
223
+ const padding = options && options.padding || 1000;
224
+ vars.stream = function(_password) {
225
+ const totalSize = 30
226
+ + padding // add an extra buffer
227
+ + (vars.extraFieldLength || 0)
228
+ + (vars.fileNameLength || 0)
229
+ + vars.compressedSize;
230
+
231
+ return unzip(source, vars.offsetToLocalFileHeader, _password, vars, totalSize);
232
+ };
233
+ vars.buffer = function(_password) {
234
+ return BufferStream(vars.stream(_password));
235
+ };
236
+ return vars;
237
+ });
238
+ });
239
+ });
240
+
241
+ return Bluebird.props(vars);
242
+ });
243
+ };
@@ -0,0 +1,141 @@
1
+ const fs = require('graceful-fs');
2
+ const directory = require('./directory');
3
+ const Stream = require('stream');
4
+
5
+ module.exports = {
6
+ buffer: function(buffer, options) {
7
+ const source = {
8
+ stream: function(offset, length) {
9
+ const stream = Stream.PassThrough();
10
+ const end = length ? offset + length : undefined;
11
+ stream.end(buffer.slice(offset, end));
12
+ return stream;
13
+ },
14
+ size: function() {
15
+ return Promise.resolve(buffer.length);
16
+ }
17
+ };
18
+ return directory(source, options);
19
+ },
20
+ file: function(filename, options) {
21
+ const source = {
22
+ stream: function(start, length) {
23
+ const end = length ? start + length : undefined;
24
+ return fs.createReadStream(filename, {start, end});
25
+ },
26
+ size: function() {
27
+ return new Promise(function(resolve, reject) {
28
+ fs.stat(filename, function(err, d) {
29
+ if (err)
30
+ reject(err);
31
+ else
32
+ resolve(d.size);
33
+ });
34
+ });
35
+ }
36
+ };
37
+ return directory(source, options);
38
+ },
39
+
40
+ url: function(request, params, options) {
41
+ if (typeof params === 'string')
42
+ params = {url: params};
43
+ if (!params.url)
44
+ throw 'URL missing';
45
+ params.headers = params.headers || {};
46
+
47
+ const source = {
48
+ stream : function(offset, length) {
49
+ const options = Object.create(params);
50
+ const end = length ? offset + length : '';
51
+ options.headers = Object.create(params.headers);
52
+ options.headers.range = 'bytes='+offset+'-' + end;
53
+ return request(options);
54
+ },
55
+ size: function() {
56
+ return new Promise(function(resolve, reject) {
57
+ const req = request(params);
58
+ req.on('response', function(d) {
59
+ req.abort();
60
+ if (!d.headers['content-length'])
61
+ reject(new Error('Missing content length header'));
62
+ else
63
+ resolve(d.headers['content-length']);
64
+ }).on('error', reject);
65
+ });
66
+ }
67
+ };
68
+
69
+ return directory(source, options);
70
+ },
71
+
72
+ s3 : function(client, params, options) {
73
+ const source = {
74
+ size: function() {
75
+ return new Promise(function(resolve, reject) {
76
+ client.headObject(params, function(err, d) {
77
+ if (err)
78
+ reject(err);
79
+ else
80
+ resolve(d.ContentLength);
81
+ });
82
+ });
83
+ },
84
+ stream: function(offset, length) {
85
+ const d = {};
86
+ for (const key in params)
87
+ d[key] = params[key];
88
+ const end = length ? offset + length : '';
89
+ d.Range = 'bytes='+offset+'-' + end;
90
+ return client.getObject(d).createReadStream();
91
+ }
92
+ };
93
+
94
+ return directory(source, options);
95
+ },
96
+ s3_v3: function (client, params, options) {
97
+ //@ts-ignore
98
+ const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
99
+ const source = {
100
+ size: async () => {
101
+ const head = await client.send(
102
+ new HeadObjectCommand({
103
+ Bucket: params.Bucket,
104
+ Key: params.Key,
105
+ })
106
+ );
107
+
108
+ if(!head.ContentLength) {
109
+ return 0;
110
+ }
111
+
112
+ return head.ContentLength;
113
+ },
114
+ stream: (offset, length) => {
115
+ const stream = Stream.PassThrough();
116
+ const end = length ? offset + length : "";
117
+ client
118
+ .send(
119
+ new GetObjectCommand({
120
+ Bucket: params.Bucket,
121
+ Key: params.Key,
122
+ Range: `bytes=${offset}-${end}`,
123
+ })
124
+ )
125
+ .then((response) => {
126
+ response.Body.pipe(stream);
127
+ })
128
+ .catch((error) => {
129
+ stream.emit("error", error);
130
+ });
131
+
132
+ return stream;
133
+ },
134
+ };
135
+
136
+ return directory(source, options);
137
+ },
138
+ custom: function(source, options) {
139
+ return directory(source, options);
140
+ }
141
+ };
@@ -0,0 +1,120 @@
1
+ const Decrypt = require('../Decrypt');
2
+ const PullStream = require('../PullStream');
3
+ const Stream = require('stream');
4
+ const zlib = require('zlib');
5
+ const parseExtraField = require('../parseExtraField');
6
+ const parseDateTime = require('../parseDateTime');
7
+ const parseBuffer = require('../parseBuffer');
8
+
9
+ module.exports = function unzip(source, offset, _password, directoryVars, length) {
10
+ const file = PullStream(),
11
+ entry = Stream.PassThrough();
12
+
13
+ const req = source.stream(offset, length);
14
+ req.pipe(file).on('error', function(e) {
15
+ entry.emit('error', e);
16
+ });
17
+
18
+ entry.vars = file.pull(30)
19
+ .then(function(data) {
20
+ let vars = parseBuffer.parse(data, [
21
+ ['signature', 4],
22
+ ['versionsNeededToExtract', 2],
23
+ ['flags', 2],
24
+ ['compressionMethod', 2],
25
+ ['lastModifiedTime', 2],
26
+ ['lastModifiedDate', 2],
27
+ ['crc32', 4],
28
+ ['compressedSize', 4],
29
+ ['uncompressedSize', 4],
30
+ ['fileNameLength', 2],
31
+ ['extraFieldLength', 2],
32
+ ]);
33
+
34
+ vars.lastModifiedDateTime = parseDateTime(vars.lastModifiedDate, vars.lastModifiedTime);
35
+
36
+ return file.pull(vars.fileNameLength)
37
+ .then(function(fileName) {
38
+ vars.fileName = fileName.toString('utf8');
39
+ return file.pull(vars.extraFieldLength);
40
+ })
41
+ .then(function(extraField) {
42
+ let checkEncryption;
43
+ vars.extra = parseExtraField(extraField, vars);
44
+ // Ignore logal file header vars if the directory vars are available
45
+ if (directoryVars && directoryVars.compressedSize) vars = directoryVars;
46
+
47
+ if (vars.flags & 0x01) checkEncryption = file.pull(12)
48
+ .then(function(header) {
49
+ if (!_password)
50
+ throw new Error('MISSING_PASSWORD');
51
+
52
+ const decrypt = Decrypt();
53
+
54
+ String(_password).split('').forEach(function(d) {
55
+ decrypt.update(d);
56
+ });
57
+
58
+ for (let i=0; i < header.length; i++)
59
+ header[i] = decrypt.decryptByte(header[i]);
60
+
61
+ vars.decrypt = decrypt;
62
+ vars.compressedSize -= 12;
63
+
64
+ const check = (vars.flags & 0x8) ? (vars.lastModifiedTime >> 8) & 0xff : (vars.crc32 >> 24) & 0xff;
65
+ if (header[11] !== check)
66
+ throw new Error('BAD_PASSWORD');
67
+
68
+ return vars;
69
+ });
70
+
71
+ return Promise.resolve(checkEncryption)
72
+ .then(function() {
73
+ entry.emit('vars', vars);
74
+ return vars;
75
+ });
76
+ });
77
+ });
78
+
79
+ entry.vars.then(function(vars) {
80
+ const fileSizeKnown = !(vars.flags & 0x08) || vars.compressedSize > 0;
81
+ let eof;
82
+
83
+ const inflater = vars.compressionMethod ? zlib.createInflateRaw() : Stream.PassThrough();
84
+
85
+ if (fileSizeKnown) {
86
+ entry.size = vars.uncompressedSize;
87
+ eof = vars.compressedSize;
88
+ } else {
89
+ eof = Buffer.alloc(4);
90
+ eof.writeUInt32LE(0x08074b50, 0);
91
+ }
92
+
93
+ let stream = file.stream(eof);
94
+
95
+ if (vars.decrypt)
96
+ stream = stream.pipe(vars.decrypt.stream());
97
+
98
+ stream
99
+ .pipe(inflater)
100
+ .on('error', function(err) { entry.emit('error', err);})
101
+ .pipe(entry)
102
+ .on('finish', function() {
103
+ if(req.destroy)
104
+ req.destroy();
105
+ else if (req.abort)
106
+ req.abort();
107
+ else if (req.close)
108
+ req.close();
109
+ else if (req.push)
110
+ req.push();
111
+ else
112
+ console.log('warning - unable to close stream');
113
+ });
114
+ })
115
+ .catch(function(e) {
116
+ entry.emit('error', e);
117
+ });
118
+
119
+ return entry;
120
+ };
@@ -0,0 +1,139 @@
1
+ const Stream = require('stream');
2
+ const util = require('util');
3
+ const strFunction = 'function';
4
+
5
+ function PullStream() {
6
+ if (!(this instanceof PullStream))
7
+ return new PullStream();
8
+
9
+ Stream.Duplex.call(this, {decodeStrings:false, objectMode:true});
10
+ this.buffer = Buffer.from('');
11
+ const self = this;
12
+ self.on('finish', function() {
13
+ self.finished = true;
14
+ self.emit('chunk', false);
15
+ });
16
+ }
17
+
18
+ util.inherits(PullStream, Stream.Duplex);
19
+
20
+ PullStream.prototype._write = function(chunk, e, cb) {
21
+ this.buffer = Buffer.concat([this.buffer, chunk]);
22
+ this.cb = cb;
23
+ this.emit('chunk');
24
+ };
25
+
26
+
27
+ // The `eof` parameter is interpreted as `file_length` if the type is number
28
+ // otherwise (i.e. buffer) it is interpreted as a pattern signaling end of stream
29
+ PullStream.prototype.stream = function(eof, includeEof) {
30
+ const p = Stream.PassThrough();
31
+ let done;
32
+ const self= this;
33
+
34
+ function cb() {
35
+ if (typeof self.cb === strFunction) {
36
+ const callback = self.cb;
37
+ self.cb = undefined;
38
+ return callback();
39
+ }
40
+ }
41
+
42
+ function pull() {
43
+ let packet;
44
+ if (self.buffer && self.buffer.length) {
45
+ if (typeof eof === 'number') {
46
+ packet = self.buffer.slice(0, eof);
47
+ self.buffer = self.buffer.slice(eof);
48
+ eof -= packet.length;
49
+ done = done || !eof;
50
+ } else {
51
+ let match = self.buffer.indexOf(eof);
52
+ if (match !== -1) {
53
+ // store signature match byte offset to allow us to reference
54
+ // this for zip64 offset
55
+ self.match = match;
56
+ if (includeEof) match = match + eof.length;
57
+ packet = self.buffer.slice(0, match);
58
+ self.buffer = self.buffer.slice(match);
59
+ done = true;
60
+ } else {
61
+ const len = self.buffer.length - eof.length;
62
+ if (len <= 0) {
63
+ cb();
64
+ } else {
65
+ packet = self.buffer.slice(0, len);
66
+ self.buffer = self.buffer.slice(len);
67
+ }
68
+ }
69
+ }
70
+ if (packet) p.write(packet, function() {
71
+ if (self.buffer.length === 0 || (eof.length && self.buffer.length <= eof.length)) cb();
72
+ });
73
+ }
74
+
75
+ if (!done) {
76
+ if (self.finished) {
77
+ self.removeListener('chunk', pull);
78
+ self.emit('error', new Error('FILE_ENDED'));
79
+ return;
80
+ }
81
+
82
+ } else {
83
+ self.removeListener('chunk', pull);
84
+ p.end();
85
+ }
86
+ }
87
+
88
+ self.on('chunk', pull);
89
+ pull();
90
+ return p;
91
+ };
92
+
93
+ PullStream.prototype.pull = function(eof, includeEof) {
94
+ if (eof === 0) return Promise.resolve('');
95
+
96
+ // If we already have the required data in buffer
97
+ // we can resolve the request immediately
98
+ if (!isNaN(eof) && this.buffer.length > eof) {
99
+ const data = this.buffer.slice(0, eof);
100
+ this.buffer = this.buffer.slice(eof);
101
+ return Promise.resolve(data);
102
+ }
103
+
104
+ // Otherwise we stream until we have it
105
+ let buffer = Buffer.from('');
106
+ const self = this;
107
+
108
+ const concatStream = new Stream.Transform();
109
+ concatStream._transform = function(d, e, cb) {
110
+ buffer = Buffer.concat([buffer, d]);
111
+ cb();
112
+ };
113
+
114
+ let rejectHandler;
115
+ let pullStreamRejectHandler;
116
+ return new Promise(function(resolve, reject) {
117
+ rejectHandler = reject;
118
+ pullStreamRejectHandler = function(e) {
119
+ self.__emittedError = e;
120
+ reject(e);
121
+ };
122
+ if (self.finished)
123
+ return reject(new Error('FILE_ENDED'));
124
+ self.once('error', pullStreamRejectHandler); // reject any errors from pullstream itself
125
+ self.stream(eof, includeEof)
126
+ .on('error', reject)
127
+ .pipe(concatStream)
128
+ .on('finish', function() {resolve(buffer);})
129
+ .on('error', reject);
130
+ })
131
+ .finally(function() {
132
+ self.removeListener('error', rejectHandler);
133
+ self.removeListener('error', pullStreamRejectHandler);
134
+ });
135
+ };
136
+
137
+ PullStream.prototype._read = function(){};
138
+
139
+ module.exports = PullStream;
package/lib/extract.js ADDED
@@ -0,0 +1,62 @@
1
+ module.exports = Extract;
2
+
3
+ const Parse = require('./parse');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const stream = require('stream');
7
+ const duplexer2 = require('duplexer2');
8
+
9
+ function Extract (opts) {
10
+ // make sure path is normalized before using it
11
+ opts.path = path.resolve(path.normalize(opts.path));
12
+
13
+ const parser = new Parse(opts);
14
+
15
+ const outStream = new stream.Writable({objectMode: true});
16
+ outStream._write = async function(entry, encoding, cb) {
17
+
18
+ // to avoid zip slip (writing outside of the destination), we resolve
19
+ // the target path, and make sure it's nested in the intended
20
+ // destination, or not extract it otherwise.
21
+ // NOTE: Need to normalize to forward slashes for UNIX OS's to properly
22
+ // ignore the zip slipped file entirely
23
+ const extractPath = path.join(opts.path, entry.path.replace(/\\/g, '/'));
24
+ if (extractPath.indexOf(opts.path) != 0) {
25
+ return cb();
26
+ }
27
+
28
+
29
+ if (entry.type == 'Directory') {
30
+ await fs.ensureDir(extractPath);
31
+ return cb();
32
+ }
33
+
34
+ await fs.ensureDir(path.dirname(extractPath));
35
+
36
+ const writer = opts.getWriter ? opts.getWriter({path: extractPath}) : fs.createWriteStream(extractPath);
37
+
38
+ entry.pipe(writer)
39
+ .on('error', cb)
40
+ .on('close', cb);
41
+ };
42
+
43
+ const extract = duplexer2(parser, outStream);
44
+ parser.once('crx-header', function(crxHeader) {
45
+ extract.crxHeader = crxHeader;
46
+ });
47
+
48
+ parser
49
+ .pipe(outStream)
50
+ .on('finish', function() {
51
+ extract.emit('close');
52
+ });
53
+
54
+ extract.promise = function() {
55
+ return new Promise(function(resolve, reject) {
56
+ extract.on('close', resolve);
57
+ extract.on('error', reject);
58
+ });
59
+ };
60
+
61
+ return extract;
62
+ }