Skip to content

Commit 3f6c105

Browse files
committed
Merge remote-tracking branch 'origin/main' into koesie10/variant-analysis-yauzl
2 parents 7708347 + b833591 commit 3f6c105

File tree

10 files changed

+245
-65
lines changed

10 files changed

+245
-65
lines changed

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { WriteStream } from "fs";
55
import { createWriteStream, ensureDir } from "fs-extra";
66

77
// We can't use promisify because it picks up the wrong overload.
8-
function openZip(path: string, options: ZipOptions = {}): Promise<ZipFile> {
8+
export function openZip(
9+
path: string,
10+
options: ZipOptions = {},
11+
): Promise<ZipFile> {
912
return new Promise((resolve, reject) => {
1013
open(path, options, (err, zipFile) => {
1114
if (err) {
@@ -18,7 +21,11 @@ function openZip(path: string, options: ZipOptions = {}): Promise<ZipFile> {
1821
});
1922
}
2023

21-
function readZipEntries(zipFile: ZipFile): Promise<ZipEntry[]> {
24+
export function excludeDirectories(entries: ZipEntry[]): ZipEntry[] {
25+
return entries.filter((entry) => !/\/$/.test(entry.fileName));
26+
}
27+
28+
export function readZipEntries(zipFile: ZipFile): Promise<ZipEntry[]> {
2229
return new Promise((resolve, reject) => {
2330
const files: ZipEntry[] = [];
2431

@@ -60,6 +67,25 @@ function openZipReadStream(
6067
});
6168
}
6269

70+
export async function openZipBuffer(
71+
zipFile: ZipFile,
72+
entry: ZipEntry,
73+
): Promise<Buffer> {
74+
const readable = await openZipReadStream(zipFile, entry);
75+
return new Promise((resolve, reject) => {
76+
const chunks: Buffer[] = [];
77+
readable.on("data", (chunk) => {
78+
chunks.push(chunk);
79+
});
80+
readable.on("error", (err) => {
81+
reject(err);
82+
});
83+
readable.on("end", () => {
84+
resolve(Buffer.concat(chunks));
85+
});
86+
});
87+
}
88+
6389
async function copyStream(
6490
readable: Readable,
6591
writeStream: WriteStream,

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/src/databases/database-fetcher.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import fetch, { Response } from "node-fetch";
22
import { zip } from "zip-a-folder";
3-
import { Open } from "unzipper";
43
import { Uri, window, InputBoxOptions } from "vscode";
54
import { CodeQLCliServer } from "../codeql-cli/cli";
65
import {
@@ -46,7 +45,7 @@ export async function promptImportInternetDatabase(
4645
databaseManager: DatabaseManager,
4746
storagePath: string,
4847
progress: ProgressCallback,
49-
cli?: CodeQLCliServer,
48+
cli: CodeQLCliServer,
5049
): Promise<DatabaseItem | undefined> {
5150
const databaseUrl = await window.showInputBox({
5251
prompt: "Enter URL of zipfile of database to download",
@@ -101,7 +100,7 @@ export async function promptImportGithubDatabase(
101100
storagePath: string,
102101
credentials: Credentials | undefined,
103102
progress: ProgressCallback,
104-
cli?: CodeQLCliServer,
103+
cli: CodeQLCliServer,
105104
language?: string,
106105
makeSelected = true,
107106
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
@@ -180,7 +179,7 @@ export async function downloadGitHubDatabase(
180179
storagePath: string,
181180
credentials: Credentials | undefined,
182181
progress: ProgressCallback,
183-
cli?: CodeQLCliServer,
182+
cli: CodeQLCliServer,
184183
language?: string,
185184
makeSelected = true,
186185
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
@@ -235,7 +234,7 @@ export async function downloadGitHubDatabaseFromUrl(
235234
progress: ProgressCallback,
236235
databaseManager: DatabaseManager,
237236
storagePath: string,
238-
cli?: CodeQLCliServer,
237+
cli: CodeQLCliServer,
239238
makeSelected = true,
240239
addSourceArchiveFolder = true,
241240
): Promise<DatabaseItem | undefined> {
@@ -279,14 +278,15 @@ export async function downloadGitHubDatabaseFromUrl(
279278
* @param databaseUrl the file url of the archive to import
280279
* @param databaseManager the DatabaseManager
281280
* @param storagePath where to store the unzipped database.
281+
* @param cli the CodeQL CLI server
282282
*/
283283
export async function importArchiveDatabase(
284284
commandManager: AppCommandManager,
285285
databaseUrl: string,
286286
databaseManager: DatabaseManager,
287287
storagePath: string,
288288
progress: ProgressCallback,
289-
cli?: CodeQLCliServer,
289+
cli: CodeQLCliServer,
290290
): Promise<DatabaseItem | undefined> {
291291
try {
292292
const item = await databaseArchiveFetcher(
@@ -333,6 +333,7 @@ export async function importArchiveDatabase(
333333
* @param nameOverride a name for the database that overrides the default
334334
* @param origin the origin of the database
335335
* @param progress callback to send progress messages to
336+
* @param cli the CodeQL CLI server
336337
* @param makeSelected make the new database selected in the databases panel (default: true)
337338
* @param addSourceArchiveFolder whether to add a workspace folder containing the source archive to the workspace
338339
*/
@@ -344,7 +345,7 @@ async function databaseArchiveFetcher(
344345
nameOverride: string | undefined,
345346
origin: DatabaseOrigin,
346347
progress: ProgressCallback,
347-
cli?: CodeQLCliServer,
348+
cli: CodeQLCliServer,
348349
makeSelected = true,
349350
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
350351
): Promise<DatabaseItem> {
@@ -443,34 +444,24 @@ function validateUrl(databaseUrl: string) {
443444
async function readAndUnzip(
444445
zipUrl: string,
445446
unzipPath: string,
446-
cli?: CodeQLCliServer,
447+
cli: CodeQLCliServer,
447448
progress?: ProgressCallback,
448449
) {
449-
// TODO: Providing progress as the file is unzipped is currently blocked
450-
// on https://github.com/ZJONSSON/node-unzipper/issues/222
451450
const zipFile = Uri.parse(zipUrl).fsPath;
452451
progress?.({
453452
maxStep: 10,
454453
step: 9,
455454
message: `Unzipping into ${basename(unzipPath)}`,
456455
});
457-
if (cli) {
458-
// Use the `database unbundle` command if the installed cli version supports it
459-
await cli.databaseUnbundle(zipFile, unzipPath);
460-
} else {
461-
// Must get the zip central directory since streaming the
462-
// zip contents may not have correct local file headers.
463-
// Instead, we can only rely on the central directory.
464-
const directory = await Open.file(zipFile);
465-
await directory.extract({ path: unzipPath });
466-
}
456+
457+
await cli.databaseUnbundle(zipFile, unzipPath);
467458
}
468459

469460
async function fetchAndUnzip(
470461
databaseUrl: string,
471462
requestHeaders: { [key: string]: string },
472463
unzipPath: string,
473-
cli?: CodeQLCliServer,
464+
cli: CodeQLCliServer,
474465
progress?: ProgressCallback,
475466
) {
476467
// Although it is possible to download and stream directly to an unzipped directory,

extensions/ql-vscode/src/databases/local-databases-ui.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export class DatabaseUI extends DisposableObject {
233233
private app: App,
234234
private databaseManager: DatabaseManager,
235235
languageContext: LanguageContextStore,
236-
private readonly queryServer: QueryRunner | undefined,
236+
private readonly queryServer: QueryRunner,
237237
private readonly storagePath: string,
238238
readonly extensionPath: string,
239239
) {
@@ -402,10 +402,7 @@ export class DatabaseUI extends DisposableObject {
402402
workspace.workspaceFolders[0].uri.fsPath,
403403
"tutorial-queries",
404404
);
405-
const cli = this.queryServer?.cliServer;
406-
if (!cli) {
407-
throw new Error("No CLI server found");
408-
}
405+
const cli = this.queryServer.cliServer;
409406
await cli.packInstall(tutorialQueriesPath);
410407
}
411408
}
@@ -528,7 +525,7 @@ export class DatabaseUI extends DisposableObject {
528525
this.databaseManager,
529526
this.storagePath,
530527
progress,
531-
this.queryServer?.cliServer,
528+
this.queryServer.cliServer,
532529
);
533530
},
534531
{
@@ -548,7 +545,7 @@ export class DatabaseUI extends DisposableObject {
548545
this.storagePath,
549546
credentials,
550547
progress,
551-
this.queryServer?.cliServer,
548+
this.queryServer.cliServer,
552549
);
553550
},
554551
{
@@ -704,7 +701,7 @@ export class DatabaseUI extends DisposableObject {
704701
this.databaseManager,
705702
this.storagePath,
706703
progress,
707-
this.queryServer?.cliServer,
704+
this.queryServer.cliServer,
708705
);
709706
} else {
710707
await this.databaseManager.openDatabase(uri, {
@@ -836,7 +833,7 @@ export class DatabaseUI extends DisposableObject {
836833
this.databaseManager,
837834
this.storagePath,
838835
progress,
839-
this.queryServer?.cliServer,
836+
this.queryServer.cliServer,
840837
);
841838
}
842839
},

extensions/ql-vscode/src/model-editor/yaml.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,25 +181,40 @@ function createDataExtensionYamlsByGrouping(
181181
>,
182182
createFilename: (method: Method) => string,
183183
): Record<string, string> {
184-
const methodsByFilename: Record<string, Record<string, ModeledMethod[]>> = {};
184+
const actualFilenameByCanonicalFilename: Record<string, string> = {};
185+
186+
const methodsByCanonicalFilename: Record<
187+
string,
188+
Record<string, ModeledMethod[]>
189+
> = {};
185190

186191
// We only want to generate a yaml file when it's a known external API usage
187192
// and there are new modeled methods for it. This avoids us overwriting other
188193
// files that may contain data we don't know about.
189194
for (const method of methods) {
190195
if (method.signature in newModeledMethods) {
191-
methodsByFilename[createFilename(method)] = {};
196+
const filename = createFilename(method);
197+
const canonicalFilename = canonicalizeFilename(filename);
198+
199+
methodsByCanonicalFilename[canonicalFilename] = {};
200+
actualFilenameByCanonicalFilename[canonicalFilename] = filename;
192201
}
193202
}
194203

195204
// First populate methodsByFilename with any existing modeled methods.
196205
for (const [filename, methodsBySignature] of Object.entries(
197206
existingModeledMethods,
198207
)) {
199-
if (filename in methodsByFilename) {
208+
const canonicalFilename = canonicalizeFilename(filename);
209+
210+
if (canonicalFilename in methodsByCanonicalFilename) {
200211
for (const [signature, methods] of Object.entries(methodsBySignature)) {
201-
methodsByFilename[filename][signature] = [...methods];
212+
methodsByCanonicalFilename[canonicalFilename][signature] = [...methods];
202213
}
214+
215+
// Ensure that if a file exists on disk, we use the same capitalization
216+
// as the original file.
217+
actualFilenameByCanonicalFilename[canonicalFilename] = filename;
203218
}
204219
}
205220

@@ -209,19 +224,25 @@ function createDataExtensionYamlsByGrouping(
209224
const newMethods = newModeledMethods[method.signature];
210225
if (newMethods) {
211226
const filename = createFilename(method);
227+
const canonicalFilename = canonicalizeFilename(filename);
212228

213229
// Override any existing modeled methods with the new ones.
214-
methodsByFilename[filename][method.signature] = [...newMethods];
230+
methodsByCanonicalFilename[canonicalFilename][method.signature] = [
231+
...newMethods,
232+
];
215233
}
216234
}
217235

218236
const result: Record<string, string> = {};
219237

220-
for (const [filename, methods] of Object.entries(methodsByFilename)) {
221-
result[filename] = createDataExtensionYaml(
222-
language,
223-
Object.values(methods).flatMap((methods) => methods),
224-
);
238+
for (const [canonicalFilename, methods] of Object.entries(
239+
methodsByCanonicalFilename,
240+
)) {
241+
result[actualFilenameByCanonicalFilename[canonicalFilename]] =
242+
createDataExtensionYaml(
243+
language,
244+
Object.values(methods).flatMap((methods) => methods),
245+
);
225246
}
226247

227248
return result;
@@ -299,6 +320,13 @@ export function createFilenameForPackage(
299320
return `${prefix}${packageName}${suffix}.yml`;
300321
}
301322

323+
function canonicalizeFilename(filename: string) {
324+
// We want to canonicalize filenames so that they are always in the same format
325+
// for comparison purposes. This is important because we want to avoid overwriting
326+
// data extension YAML files on case-insensitive file systems.
327+
return filename.toLowerCase();
328+
}
329+
302330
function validateModelExtensionFile(data: unknown): data is ModelExtensionFile {
303331
modelExtensionFileSchemaValidate(data);
304332

0 commit comments

Comments
 (0)