|
| 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 | +} |
0 commit comments