Skip to content

Commit e824fda

Browse files
authored
Merge pull request #3153 from github/koesie10/yauzl-unzip-tests
Add simple tests for `yauzl`-based unzip functions
2 parents ef8bf9f + f1fe4a2 commit e824fda

File tree

4 files changed

+203
-7
lines changed

4 files changed

+203
-7
lines changed

extensions/ql-vscode/src/common/files.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,23 @@ export async function readDirFullPaths(path: string): Promise<string[]> {
9191
* Symbolic links are ignored.
9292
*
9393
* @param dir the directory to walk
94+
* @param includeDirectories whether to include directories in the results
9495
*
9596
* @return An iterator of the full path to all files recursively found in the directory.
9697
*/
9798
export async function* walkDirectory(
9899
dir: string,
100+
includeDirectories = false,
99101
): AsyncIterableIterator<string> {
100102
const seenFiles = new Set<string>();
101103
for await (const d of await opendir(dir)) {
102104
const entry = join(dir, d.name);
103105
seenFiles.add(entry);
104106
if (d.isDirectory()) {
105-
yield* walkDirectory(entry);
107+
if (includeDirectories) {
108+
yield entry;
109+
}
110+
yield* walkDirectory(entry, includeDirectories);
106111
} else if (d.isFile()) {
107112
yield entry;
108113
}

extensions/ql-vscode/src/common/unzip.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,7 @@ export function readZipEntries(zipFile: ZipFile): Promise<ZipEntry[]> {
3131

3232
zipFile.readEntry();
3333
zipFile.on("entry", (entry: ZipEntry) => {
34-
if (/\/$/.test(entry.fileName)) {
35-
// Directory file names end with '/'
36-
// We don't need to do anything for directories.
37-
} else {
38-
files.push(entry);
39-
}
34+
files.push(entry);
4035

4136
zipFile.readEntry();
4237
});
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { createHash } from "crypto";
2+
import { open } from "fs/promises";
3+
import { join, relative, resolve, sep } from "path";
4+
import { chmod, pathExists, readdir } from "fs-extra";
5+
import { dir, DirectoryResult } from "tmp-promise";
6+
import {
7+
excludeDirectories,
8+
openZip,
9+
openZipBuffer,
10+
readZipEntries,
11+
unzipToDirectory,
12+
} from "../../../src/common/unzip";
13+
import { walkDirectory } from "../../../src/common/files";
14+
15+
const zipPath = resolve(__dirname, "../data/unzip/test-zip.zip");
16+
17+
describe("openZip", () => {
18+
it("can open a zip file", async () => {
19+
const zipFile = await openZip(zipPath, {
20+
lazyEntries: false,
21+
});
22+
23+
expect(zipFile.entryCount).toEqual(11);
24+
});
25+
});
26+
27+
describe("readZipEntries", () => {
28+
it("can read the entries when there are multiple directories", async () => {
29+
const zipFile = await openZip(zipPath, {
30+
lazyEntries: true,
31+
});
32+
const entries = await readZipEntries(zipFile);
33+
34+
expect(entries.map((entry) => entry.fileName).sort()).toEqual([
35+
"directory/",
36+
"directory/file.txt",
37+
"directory/file2.txt",
38+
"directory2/",
39+
"directory2/file.txt",
40+
"empty-directory/",
41+
"tools/",
42+
"tools/osx64/",
43+
"tools/osx64/java-aarch64/",
44+
"tools/osx64/java-aarch64/bin/",
45+
"tools/osx64/java-aarch64/bin/java",
46+
]);
47+
});
48+
});
49+
50+
describe("excludeDirectories", () => {
51+
it("excludes directories", async () => {
52+
const zipFile = await openZip(zipPath, {
53+
lazyEntries: true,
54+
});
55+
const entries = await readZipEntries(zipFile);
56+
const entriesWithoutDirectories = excludeDirectories(entries);
57+
58+
expect(
59+
entriesWithoutDirectories.map((entry) => entry.fileName).sort(),
60+
).toEqual([
61+
"directory/file.txt",
62+
"directory/file2.txt",
63+
"directory2/file.txt",
64+
"tools/osx64/java-aarch64/bin/java",
65+
]);
66+
});
67+
});
68+
69+
describe("openZipBuffer", () => {
70+
it("can read an entry in the zip file", async () => {
71+
const zipFile = await openZip(zipPath, {
72+
lazyEntries: true,
73+
autoClose: false,
74+
});
75+
const entries = await readZipEntries(zipFile);
76+
77+
const entry = entries.find(
78+
(entry) => entry.fileName === "directory/file.txt",
79+
);
80+
expect(entry).toBeDefined();
81+
if (!entry) {
82+
return;
83+
}
84+
85+
const buffer = await openZipBuffer(zipFile, entry);
86+
expect(buffer).toHaveLength(13);
87+
expect(buffer.toString("utf8")).toEqual("I am a file\n\n");
88+
});
89+
});
90+
91+
describe("unzipToDirectory", () => {
92+
let tmpDir: DirectoryResult;
93+
94+
beforeEach(async () => {
95+
tmpDir = await dir({
96+
unsafeCleanup: true,
97+
});
98+
});
99+
100+
afterEach(async () => {
101+
for await (const file of walkDirectory(tmpDir.path)) {
102+
await chmod(file, 0o777);
103+
}
104+
105+
await tmpDir?.cleanup();
106+
});
107+
108+
it("extracts all files", async () => {
109+
await unzipToDirectory(zipPath, tmpDir.path);
110+
111+
const allFiles = [];
112+
for await (const file of walkDirectory(tmpDir.path, true)) {
113+
allFiles.push(file);
114+
}
115+
116+
expect(
117+
allFiles
118+
.map((filePath) => relative(tmpDir.path, filePath).split(sep).join("/"))
119+
.sort(),
120+
).toEqual([
121+
"directory",
122+
"directory/file.txt",
123+
"directory/file2.txt",
124+
"directory2",
125+
"directory2/file.txt",
126+
"empty-directory",
127+
"tools",
128+
"tools/osx64",
129+
"tools/osx64/java-aarch64",
130+
"tools/osx64/java-aarch64/bin",
131+
"tools/osx64/java-aarch64/bin/java",
132+
]);
133+
134+
await expectFile(join(tmpDir.path, "directory", "file.txt"), {
135+
mode: 0o100644,
136+
contents: "I am a file\n\n",
137+
});
138+
await expectFile(join(tmpDir.path, "directory", "file2.txt"), {
139+
mode: 0o100644,
140+
contents: "I am another file\n\n",
141+
});
142+
await expectFile(join(tmpDir.path, "directory2", "file.txt"), {
143+
mode: 0o100600,
144+
contents: "I am secret\n",
145+
});
146+
await expectFile(
147+
join(tmpDir.path, "tools", "osx64", "java-aarch64", "bin", "java"),
148+
{
149+
mode: 0o100755,
150+
hash: "68b832b5c0397c5baddbbb0a76cf5407b7ea5eee8f84f9ab9488f04a52e529eb",
151+
},
152+
);
153+
154+
expect(await pathExists(join(tmpDir.path, "empty-directory"))).toBe(true);
155+
expect(await readdir(join(tmpDir.path, "empty-directory"))).toEqual([]);
156+
});
157+
});
158+
159+
async function expectFile(
160+
filePath: string,
161+
{
162+
mode: expectedMode,
163+
hash: expectedHash,
164+
contents: expectedContents,
165+
}: {
166+
mode: number;
167+
hash?: string;
168+
contents?: string;
169+
},
170+
) {
171+
const file = await open(filePath, "r");
172+
173+
// Windows doesn't really support file modes
174+
if (process.platform !== "win32") {
175+
const stats = await file.stat();
176+
expect(stats.mode).toEqual(expectedMode);
177+
}
178+
179+
const contents = await file.readFile();
180+
181+
if (expectedHash) {
182+
const hash = await computeHash(contents);
183+
expect(hash).toEqual(expectedHash);
184+
}
185+
186+
if (expectedContents) {
187+
expect(contents.toString("utf-8")).toEqual(expectedContents);
188+
}
189+
}
190+
191+
async function computeHash(contents: Buffer) {
192+
const hash = createHash("sha256");
193+
hash.update(contents);
194+
195+
return hash.digest("hex");
196+
}
1.31 KB
Binary file not shown.

0 commit comments

Comments
 (0)