-
Notifications
You must be signed in to change notification settings - Fork 228
Expand file tree
/
Copy pathshort-paths.ts
More file actions
178 lines (154 loc) · 5.8 KB
/
short-paths.ts
File metadata and controls
178 lines (154 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import { arch, platform } from "os";
import { basename, dirname, join, normalize, resolve } from "path";
import { lstat, readdir } from "fs/promises";
import type { BaseLogger } from "./logging";
import type { KoffiFunction } from "koffi";
import { getErrorMessage } from "./helpers-pure";
/**
* Expands a path that potentially contains 8.3 short names (e.g. "C:\PROGRA~1" instead of "C:\Program Files").
*
* See https://en.wikipedia.org/wiki/8.3_filename if you're not familiar with Windows 8.3 short names.
*
* @param shortPath The path to expand.
* @returns A normalized, absolute path, with any short components expanded.
*/
export async function expandShortPaths(
shortPath: string,
logger: BaseLogger,
): Promise<string> {
const absoluteShortPath = normalize(resolve(shortPath));
if (platform() !== "win32") {
// POSIX doesn't have short paths.
return absoluteShortPath;
}
void logger.log(`Expanding short paths in: ${absoluteShortPath}`);
// A quick check to see if there might be any short components.
// There might be a case where a short component doesn't contain a `~`, but if there is, I haven't
// found it.
// This may find long components that happen to have a '~', but that's OK.
if (absoluteShortPath.indexOf("~") < 0) {
// No short components to expand.
void logger.log(`Skipping due to no short components`);
return absoluteShortPath;
}
const longPath = await expandShortPathRecursive(absoluteShortPath, logger);
if (longPath.indexOf("~") < 0) {
return longPath;
}
void logger.log(
"Short path was not resolved to long path, using native method",
);
try {
return await expandShortPathNative(absoluteShortPath, logger);
} catch (e) {
void logger.log(
`Failed to expand short path using native method: ${getErrorMessage(e)}`,
);
return longPath;
}
}
/**
* Expand a single short path component
* @param dir The absolute path of the directory containing the short path component.
* @param shortBase The shot path component to expand.
* @returns The expanded path component.
*/
async function expandShortPathComponent(
dir: string,
shortBase: string,
logger: BaseLogger,
): Promise<string> {
void logger.log(`Expanding short path component: ${shortBase}`);
const fullPath = join(dir, shortBase);
// Use `lstat` instead of `stat` to avoid following symlinks.
const stats = await lstat(fullPath, { bigint: true });
if (stats.dev === BigInt(0) || stats.ino === BigInt(0)) {
// No inode info, so we won't be able to find this in the directory listing.
void logger.log(`No inode info available. Skipping.`);
return shortBase;
}
void logger.log(`dev/inode: ${stats.dev}/${stats.ino}`);
try {
// Enumerate the children of the parent directory, and try to find one with the same dev/inode.
const children = await readdir(dir);
for (const child of children) {
void logger.log(`considering child: ${child}`);
try {
const childStats = await lstat(join(dir, child), { bigint: true });
void logger.log(`child dev/inode: ${childStats.dev}/${childStats.ino}`);
if (childStats.dev === stats.dev && childStats.ino === stats.ino) {
// Found a match.
void logger.log(`Found a match: ${child}`);
return child;
}
} catch (e) {
// Can't read stats for the child, so skip it.
void logger.log(`Error reading stats for child: ${e}`);
}
}
} catch (e) {
// Can't read the directory, so we won't be able to find this in the directory listing.
void logger.log(`Error reading directory: ${e}`);
return shortBase;
}
void logger.log(`No match found. Returning original.`);
return shortBase;
}
/**
* Expand the short path components in a path, including those in ancestor directories.
* @param shortPath The path to expand.
* @returns The expanded path.
*/
async function expandShortPathRecursive(
shortPath: string,
logger: BaseLogger,
): Promise<string> {
const shortBase = basename(shortPath);
if (shortBase.length === 0) {
// We've reached the root.
return shortPath;
}
const dir = await expandShortPathRecursive(dirname(shortPath), logger);
void logger.log(`dir: ${dir}`);
void logger.log(`base: ${shortBase}`);
if (shortBase.indexOf("~") < 0) {
// This component doesn't have a short name, so just append it to the (long) parent.
void logger.log(`Component is not a short name`);
return join(dir, shortBase);
}
// This component looks like it has a short name, so try to expand it.
const longBase = await expandShortPathComponent(dir, shortBase, logger);
return join(dir, longBase);
}
let GetLongPathNameW: KoffiFunction | undefined;
async function expandShortPathNative(shortPath: string, logger: BaseLogger) {
if (platform() !== "win32") {
throw new Error("expandShortPathNative is only supported on Windows");
}
if (arch() !== "x64") {
throw new Error(
"expandShortPathNative is only supported on x64 architecture",
);
}
if (GetLongPathNameW === undefined) {
// We are using koffi/indirect here to avoid including the native addon for all
// platforms in the bundle since this is only used on Windows. Instead, the
// native addon is included in the Gulpfile.
const koffi = await import("koffi/indirect");
const lib = koffi.load("kernel32.dll");
GetLongPathNameW = lib.func("__stdcall", "GetLongPathNameW", "uint32", [
"str16",
"str16",
"uint32",
]);
}
const MAX_PATH = 32767;
const buffer = Buffer.alloc(MAX_PATH * 2, 0);
const result = GetLongPathNameW(shortPath, buffer, MAX_PATH);
if (result === 0) {
throw new Error("Failed to get long path name");
}
const longPath = buffer.toString("utf16le", 0, (result - 1) * 2);
void logger.log(`Expanded short path ${shortPath} to ${longPath}`);
return longPath;
}