import * as fs from "fs"; import * as path from "path"; import * as tarStream from "tar-stream"; import { promisify } from "util"; import * as nls from "vs/nls"; import * as vszip from "vs/base/node/zip"; import { CancellationToken } from "vs/base/common/cancellation"; import { mkdirp } from "vs/base/node/pfs"; // We will be overriding these, so keep a reference to the original. const vszipExtract = vszip.extract; const vszipBuffer = vszip.buffer; export interface IExtractOptions { overwrite?: boolean; /** * Source path within the TAR/ZIP archive. Only the files * contained in this path will be extracted. */ sourcePath?: string; } export interface IFile { path: string; contents?: Buffer | string; localPath?: string; } /** * Override the standard VS Code behavior for zipping extensions to use the TAR * format instead of ZIP. */ export const zip = (tarPath: string, files: IFile[]): Promise => { return new Promise((c, e): void => { const pack = tarStream.pack(); const chunks: Buffer[] = []; const ended = new Promise((res): void => { pack.on("end", () => { res(Buffer.concat(chunks)); }); }); pack.on("data", (chunk) => { chunks.push(chunk as Buffer); }); for (let i = 0; i < files.length; i++) { const file = files[i]; pack.entry({ name: file.path, }, file.contents); } pack.finalize(); ended.then((buffer) => { return promisify(fs.writeFile)(tarPath, buffer); }).then(() => { c(tarPath); }).catch((ex) => { e(ex); }); }); }; /** * Override the standard VS Code behavior for extracting archives to first * attempt to process the archive as a TAR and then fall back to the original * implementation for processing ZIPs. */ export const extract = (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise => { return new Promise((c, e): void => { extractTar(archivePath, extractPath, options, token).then(c).catch((ex) => { if (!ex.toString().includes("Invalid tar header")) { e(ex); return; } vszipExtract(archivePath, extractPath, options, token).then(c).catch(e); }); }); }; /** * Override the standard VS Code behavior for buffering archives to first * process the Buffer as a TAR and then fall back to the original * implementation for processing ZIPs. */ export const buffer = (targetPath: string, filePath: string): Promise => { return new Promise((c, e): void => { let done: boolean = false; extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => { if (path.normalize(assetPath) === path.normalize(filePath)) { done = true; c(data); } }).then(() => { if (!done) { e("couldn't find asset " + filePath); } }).catch((ex) => { if (!ex.toString().includes("Invalid tar header")) { e(ex); return; } vszipBuffer(targetPath, filePath).then(c).catch(e); }); }); }; /** * Override the standard VS Code behavior for extracting assets from archive * Buffers to use the TAR format instead of ZIP. */ const extractAssets = (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise => { return new Promise(async (c, e): Promise => { try { const buffer = await promisify(fs.readFile)(tarPath); const extractor = tarStream.extract(); extractor.once("error", e); extractor.on("entry", (header, stream, next) => { const name = header.name; if (match.test(name)) { extractData(stream).then((data) => { callback(name, data); next(); }).catch(e); stream.resume(); } else { stream.on("end", () => { next(); }); stream.resume(); } }); extractor.on("finish", () => { c(); }); extractor.write(buffer); extractor.end(); } catch (ex) { e(ex); } }); }; const extractData = (stream: NodeJS.ReadableStream): Promise => { return new Promise((c, e): void => { const fileData: Buffer[] = []; stream.on("data", (data) => fileData.push(data)); stream.on("end", () => { const fd = Buffer.concat(fileData); c(fd); }); stream.on("error", e); }); }; const extractTar = (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise => { return new Promise(async (c, e): Promise => { try { const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ""); const buffer = await promisify(fs.readFile)(tarPath); const extractor = tarStream.extract(); extractor.once("error", e); extractor.on("entry", (header, stream, next) => { const rawName = path.normalize(header.name); const nextEntry = (): void => { stream.resume(); next(); }; if (token.isCancellationRequested) { return nextEntry(); } if (!sourcePathRegex.test(rawName)) { return nextEntry(); } const fileName = rawName.replace(sourcePathRegex, ""); const targetFileName = path.join(targetPath, fileName); if (/\/$/.test(fileName)) { stream.resume(); mkdirp(targetFileName).then(() => { next(); }, e); return; } const dirName = path.dirname(fileName); const targetDirName = path.join(targetPath, dirName); if (targetDirName.indexOf(targetPath) !== 0) { e(nls.localize("invalid file", "Error extracting {0}. Invalid file.", fileName)); return nextEntry(); } return mkdirp(targetDirName, undefined, token).then(() => { const fstream = fs.createWriteStream(targetFileName, { mode: header.mode }); fstream.once("close", () => { next(); }); fstream.once("error", e); stream.pipe(fstream); stream.resume(); }); }); extractor.once("finish", c); extractor.write(buffer); extractor.end(); } catch (ex) { e(ex); } }); }; // Override original functionality so we can use tar instead of zip. const target = vszip as typeof vszip; target.zip = zip; target.extract = extract; target.buffer = buffer;