forked from Zoo-Code-Org/Zoo-Code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWorkspacePathResolver.ts
More file actions
66 lines (60 loc) · 3.03 KB
/
Copy pathWorkspacePathResolver.ts
File metadata and controls
66 lines (60 loc) · 3.03 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
import * as path from "path"
import * as fs from "fs/promises"
/** Narrow an unknown error to a Node errno exception with the given `code`. */
function isErrnoException(err: unknown, code: string): boolean {
return err instanceof Error && (err as NodeJS.ErrnoException).code === code
}
// macOS APFS/HFS+ and Windows are case-insensitive: `realpath` can return a different case
// than the one VS Code registered, so we lowercase the result before returning to keep later
// comparisons (e.g. against `uri.fsPath`) reliable on those platforms only. Platform is read at
// call time (not cached) so the behavior stays correct and testable.
function normalizeCase(p: string): string {
const caseInsensitive = process.platform === "darwin" || process.platform === "win32"
return caseInsensitive ? p.toLowerCase() : p
}
/**
* Resolve a filesystem path to its canonical, symlink-followed form.
*
* This is the canonicalization primitive for the workspace boundary check (issue #169). It owns
* **only** path resolution — no workspace policy, no settings, no tool logic. The authorization
* decision (and the `allowSymlinksOutsideWorkspace` opt-in) lives in `WorkspaceFileAccess`.
*
* Behavior:
* - **Async only** (`fs.promises.realpath`); never blocks the extension host event loop.
* - If `target` does not exist yet (e.g. a file about to be created), the realpath of the nearest
* existing ancestor is resolved and the remaining segments are re-appended, so a symlink
* anywhere along the path is still followed while not-yet-created paths can still be evaluated.
* - Only `ENOENT` triggers the walk-up. Any other error (e.g. `EACCES`, `ELOOP`) is **re-thrown**
* so a caller performing a security check can fail closed. Silently walking up would mask the
* symlink and could make an out-of-workspace target look "inside" (#169).
* - The result is case-normalized on case-insensitive filesystems (macOS, Windows).
*
* Workspace folder paths should be resolved through this same function by callers, since a folder
* may itself be reached via a symlink.
*/
export async function resolveRealPath(target: string): Promise<string> {
let current = path.resolve(target)
const trailing: string[] = []
// Walk up until an existing path can be resolved, bounded by the filesystem root.
while (true) {
try {
const resolved = await fs.realpath(current)
const joined = trailing.length > 0 ? path.join(resolved, ...trailing.reverse()) : resolved
return normalizeCase(joined)
} catch (err) {
if (!isErrnoException(err, "ENOENT")) {
// Non-ENOENT (e.g. EACCES, ELOOP): propagate so the caller's security check can
// fail closed instead of falling through to the lexical path.
throw err
}
const parent = path.dirname(current)
if (parent === current) {
// Reached the filesystem root without finding an existing path; fall back to the
// lexically resolved path (still case-normalized for consistent comparisons).
return normalizeCase(path.resolve(target))
}
trailing.push(path.basename(current))
current = parent
}
}
}