zip-lib
Version:
A zip and unzip library for Node.js.
451 lines (450 loc) • 17 kB
JavaScript
"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;