Skip to content

Commit ee73639

Browse files
author
Dave Bartolomeo
committed
Expand short paths before calling codeql pack bundle
1 parent 9d2190a commit ee73639

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { platform } from "os";
2+
import { basename, dirname, join, normalize, resolve } from "path";
3+
import { lstat, readdir } from "fs/promises";
4+
import { extLogger } from "./logging/vscode";
5+
6+
async function log(message: string): Promise<void> {
7+
await extLogger.log(message);
8+
}
9+
10+
/**
11+
* Expand a single short path component
12+
* @param dir The absolute path of the directory containing the short path component.
13+
* @param shortBase The shot path component to expand.
14+
* @returns The expanded path component.
15+
*/
16+
async function expandShortPathComponent(
17+
dir: string,
18+
shortBase: string,
19+
): Promise<string> {
20+
await log(`Expanding short path component: ${shortBase}`);
21+
22+
const fullPath = join(dir, shortBase);
23+
24+
// Use `lstat` instead of `stat` to avoid following symlinks.
25+
const stats = await lstat(fullPath, { bigint: true });
26+
if (stats.dev === BigInt(0) || stats.ino === BigInt(0)) {
27+
// No inode info, so we won't be able to find this in the directory listing.
28+
await log(`No inode info available. Skipping.`);
29+
return shortBase;
30+
}
31+
await log(`dev/inode: ${stats.dev}/${stats.ino}`);
32+
33+
try {
34+
// Enumerate the children of the parent directory, and try to find one with the same dev/inode.
35+
const children = await readdir(dir);
36+
for (const child of children) {
37+
await log(`considering child: ${child}`);
38+
try {
39+
const childStats = await lstat(join(dir, child), { bigint: true });
40+
await log(`child dev/inode: ${childStats.dev}/${childStats.ino}`);
41+
if (childStats.dev === stats.dev && childStats.ino === stats.ino) {
42+
// Found a match.
43+
await log(`Found a match: ${child}`);
44+
return child;
45+
}
46+
} catch (e) {
47+
// Can't read stats for the child, so skip it.
48+
await log(`Error reading stats for child: ${e}`);
49+
}
50+
}
51+
} catch (e) {
52+
// Can't read the directory, so we won't be able to find this in the directory listing.
53+
await log(`Error reading directory: ${e}`);
54+
return shortBase;
55+
}
56+
57+
await log(`No match found. Returning original.`);
58+
return shortBase;
59+
}
60+
61+
/**
62+
* Expand the short path components in a path, including those in ancestor directories.
63+
* @param shortPath The path to expand.
64+
* @returns The expanded path.
65+
*/
66+
async function expandShortPathRecursive(shortPath: string): Promise<string> {
67+
const shortBase = basename(shortPath);
68+
if (shortBase.length === 0) {
69+
// We've reached the root.
70+
return shortPath;
71+
}
72+
73+
const dir = await expandShortPathRecursive(dirname(shortPath));
74+
await log(`dir: ${dir}`);
75+
await log(`base: ${shortBase}`);
76+
if (shortBase.indexOf("~") < 0) {
77+
// This component doesn't have a short name, so just append it to the (long) parent.
78+
await log(`Component is not a short name`);
79+
return join(dir, shortBase);
80+
}
81+
82+
// This component looks like it has a short name, so try to expand it.
83+
const longBase = await expandShortPathComponent(dir, shortBase);
84+
return join(dir, longBase);
85+
}
86+
87+
/**
88+
* Expands a path that potentially contains 8.3 short names (e.g. "C:\PROGRA~1" instead of "C:\Program Files").
89+
* @param shortPath The path to expand.
90+
* @returns A normalized, absolute path, with any short components expanded.
91+
*/
92+
export async function expandShortPaths(shortPath: string): Promise<string> {
93+
const absoluteShortPath = normalize(resolve(shortPath));
94+
if (platform() !== "win32") {
95+
// POSIX doesn't have short paths.
96+
return absoluteShortPath;
97+
}
98+
99+
await log(`Expanding short paths in: ${absoluteShortPath}`);
100+
// A quick check to see if there might be any short components.
101+
// There might be a case where a short component doesn't contain a `~`, but if there is, I haven't
102+
// found it.
103+
// This may find long components that happen to have a '~', but that's OK.
104+
if (absoluteShortPath.indexOf("~") < 0) {
105+
// No short components to expand.
106+
await log(`Skipping due to no short components`);
107+
return absoluteShortPath;
108+
}
109+
110+
return await expandShortPathRecursive(absoluteShortPath);
111+
}

extensions/ql-vscode/src/variant-analysis/run-remote-query.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type { QueryLanguage } from "../common/query-language";
3838
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
3939
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
4040
import type { QlPackFile } from "../packaging/qlpack-file";
41+
import { expandShortPaths } from "../common/short-paths";
4142

4243
/**
4344
* Well-known names for the query pack used by the server.
@@ -286,10 +287,16 @@ interface RemoteQueryTempDir {
286287
}
287288

288289
async function createRemoteQueriesTempDirectory(): Promise<RemoteQueryTempDir> {
289-
const remoteQueryDir = await dir({
290+
const shortRemoteQueryDir = await dir({
290291
dir: tmpDir.name,
291292
unsafeCleanup: true,
292293
});
294+
// Expand 8.3 filenames here to work around a CLI bug where `codeql pack bundle` produces an empty
295+
// archive if the pack path contains any 8.3 components.
296+
const remoteQueryDir = {
297+
...shortRemoteQueryDir,
298+
dir: expandShortPaths(shortRemoteQueryDir.path),
299+
};
293300
const queryPackDir = join(remoteQueryDir.path, "query-pack");
294301
await mkdirp(queryPackDir);
295302
const compiledPackDir = join(remoteQueryDir.path, "compiled-pack");

0 commit comments

Comments
 (0)