Skip to content

Commit b833591

Browse files
authored
Merge pull request #3150 from github/koesie10/archive-filesystem-yauzl
Switch to `yauzl` in the archive filesystem provider
2 parents 82f17c4 + bc0506d commit b833591

File tree

5 files changed

+128
-24
lines changed

5 files changed

+128
-24
lines changed

extensions/ql-vscode/package-lock.json

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,7 @@
19461946
"vscode-languageclient": "^8.0.2",
19471947
"vscode-test-adapter-api": "^1.7.0",
19481948
"vscode-test-adapter-util": "^0.7.0",
1949+
"yauzl": "^2.10.0",
19491950
"zip-a-folder": "^3.1.3"
19501951
},
19511952
"devDependencies": {
@@ -1997,6 +1998,7 @@
19971998
"@types/vscode": "^1.82.0",
19981999
"@types/webpack": "^5.28.0",
19992000
"@types/webpack-env": "^1.18.0",
2001+
"@types/yauzl": "^2.10.3",
20002002
"@typescript-eslint/eslint-plugin": "^6.2.1",
20012003
"@typescript-eslint/parser": "^6.14.0",
20022004
"@vscode/test-electron": "^2.2.0",
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Entry as ZipEntry, open, Options as ZipOptions, ZipFile } from "yauzl";
2+
import { Readable } from "stream";
3+
4+
// We can't use promisify because it picks up the wrong overload.
5+
export function openZip(
6+
path: string,
7+
options: ZipOptions = {},
8+
): Promise<ZipFile> {
9+
return new Promise((resolve, reject) => {
10+
open(path, options, (err, zipFile) => {
11+
if (err) {
12+
reject(err);
13+
return;
14+
}
15+
16+
resolve(zipFile);
17+
});
18+
});
19+
}
20+
21+
export function excludeDirectories(entries: ZipEntry[]): ZipEntry[] {
22+
return entries.filter((entry) => !/\/$/.test(entry.fileName));
23+
}
24+
25+
export function readZipEntries(zipFile: ZipFile): Promise<ZipEntry[]> {
26+
return new Promise((resolve, reject) => {
27+
const files: ZipEntry[] = [];
28+
29+
zipFile.readEntry();
30+
zipFile.on("entry", (entry: ZipEntry) => {
31+
if (/\/$/.test(entry.fileName)) {
32+
// Directory file names end with '/'
33+
// We don't need to do anything for directories.
34+
} else {
35+
files.push(entry);
36+
}
37+
38+
zipFile.readEntry();
39+
});
40+
41+
zipFile.on("end", () => {
42+
resolve(files);
43+
});
44+
45+
zipFile.on("error", (err) => {
46+
reject(err);
47+
});
48+
});
49+
}
50+
51+
function openZipReadStream(
52+
zipFile: ZipFile,
53+
entry: ZipEntry,
54+
): Promise<Readable> {
55+
return new Promise((resolve, reject) => {
56+
zipFile.openReadStream(entry, (err, readStream) => {
57+
if (err) {
58+
reject(err);
59+
return;
60+
}
61+
62+
resolve(readStream);
63+
});
64+
});
65+
}
66+
67+
export async function openZipBuffer(
68+
zipFile: ZipFile,
69+
entry: ZipEntry,
70+
): Promise<Buffer> {
71+
const readable = await openZipReadStream(zipFile, entry);
72+
return new Promise((resolve, reject) => {
73+
const chunks: Buffer[] = [];
74+
readable.on("data", (chunk) => {
75+
chunks.push(chunk);
76+
});
77+
readable.on("error", (err) => {
78+
reject(err);
79+
});
80+
readable.on("end", () => {
81+
resolve(Buffer.concat(chunks));
82+
});
83+
});
84+
}

extensions/ql-vscode/src/common/vscode/archive-filesystem-provider.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { pathExists } from "fs-extra";
2-
import * as unzipper from "unzipper";
2+
import { Entry as ZipEntry, ZipFile } from "yauzl";
33
import * as vscode from "vscode";
44
import { extLogger } from "../logging/vscode";
5+
import {
6+
excludeDirectories,
7+
openZip,
8+
openZipBuffer,
9+
readZipEntries,
10+
} from "../unzip";
511

612
// All path operations in this file must be on paths *within* the zip
713
// archive.
@@ -177,20 +183,31 @@ function ensureDir(map: DirectoryHierarchyMap, dir: string) {
177183
}
178184

179185
type Archive = {
180-
unzipped: unzipper.CentralDirectory;
186+
zipFile: ZipFile;
187+
entries: ZipEntry[];
181188
dirMap: DirectoryHierarchyMap;
182189
};
183190

184191
async function parse_zip(zipPath: string): Promise<Archive> {
185192
if (!(await pathExists(zipPath))) {
186193
throw vscode.FileSystemError.FileNotFound(zipPath);
187194
}
195+
const zipFile = await openZip(zipPath, {
196+
lazyEntries: true,
197+
autoClose: false,
198+
strictFileNames: true,
199+
});
200+
201+
const entries = excludeDirectories(await readZipEntries(zipFile));
202+
188203
const archive: Archive = {
189-
unzipped: await unzipper.Open.file(zipPath),
204+
zipFile,
205+
entries,
190206
dirMap: new Map(),
191207
};
192-
archive.unzipped.files.forEach((f) => {
193-
ensureFile(archive.dirMap, path.resolve("/", f.path));
208+
209+
entries.forEach((f) => {
210+
ensureFile(archive.dirMap, path.resolve("/", f.fileName));
194211
});
195212
return archive;
196213
}
@@ -276,22 +293,16 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
276293
// use '/' as path separator throughout
277294
const reqPath = ref.pathWithinSourceArchive;
278295

279-
const file = archive.unzipped.files.find((f) => {
280-
const absolutePath = path.resolve("/", f.path);
296+
const file = archive.entries.find((f) => {
297+
const absolutePath = path.resolve("/", f.fileName);
281298
return (
282299
absolutePath === reqPath ||
283300
absolutePath === path.join("/src_archive", reqPath)
284301
);
285302
});
286303
if (file !== undefined) {
287-
if (file.type === "File") {
288-
return new File(reqPath, await file.buffer());
289-
} else {
290-
// file.type === 'Directory'
291-
// I haven't observed this case in practice. Could it happen
292-
// with a zip file that contains empty directories?
293-
return new Directory(reqPath);
294-
}
304+
const buffer = await openZipBuffer(archive.zipFile, file);
305+
return new File(reqPath, buffer);
295306
}
296307
if (archive.dirMap.has(reqPath)) {
297308
return new Directory(reqPath);

extensions/ql-vscode/test/vscode-tests/no-workspace/common/vscode/archive-filesystem-provider.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,10 @@ describe("archive-filesystem-provider", () => {
4747
pathWithinSourceArchive: "folder1",
4848
});
4949
const files = await archiveProvider.readDirectory(uri);
50-
expect(files).toEqual([
51-
["folder2", FileType.Directory],
52-
["textFile.txt", FileType.File],
53-
["textFile2.txt", FileType.File],
54-
]);
50+
expect(files).toHaveLength(3);
51+
expect(files).toContainEqual(["folder2", FileType.Directory]);
52+
expect(files).toContainEqual(["textFile.txt", FileType.File]);
53+
expect(files).toContainEqual(["textFile2.txt", FileType.File]);
5554
});
5655

5756
it("should handle a missing directory", async () => {

0 commit comments

Comments
 (0)