Fix install from VSIX for TAR and ZIP formats (#245)

* Fix install from VSIX for TAR and ZIP formats

* Parse TAR before ZIP, when installing from VSIX
This commit is contained in:
Forest Hoffman 2019-03-21 14:04:09 -05:00 committed by Kyle Carberry
parent 75435be949
commit 18f395b853
8 changed files with 212 additions and 111 deletions

View File

@ -12,6 +12,11 @@
"xmlhttprequest": "1.8.0" "xmlhttprequest": "1.8.0"
}, },
"jest": { "jest": {
"globals": {
"ts-jest": {
"diagnostics": false
}
},
"moduleFileExtensions": [ "moduleFileExtensions": [
"ts", "ts",
"tsx", "tsx",
@ -26,7 +31,9 @@
"@coder/ide/src/fill/evaluation": "<rootDir>/ide/src/fill/evaluation", "@coder/ide/src/fill/evaluation": "<rootDir>/ide/src/fill/evaluation",
"@coder/ide/src/fill/client": "<rootDir>/ide/src/fill/client", "@coder/ide/src/fill/client": "<rootDir>/ide/src/fill/client",
"@coder/(.*)/test": "<rootDir>/$1/test", "@coder/(.*)/test": "<rootDir>/$1/test",
"@coder/(.*)": "<rootDir>/$1/src" "@coder/(.*)": "<rootDir>/$1/src",
"vs/(.*)": "<rootDir>/../lib/vscode/src/vs/$1",
"vszip": "<rootDir>/../lib/vscode/src/vs/base/node/zip.ts"
}, },
"transform": { "transform": {
"^.+\\.tsx?$": "ts-jest" "^.+\\.tsx?$": "ts-jest"

View File

@ -1 +1,2 @@
bin bin
test/.test*

View File

@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as nls from "vs/nls"; import * as nls from "vs/nls";
import * as vszip from "vszip";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import * as tarStream from "tar-stream"; import * as tarStream from "tar-stream";
import { promisify } from "util"; import { promisify } from "util";
import { ILogService } from "vs/platform/log/common/log";
import { CancellationToken } from "vs/base/common/cancellation"; import { CancellationToken } from "vs/base/common/cancellation";
import { mkdirp } from "vs/base/node/pfs"; import { mkdirp } from "vs/base/node/pfs";
@ -16,8 +16,8 @@ export interface IExtractOptions {
overwrite?: boolean; overwrite?: boolean;
/** /**
* Source path within the ZIP archive. Only the files contained in this * Source path within the TAR/ZIP archive. Only the files
* path will be extracted. * contained in this path will be extracted.
*/ */
sourcePath?: string; sourcePath?: string;
} }
@ -28,11 +28,15 @@ export interface IFile {
localPath?: string; localPath?: string;
} }
export function zip(tarPath: string, files: IFile[]): Promise<string> { /**
return new Promise<string>((c, e) => { * 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<string> => {
return new Promise<string>((c, e): void => {
const pack = tarStream.pack(); const pack = tarStream.pack();
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
const ended = new Promise<Buffer>((res, rej) => { const ended = new Promise<Buffer>((res): void => {
pack.on("end", () => { pack.on("end", () => {
res(Buffer.concat(chunks)); res(Buffer.concat(chunks));
}); });
@ -56,132 +60,160 @@ export function zip(tarPath: string, files: IFile[]): Promise<string> {
e(ex); e(ex);
}); });
}); });
} };
export async function extract(tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> { /**
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ''); * Override the standard VS Code behavior for extracting
* archives, to first attempt to process the archive as a TAR
* and then fallback on the original implementation, for processing
* ZIPs.
*/
export const extract = (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
return new Promise<void>((c, e): void => {
extractTar(archivePath, extractPath, options, token).then(c).catch((ex) => {
if (!ex.toString().includes("Invalid tar header")) {
e(ex);
return new Promise<void>(async (c, e) => {
const buffer = await promisify(fs.readFile)(tarPath);
const extractor = tarStream.extract();
extractor.once('error', e);
extractor.on('entry', (header, stream, next) => {
const rawName = 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; return;
} }
vszip.extract(archivePath, extractPath, options, token).then(c).catch(e);
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();
}
mkdirp(targetDirName, void 0, token).then(() => {
const fstream = fs.createWriteStream(targetFileName, { mode: header.mode });
fstream.once('close', () => {
next();
});
fstream.once('error', (err) => {
e(err);
});
stream.pipe(fstream);
stream.resume();
});
}); });
extractor.once('finish', () => {
c();
});
extractor.write(buffer);
extractor.end();
}); });
} };
export function buffer(tarPath: string, filePath: string): Promise<Buffer> { /**
return new Promise<Buffer>(async (c, e) => { * Override the standard VS Code behavior for buffering
* archives, to first process the Buffer as a TAR and then
* fallback on the original implementation, for processing ZIPs.
*/
export const buffer = (targetPath: string, filePath: string): Promise<Buffer> => {
return new Promise<Buffer>((c, e): void => {
let done: boolean = false; let done: boolean = false;
extractAssets(tarPath, new RegExp(filePath), (path: string, data: Buffer) => { extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => {
if (path === filePath) { if (path.normalize(assetPath) === path.normalize(filePath)) {
done = true; done = true;
c(data); c(data);
} }
}).then(() => { }).then(() => {
if (!done) { if (!done) {
e("couldnt find asset " + filePath); e("couldn't find asset " + filePath);
} }
}).catch((ex) => { }).catch((ex) => {
e(ex); if (!ex.toString().includes("Invalid tar header")) {
e(ex);
return;
}
vszip.buffer(targetPath, filePath).then(c).catch(e);
}); });
}); });
} };
async function extractAssets(tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> { /**
const buffer = await promisify(fs.readFile)(tarPath); * Override the standard VS Code behavior for extracting assets
const extractor = tarStream.extract(); * from archive Buffers to use the TAR format instead of ZIP.
let callbackResolve: () => void; */
let callbackReject: (ex?) => void; export const extractAssets = (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> => {
const complete = new Promise<void>((r, rej) => { return new Promise<void>(async (c, e): Promise<void> => {
callbackResolve = r; try {
callbackReject = rej; const buffer = await promisify(fs.readFile)(tarPath);
}); const extractor = tarStream.extract();
extractor.once("error", (err) => { extractor.once("error", e);
callbackReject(err); extractor.on("entry", (header, stream, next) => {
}); const name = header.name;
extractor.on("entry", (header, stream, next) => { if (match.test(name)) {
const name = header.name; extractData(stream).then((data) => {
if (match.test(name)) { callback(name, data);
extractData(stream).then((data) => { next();
callback(name, data); }).catch(e);
next(); stream.resume();
} else {
stream.on("end", () => {
next();
});
stream.resume();
}
}); });
stream.resume(); extractor.on("finish", () => {
} else { c();
stream.on("end", () => {
next();
}); });
stream.resume(); extractor.write(buffer);
extractor.end();
} catch (ex) {
e(ex);
} }
}); });
extractor.on("finish", () => { };
callbackResolve();
});
extractor.write(buffer);
extractor.end();
return complete;
}
async function extractData(stream: NodeJS.ReadableStream): Promise<Buffer> { const extractData = (stream: NodeJS.ReadableStream): Promise<Buffer> => {
return new Promise<Buffer>((res, rej) => { return new Promise<Buffer>((c, e): void => {
const fileData: Buffer[] = []; const fileData: Buffer[] = [];
stream.on('data', (data) => fileData.push(data)); stream.on("data", (data) => fileData.push(data));
stream.on('end', () => { stream.on("end", () => {
const fd = Buffer.concat(fileData); const fd = Buffer.concat(fileData);
res(fd); c(fd);
});
stream.on('error', (err) => {
rej(err);
}); });
stream.on("error", e);
}); });
} };
const extractTar = (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
return new Promise<void>(async (c, e): Promise<void> => {
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);
}
});
};

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,59 @@
import * as zip from "../src/fill/zip";
import * as path from "path";
import * as fs from "fs";
import * as cp from "child_process";
import { CancellationToken } from "vs/base/common/cancellation";
// tslint:disable-next-line:no-any
jest.mock("vs/nls", () => ({ "localize": (...args: any): string => `${JSON.stringify(args)}` }));
describe("zip", () => {
const tarPath = path.resolve(__dirname, "./test-extension.tar");
const vsixPath = path.resolve(__dirname, "./test-extension.vsix");
const extractPath = path.resolve(__dirname, "./.test-extension");
beforeEach(() => {
if (!fs.existsSync(extractPath) || path.dirname(extractPath) !== __dirname) {
return;
}
cp.execSync(`rm -rf '${extractPath}'`);
});
const resolveExtract = async (archivePath: string): Promise<void> => {
expect(fs.existsSync(archivePath)).toEqual(true);
await expect(zip.extract(
archivePath,
extractPath,
{ sourcePath: "extension", overwrite: true },
CancellationToken.None,
)).resolves.toBe(undefined);
expect(fs.existsSync(extractPath)).toEqual(true);
};
// tslint:disable-next-line:no-any
const extract = (archivePath: string): () => any => {
// tslint:disable-next-line:no-any
return async (): Promise<any> => {
await resolveExtract(archivePath);
expect(fs.existsSync(path.resolve(extractPath, ".vsixmanifest"))).toEqual(true);
expect(fs.existsSync(path.resolve(extractPath, "package.json"))).toEqual(true);
};
};
it("should extract from tarred VSIX", extract(tarPath), 2000);
it("should extract from zipped VSIX", extract(vsixPath), 2000);
// tslint:disable-next-line:no-any
const buffer = (archivePath: string): () => any => {
// tslint:disable-next-line:no-any
return async (): Promise<any> => {
await resolveExtract(archivePath);
const manifestPath = path.resolve(extractPath, ".vsixmanifest");
expect(fs.existsSync(manifestPath)).toEqual(true);
const manifestBuf = fs.readFileSync(manifestPath);
expect(manifestBuf.length).toBeGreaterThan(0);
await expect(zip.buffer(archivePath, "extension.vsixmanifest")).resolves.toEqual(manifestBuf);
};
};
it("should buffer tarred VSIX", buffer(tarPath), 2000);
it("should buffer zipped VSIX", buffer(vsixPath), 2000);
});

View File

@ -62,6 +62,7 @@ module.exports = merge(
"vs/platform/product/node/package": path.resolve(vsFills, "package.ts"), "vs/platform/product/node/package": path.resolve(vsFills, "package.ts"),
"vs/platform/product/node/product": path.resolve(vsFills, "product.ts"), "vs/platform/product/node/product": path.resolve(vsFills, "product.ts"),
"vs/base/node/zip": path.resolve(vsFills, "zip.ts"), "vs/base/node/zip": path.resolve(vsFills, "zip.ts"),
"vszip": path.resolve(root, "lib/vscode/src/vs/base/node/zip.ts"),
"vs": path.resolve(root, "lib/vscode/src/vs"), "vs": path.resolve(root, "lib/vscode/src/vs"),
}, },
}, },

View File

@ -75,6 +75,7 @@ module.exports = merge(
"vs/platform/product/node/package": path.resolve(vsFills, "package.ts"), "vs/platform/product/node/package": path.resolve(vsFills, "package.ts"),
"vs/platform/product/node/product": path.resolve(vsFills, "product.ts"), "vs/platform/product/node/product": path.resolve(vsFills, "product.ts"),
"vs/base/node/zip": path.resolve(vsFills, "zip.ts"), "vs/base/node/zip": path.resolve(vsFills, "zip.ts"),
"vszip": path.resolve(root, "lib/vscode/src/vs/base/node/zip.ts"),
"vs": path.join(root, "lib", "vscode", "src", "vs"), "vs": path.join(root, "lib", "vscode", "src", "vs"),
}, },
}, },