244 lines
8.5 KiB
JavaScript
244 lines
8.5 KiB
JavaScript
|
|
const PullStream = require('../PullStream');
|
||
|
|
const unzip = require('./unzip');
|
||
|
|
const BufferStream = require('../BufferStream');
|
||
|
|
const parseExtraField = require('../parseExtraField');
|
||
|
|
const path = require('path');
|
||
|
|
const fs = require('fs-extra');
|
||
|
|
const parseDateTime = require('../parseDateTime');
|
||
|
|
const parseBuffer = require('../parseBuffer');
|
||
|
|
const Bluebird = require('bluebird');
|
||
|
|
|
||
|
|
const signature = Buffer.alloc(4);
|
||
|
|
signature.writeUInt32LE(0x06054b50, 0);
|
||
|
|
|
||
|
|
function getCrxHeader(source) {
|
||
|
|
const sourceStream = source.stream(0).pipe(PullStream());
|
||
|
|
|
||
|
|
return sourceStream.pull(4).then(function(data) {
|
||
|
|
const signature = data.readUInt32LE(0);
|
||
|
|
if (signature === 0x34327243) {
|
||
|
|
let crxHeader;
|
||
|
|
return sourceStream.pull(12).then(function(data) {
|
||
|
|
crxHeader = parseBuffer.parse(data, [
|
||
|
|
['version', 4],
|
||
|
|
['pubKeyLength', 4],
|
||
|
|
['signatureLength', 4],
|
||
|
|
]);
|
||
|
|
}).then(function() {
|
||
|
|
return sourceStream.pull(crxHeader.pubKeyLength +crxHeader.signatureLength);
|
||
|
|
}).then(function(data) {
|
||
|
|
crxHeader.publicKey = data.slice(0, crxHeader.pubKeyLength);
|
||
|
|
crxHeader.signature = data.slice(crxHeader.pubKeyLength);
|
||
|
|
crxHeader.size = 16 + crxHeader.pubKeyLength +crxHeader.signatureLength;
|
||
|
|
return crxHeader;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||
|
|
function getZip64CentralDirectory(source, zip64CDL) {
|
||
|
|
const d64loc = parseBuffer.parse(zip64CDL, [
|
||
|
|
['signature', 4],
|
||
|
|
['diskNumber', 4],
|
||
|
|
['offsetToStartOfCentralDirectory', 8],
|
||
|
|
['numberOfDisks', 4],
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (d64loc.signature != 0x07064b50) {
|
||
|
|
throw new Error('invalid zip64 end of central dir locator signature (0x07064b50): 0x' + d64loc.signature.toString(16));
|
||
|
|
}
|
||
|
|
|
||
|
|
const dir64 = PullStream();
|
||
|
|
source.stream(d64loc.offsetToStartOfCentralDirectory).pipe(dir64);
|
||
|
|
|
||
|
|
return dir64.pull(56);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||
|
|
function parseZip64DirRecord (dir64record) {
|
||
|
|
const vars = parseBuffer.parse(dir64record, [
|
||
|
|
['signature', 4],
|
||
|
|
['sizeOfCentralDirectory', 8],
|
||
|
|
['version', 2],
|
||
|
|
['versionsNeededToExtract', 2],
|
||
|
|
['diskNumber', 4],
|
||
|
|
['diskStart', 4],
|
||
|
|
['numberOfRecordsOnDisk', 8],
|
||
|
|
['numberOfRecords', 8],
|
||
|
|
['sizeOfCentralDirectory', 8],
|
||
|
|
['offsetToStartOfCentralDirectory', 8],
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (vars.signature != 0x06064b50) {
|
||
|
|
throw new Error('invalid zip64 end of central dir locator signature (0x06064b50): 0x0' + vars.signature.toString(16));
|
||
|
|
}
|
||
|
|
|
||
|
|
return vars;
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = function centralDirectory(source, options) {
|
||
|
|
const endDir = PullStream();
|
||
|
|
const records = PullStream();
|
||
|
|
const tailSize = (options && options.tailSize) || 80;
|
||
|
|
let sourceSize,
|
||
|
|
crxHeader,
|
||
|
|
startOffset,
|
||
|
|
vars;
|
||
|
|
|
||
|
|
if (options && options.crx)
|
||
|
|
crxHeader = getCrxHeader(source);
|
||
|
|
|
||
|
|
return source.size()
|
||
|
|
.then(function(size) {
|
||
|
|
sourceSize = size;
|
||
|
|
|
||
|
|
source.stream(Math.max(0, size-tailSize))
|
||
|
|
.on('error', function (error) { endDir.emit('error', error); })
|
||
|
|
.pipe(endDir);
|
||
|
|
|
||
|
|
return endDir.pull(signature);
|
||
|
|
})
|
||
|
|
.then(function() {
|
||
|
|
return Bluebird.props({directory: endDir.pull(22), crxHeader: crxHeader});
|
||
|
|
})
|
||
|
|
.then(function(d) {
|
||
|
|
const data = d.directory;
|
||
|
|
startOffset = d.crxHeader && d.crxHeader.size || 0;
|
||
|
|
|
||
|
|
vars = parseBuffer.parse(data, [
|
||
|
|
['signature', 4],
|
||
|
|
['diskNumber', 2],
|
||
|
|
['diskStart', 2],
|
||
|
|
['numberOfRecordsOnDisk', 2],
|
||
|
|
['numberOfRecords', 2],
|
||
|
|
['sizeOfCentralDirectory', 4],
|
||
|
|
['offsetToStartOfCentralDirectory', 4],
|
||
|
|
['commentLength', 2],
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Is this zip file using zip64 format? Use same check as Go:
|
||
|
|
// https://github.com/golang/go/blob/master/src/archive/zip/reader.go#L503
|
||
|
|
// For zip64 files, need to find zip64 central directory locator header to extract
|
||
|
|
// relative offset for zip64 central directory record.
|
||
|
|
if (vars.diskNumber == 0xffff || vars.numberOfRecords == 0xffff ||
|
||
|
|
vars.offsetToStartOfCentralDirectory == 0xffffffff) {
|
||
|
|
|
||
|
|
// Offset to zip64 CDL is 20 bytes before normal CDR
|
||
|
|
const zip64CDLSize = 20;
|
||
|
|
const zip64CDLOffset = sourceSize - (tailSize - endDir.match + zip64CDLSize);
|
||
|
|
const zip64CDLStream = PullStream();
|
||
|
|
|
||
|
|
source.stream(zip64CDLOffset).pipe(zip64CDLStream);
|
||
|
|
|
||
|
|
return zip64CDLStream.pull(zip64CDLSize)
|
||
|
|
.then(function (d) { return getZip64CentralDirectory(source, d); })
|
||
|
|
.then(function (dir64record) {
|
||
|
|
vars = parseZip64DirRecord(dir64record);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
vars.offsetToStartOfCentralDirectory += startOffset;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.then(function() {
|
||
|
|
if (vars.commentLength) return endDir.pull(vars.commentLength).then(function(comment) {
|
||
|
|
vars.comment = comment.toString('utf8');
|
||
|
|
});
|
||
|
|
})
|
||
|
|
.then(function() {
|
||
|
|
source.stream(vars.offsetToStartOfCentralDirectory).pipe(records);
|
||
|
|
|
||
|
|
vars.extract = function(opts) {
|
||
|
|
if (!opts || !opts.path) throw new Error('PATH_MISSING');
|
||
|
|
// make sure path is normalized before using it
|
||
|
|
opts.path = path.resolve(path.normalize(opts.path));
|
||
|
|
return vars.files.then(function(files) {
|
||
|
|
return Bluebird.map(files, async function(entry) {
|
||
|
|
// to avoid zip slip (writing outside of the destination), we resolve
|
||
|
|
// the target path, and make sure it's nested in the intended
|
||
|
|
// destination, or not extract it otherwise.
|
||
|
|
const extractPath = path.join(opts.path, entry.path);
|
||
|
|
if (extractPath.indexOf(opts.path) != 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.type == 'Directory') {
|
||
|
|
await fs.ensureDir(extractPath);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await fs.ensureDir(path.dirname(extractPath));
|
||
|
|
|
||
|
|
const writer = opts.getWriter ? opts.getWriter({path: extractPath}) : fs.createWriteStream(extractPath);
|
||
|
|
|
||
|
|
return new Promise(function(resolve, reject) {
|
||
|
|
entry.stream(opts.password)
|
||
|
|
.on('error', reject)
|
||
|
|
.pipe(writer)
|
||
|
|
.on('close', resolve)
|
||
|
|
.on('error', reject);
|
||
|
|
});
|
||
|
|
}, { concurrency: opts.concurrency > 1 ? opts.concurrency : 1 });
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
vars.files = Bluebird.mapSeries(Array(vars.numberOfRecords), function() {
|
||
|
|
return records.pull(46).then(function(data) {
|
||
|
|
const vars = parseBuffer.parse(data, [
|
||
|
|
['signature', 4],
|
||
|
|
['versionMadeBy', 2],
|
||
|
|
['versionsNeededToExtract', 2],
|
||
|
|
['flags', 2],
|
||
|
|
['compressionMethod', 2],
|
||
|
|
['lastModifiedTime', 2],
|
||
|
|
['lastModifiedDate', 2],
|
||
|
|
['crc32', 4],
|
||
|
|
['compressedSize', 4],
|
||
|
|
['uncompressedSize', 4],
|
||
|
|
['fileNameLength', 2],
|
||
|
|
['extraFieldLength', 2],
|
||
|
|
['fileCommentLength', 2],
|
||
|
|
['diskNumber', 2],
|
||
|
|
['internalFileAttributes', 2],
|
||
|
|
['externalFileAttributes', 4],
|
||
|
|
['offsetToLocalFileHeader', 4],
|
||
|
|
]);
|
||
|
|
|
||
|
|
vars.offsetToLocalFileHeader += startOffset;
|
||
|
|
vars.lastModifiedDateTime = parseDateTime(vars.lastModifiedDate, vars.lastModifiedTime);
|
||
|
|
|
||
|
|
return records.pull(vars.fileNameLength).then(function(fileNameBuffer) {
|
||
|
|
vars.pathBuffer = fileNameBuffer;
|
||
|
|
vars.path = fileNameBuffer.toString('utf8');
|
||
|
|
vars.isUnicode = (vars.flags & 0x800) != 0;
|
||
|
|
return records.pull(vars.extraFieldLength);
|
||
|
|
})
|
||
|
|
.then(function(extraField) {
|
||
|
|
vars.extra = parseExtraField(extraField, vars);
|
||
|
|
return records.pull(vars.fileCommentLength);
|
||
|
|
})
|
||
|
|
.then(function(comment) {
|
||
|
|
vars.comment = comment;
|
||
|
|
vars.type = (vars.uncompressedSize === 0 && /[/\\]$/.test(vars.path)) ? 'Directory' : 'File';
|
||
|
|
const padding = options && options.padding || 1000;
|
||
|
|
vars.stream = function(_password) {
|
||
|
|
const totalSize = 30
|
||
|
|
+ padding // add an extra buffer
|
||
|
|
+ (vars.extraFieldLength || 0)
|
||
|
|
+ (vars.fileNameLength || 0)
|
||
|
|
+ vars.compressedSize;
|
||
|
|
|
||
|
|
return unzip(source, vars.offsetToLocalFileHeader, _password, vars, totalSize);
|
||
|
|
};
|
||
|
|
vars.buffer = function(_password) {
|
||
|
|
return BufferStream(vars.stream(_password));
|
||
|
|
};
|
||
|
|
return vars;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
return Bluebird.props(vars);
|
||
|
|
});
|
||
|
|
};
|