@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.
- package/.gitattributes +1 -0
- package/LICENSE +25 -0
- package/README.md +407 -0
- package/eslint.config.mjs +29 -0
- package/lib/BufferStream.js +19 -0
- package/lib/Decrypt.js +90 -0
- package/lib/NoopStream.js +14 -0
- package/lib/Open/directory.js +243 -0
- package/lib/Open/index.js +141 -0
- package/lib/Open/unzip.js +120 -0
- package/lib/PullStream.js +139 -0
- package/lib/extract.js +62 -0
- package/lib/parse.js +288 -0
- package/lib/parseBuffer.js +55 -0
- package/lib/parseDateTime.js +13 -0
- package/lib/parseExtraField.js +40 -0
- package/lib/parseOne.js +54 -0
- package/package.json +65 -0
- package/unzip.js +5 -0
|
@@ -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
|
+
}
|