UNPKG

unzipper-esm

Version:
249 lines (216 loc) 8.96 kB
import path from 'path'; // 'node:path' import fs from 'fs'; // 'node:fs' import unzip from './unzip.js'; import mapPromises from '../mapPromises.js'; import PullStream from '../PullStream.js'; import BufferStream from '../BufferStream.js'; import parseExtraField from '../parseExtraField.js'; import parseDateTime from '../parseDateTime.js'; import parseBuffer from '../parseBuffer.js'; import ensureDir from '../ensureDir.js'; const signature = Buffer.alloc(4); signature.writeUInt32LE(0x06054b50, 0); function getCrxHeader(source) { const sourceStream = source.stream(0).pipe(new 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(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(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 = new 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(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; } export default function centralDirectory(source, options) { const endDir = new PullStream(); const records = new 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 Promise.all([endDir.pull(22), crxHeader]); }) .then(function([directory, crxHeader]) { const data = directory; startOffset = crxHeader && crxHeader.size || 0; vars = parseBuffer(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 = new 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(async function() { source.stream(vars.offsetToStartOfCentralDirectory).pipe(records); vars.extract = async 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)); const files = vars.files; return await mapPromises(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 ensureDir(extractPath); return; } await 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 = []; let i = 0; while (i < vars.numberOfRecords) { vars.files[i] = await records.pull(46).then(function(data) { const vars = parseBuffer(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; }); }); i++; } return vars; }); };