UNPKG

zip-lib

Version:

A zip and unzip library for Node.js.

451 lines (450 loc) 17 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Unzip = void 0; const node_fs_1 = require("node:fs"); const fs = __importStar(require("node:fs/promises")); const path = __importStar(require("node:path")); const promises_1 = require("node:stream/promises"); const yauzl = __importStar(require("yauzl")); const cancelable_1 = require("./cancelable"); const exfs = __importStar(require("./fs")); class EntryEvent { /** * */ constructor(_entryCount) { this._entryCount = _entryCount; this._entryName = ""; this._isPrevented = false; } get entryName() { return this._entryName; } set entryName(name) { this._entryName = name; } get entryCount() { return this._entryCount; } get isPrevented() { return this._isPrevented; } preventDefault() { this._isPrevented = true; } reset() { this._isPrevented = false; } } class EntryContext { constructor(_targetFolder, _realTargetFolder, symlinkAsFileOnWindows) { this._targetFolder = _targetFolder; this._realTargetFolder = _realTargetFolder; this.symlinkAsFileOnWindows = symlinkAsFileOnWindows; this._decodeEntryFileName = ""; this._symlinkFileNames = []; this._symlinkFolders = []; this._inspectedFolders = []; this._ensuredFolders = []; } get decodeEntryFileName() { return this._decodeEntryFileName; } set decodeEntryFileName(name) { this._decodeEntryFileName = name; } get targetFolder() { return this._targetFolder; } get realTargetFolder() { return this._realTargetFolder; } get symlinkFileNames() { return this._symlinkFileNames; } addSymlinkFolder(folder, realpath) { const find = this._symlinkFolders.find((item) => item.folder === folder); if (!find) { this._symlinkFolders.push({ folder, realpath }); } } getFilePath() { return path.resolve(path.join(this.targetFolder, this.decodeEntryFileName)); } async inspectFolder(folder) { if (this._inspectedFolders.includes(folder) || folder === path.dirname(folder)) { return; } await this.inspectFolder(path.dirname(folder)); const folderStat = await exfs.statFolder(folder); if (!folderStat) { return; } if (folderStat.isSymbolicLink) { this.addSymlinkFolder(folder, folderStat.realpath); } this._inspectedFolders.push(folder); } async ensureFolder(folder) { if (this._ensuredFolders.includes(folder)) { return; } await this.inspectFolder(path.dirname(folder)); const folderStat = await exfs.ensureFolder(folder); if (folderStat.isSymbolicLink) { this.addSymlinkFolder(folder, folderStat.realpath); } this._ensuredFolders.push(folder); } isSymlinkTargetOutsideTargetFolder(linkTarget, linkFilePath) { const fileDir = path.dirname(linkFilePath); return exfs.isOutside(this.realTargetFolder, path.resolve(fileDir, linkTarget)); } async isOutsideTargetFolder(tpath) { if (this._symlinkFileNames.length === 0 && this._symlinkFolders.length === 0) { return false; } if (process.platform === "win32" && this.symlinkAsFileOnWindows) { return false; } for (const { folder, realpath } of this._symlinkFolders) { if (!exfs.isOutside(folder, tpath)) { if (exfs.isOutside(this.realTargetFolder, realpath)) { return true; } } } for (const fileName of this._symlinkFileNames) { if (!exfs.isOutside(fileName, tpath)) { const realFilePath = await exfs.realpath(tpath); if (exfs.isOutside(this.realTargetFolder, realFilePath)) { return true; } } } return false; } } /** * Extract the zip file. */ class Unzip extends cancelable_1.Cancelable { /** * */ constructor(options) { super(); this.options = options; this.zipFile = null; this.token = null; } async extract(zipFileOrBuffer, targetFolder) { const token = new cancelable_1.CancellationToken(); this.token = token; let currentZFile = null; try { const { zfile, realTargetFolder } = await this.prepareExtraction(zipFileOrBuffer, targetFolder, token); currentZFile = zfile; this.zipFile = zfile; await this.processEntries(zfile, targetFolder, realTargetFolder, token); } finally { if (this.token === token) { this.token = null; } if (this.zipFile && this.zipFile === currentZFile) { this.zipFile = null; } } } async prepareExtraction(zipFileOrBuffer, targetFolder, token) { if (this.isOverwrite()) { await exfs.rimraf(targetFolder); } if (token.isCancelled) { throw this.canceledError(); } await exfs.ensureFolder(targetFolder); const realTargetFolder = await exfs.realpath(targetFolder); const zfile = await this.openZip(zipFileOrBuffer, token); if (token.isCancelled) { throw this.canceledError(); } return { zfile, realTargetFolder }; } async processEntries(zfile, targetFolder, realTargetFolder, token) { var _a, e_1, _b, _c; try { const total = zfile.entryCount; const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile()); const entryEvent = new EntryEvent(total); try { for (var _d = true, _e = __asyncValues(zfile.eachEntry()), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) { _c = _f.value; _d = false; const entry = _c; await this.handleZipEntry(zfile, entry, entryContext, entryEvent, token); if (token.isCancelled) { throw this.canceledError(); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) await _b.call(_e); } finally { if (e_1) throw e_1.error; } } } catch (error) { throw this.wrapError(error, token.isCancelled); } } async handleZipEntry(zfile, entry, entryContext, entryEvent, token) { // use UTF-8 in all situations // see https://github.com/thejoshwolfe/yauzl/issues/84 const rawName = entry.fileName.toString("utf8"); // allow backslash const fileName = rawName.replace(/\\/g, "/"); // Because decodeStrings is false, we need to manually verify the entry name // see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename const errorMessage = yauzl.validateFileName(fileName); if (errorMessage != null) { throw new Error(errorMessage); } entryEvent.entryName = fileName; this.onEntryCallback(entryEvent); entryContext.decodeEntryFileName = fileName; if (entryEvent.isPrevented) { entryEvent.reset(); return; } await this.handleEntry(zfile, entry, entryContext, token); } /** * Cancel extraction. * If the cancel method is called after the extract is complete, nothing will happen. */ cancel() { if (this.token) { this.token.cancel(); this.token = null; } this.closeZip(); } closeZip(zfile) { const target = zfile !== null && zfile !== void 0 ? zfile : this.zipFile; if (target) { target.close(); } if (!zfile || this.zipFile === zfile) { this.zipFile = null; } } async openZip(zipFileOrBuffer, token) { const options = { lazyEntries: true, // see https://github.com/thejoshwolfe/yauzl/issues/84 decodeStrings: false, }; try { if (typeof zipFileOrBuffer === "string") { return yauzl.openPromise(zipFileOrBuffer, options); } else { return yauzl.fromBufferPromise(zipFileOrBuffer, options); } } catch (error) { throw this.wrapError(error, token.isCancelled); } } async handleEntry(zfile, entry, entryContext, token) { if (/\/$/.test(entryContext.decodeEntryFileName)) { // Directory file names end with '/'. // Note that entries for directories themselves are optional. // An entry's fileName implicitly requires its parent directories to exist. await exfs.ensureFolder(entryContext.getFilePath()); } else { // file entry await this.extractEntry(zfile, entry, entryContext, token); } } openZipFileStream(zfile, entry) { return zfile.openReadStreamPromise(entry); } async extractEntry(zfile, entry, entryContext, token) { const filePath = entryContext.getFilePath(); const fileDir = path.dirname(filePath); await entryContext.inspectFolder(fileDir); const outside = await entryContext.isOutsideTargetFolder(filePath); if (outside) { const error = new Error(`Refuse to write file outside "${entryContext.targetFolder}", file: "${filePath}"`); error.name = "AFWRITE"; throw error; } const readStream = await this.openZipFileStream(zfile, entry); await this.writeEntryToFile(readStream, entry, entryContext, token); } async writeEntryToFile(readStream, entry, entryContext, token) { var _a; try { const filePath = entryContext.getFilePath(); const mode = this.modeFromEntry(entry); // see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink const isSymlink = (mode & 0o170000) === 0o120000; if (isSymlink) { entryContext.symlinkFileNames.push(path.resolve(path.join(entryContext.targetFolder, entryContext.decodeEntryFileName))); } if (isSymlink && !this.symlinkToFile()) { const linkContent = await this.readStreamContent(readStream, token); if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.safeSymlinksOnly) && entryContext.isSymlinkTargetOutsideTargetFolder(linkContent, filePath)) { const error = new Error(`Dangerous link path was refused : "${entryContext.targetFolder}", file: "${filePath}", target: "${linkContent}". Set safeSymlinksOnly to false to allow writing through this symlink.`); error.name = "AF_ILLEGAL_TARGET"; throw error; } await entryContext.ensureFolder(path.dirname(filePath)); await this.createSymlink(linkContent, filePath); } else { await entryContext.ensureFolder(path.dirname(filePath)); const fileStream = (0, node_fs_1.createWriteStream)(filePath, { mode }); const pipelinePromise = (0, promises_1.pipeline)(readStream, fileStream); const disposeCancel = token.onCancelled(() => { fileStream.destroy(this.canceledError()); }); try { await pipelinePromise; } finally { disposeCancel(); } } } catch (error) { throw this.wrapError(error, token.isCancelled); } } async readStreamContent(readStream, token) { var _a, e_2, _b, _c; let linkContent = ""; try { for (var _d = true, readStream_1 = __asyncValues(readStream), readStream_1_1; readStream_1_1 = await readStream_1.next(), _a = readStream_1_1.done, !_a; _d = true) { _c = readStream_1_1.value; _d = false; const chunk = _c; linkContent += typeof chunk === "string" ? chunk : chunk.toString(); if (token.isCancelled) { throw this.canceledError(); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (!_d && !_a && (_b = readStream_1.return)) await _b.call(readStream_1); } finally { if (e_2) throw e_2.error; } } return linkContent; } modeFromEntry(entry) { const attr = entry.externalFileAttributes >> 16 || 33188; return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */] .map((mask) => attr & mask) .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */); } async createSymlink(linkContent, filePath) { let linkType = "file"; if (process.platform === "win32") { if (/\/$/.test(linkContent)) { linkType = "dir"; } else { let targetPath = linkContent; if (!path.isAbsolute(linkContent)) { targetPath = path.join(path.dirname(filePath), linkContent); } try { const stat = await fs.stat(targetPath); if (stat.isDirectory()) { linkType = "dir"; } } catch (_error) { // ignore } } } await fs.symlink(linkContent, filePath, linkType); } isOverwrite() { var _a; if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.overwrite) { return true; } return false; } onEntryCallback(event) { var _a; if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.onEntry) { this.options.onEntry(event); } } symlinkToFile() { let symlinkToFile = false; if (process.platform === "win32") { if (this.options && this.options.symlinkAsFileOnWindows === false) { symlinkToFile = false; } else { symlinkToFile = true; } } return symlinkToFile; } } exports.Unzip = Unzip;