File size: 5,543 Bytes
f59fbe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { lstatSync, realpathSync } from "node:fs";
import { lstat, realpath } from "node:fs/promises";
import * as path from "node:path";
import * as vscode from "vscode";

import { relativeFsPath } from "./fs-path";

export interface WorkspacePath {
  readonly uri: vscode.Uri;
  readonly relativePath: string;
}

export function workDirUriFromPath(
  workspaceRootUri: vscode.Uri,
  workspaceRoot: string,
  workDir: string,
): vscode.Uri | undefined {
  const relativePath = relativeFsPath(workspaceRoot, workDir);
  if (relativePath === undefined) return undefined;
  return relativePath === ""
    ? workspaceRootUri
    : vscode.Uri.joinPath(workspaceRootUri, ...toPathSegments(relativePath));
}

export function resolveWorkspacePath(
  workDirUri: vscode.Uri,
  input: string,
  options: { allowRoot?: boolean } = {},
): WorkspacePath | undefined {
  const normalized = input.replaceAll("\\", "/");
  if (
    path.posix.isAbsolute(normalized) ||
    path.win32.isAbsolute(input) ||
    /^[A-Za-z]:/.test(input)
  ) return undefined;

  const segments = normalized.split("/").filter((segment) => segment !== "" && segment !== ".");
  if (segments.includes("..")) return undefined;
  if (segments.length === 0) {
    return options.allowRoot === true ? { uri: workDirUri, relativePath: "" } : undefined;
  }

  return {
    uri: vscode.Uri.joinPath(workDirUri, ...segments),
    relativePath: segments.join("/"),
  };
}

export function relativeWorkspacePath(rootUri: vscode.Uri, candidateUri: vscode.Uri): string | undefined {
  if (rootUri.scheme !== candidateUri.scheme) return undefined;

  // VS Code exposes native Windows file paths through `fsPath`, while URI
  // paths always use `/`. Comparing the URI strings makes the same drive or
  // UNC path look different when only separator or drive/share casing differs.
  // Native filesystem semantics are the right contract for `file:` URIs.
  if (rootUri.scheme === "file") {
    const relativePath = relativeFsPath(rootUri.fsPath, candidateUri.fsPath);
    return relativePath === "" ? undefined : relativePath;
  }

  if (rootUri.authority !== candidateUri.authority) return undefined;

  const relativePath = path.posix.relative(normalizeUriPath(rootUri.path), normalizeUriPath(candidateUri.path));
  if (
    relativePath === "" ||
    relativePath === ".." ||
    relativePath.startsWith("../") ||
    path.posix.isAbsolute(relativePath)
  ) {
    return undefined;
  }
  return relativePath;
}

export async function isWorkspacePathContained(
  rootUri: vscode.Uri,
  candidateUri: vscode.Uri,
  options: { allowMissing?: boolean } = {},
): Promise<boolean> {
  if (rootUri.toString() !== candidateUri.toString() && relativeWorkspacePath(rootUri, candidateUri) === undefined) {
    return false;
  }
  if (rootUri.scheme !== "file" || candidateUri.scheme !== "file") return true;

  try {
    const [realRoot, realCandidate] = await Promise.all([
      realpath(rootUri.fsPath),
      options.allowMissing === true ? realExistingPath(candidateUri.fsPath) : realpath(candidateUri.fsPath),
    ]);
    return relativeFsPath(realRoot, realCandidate) !== undefined;
  } catch {
    return false;
  }
}

export function isWorkspacePathContainedSync(
  rootUri: vscode.Uri,
  candidateUri: vscode.Uri,
  options: { allowMissing?: boolean } = {},
): boolean {
  if (rootUri.toString() !== candidateUri.toString() && relativeWorkspacePath(rootUri, candidateUri) === undefined) {
    return false;
  }
  if (rootUri.scheme !== "file" || candidateUri.scheme !== "file") return true;

  try {
    const realRoot = realpathSync(rootUri.fsPath);
    const realCandidate =
      options.allowMissing === true
        ? realExistingPathSync(candidateUri.fsPath)
        : realpathSync(candidateUri.fsPath);
    return relativeFsPath(realRoot, realCandidate) !== undefined;
  } catch {
    return false;
  }
}

async function realExistingPath(candidate: string): Promise<string> {
  let current = candidate;
  while (true) {
    try {
      return await realpath(current);
    } catch (error) {
      if (!isMissingPathError(error)) throw error;
      let isDanglingSymlink = false;
      try {
        isDanglingSymlink = (await lstat(current)).isSymbolicLink();
      } catch (lstatError) {
        if (!isMissingPathError(lstatError)) throw lstatError;
      }
      if (isDanglingSymlink) throw error;
      const parent = path.dirname(current);
      if (parent === current) throw error;
      current = parent;
    }
  }
}

function realExistingPathSync(candidate: string): string {
  let current = candidate;
  while (true) {
    try {
      return realpathSync(current);
    } catch (error) {
      if (!isMissingPathError(error)) throw error;
      let isDanglingSymlink = false;
      try {
        isDanglingSymlink = lstatSync(current).isSymbolicLink();
      } catch (lstatError) {
        if (!isMissingPathError(lstatError)) throw lstatError;
      }
      if (isDanglingSymlink) throw error;
      const parent = path.dirname(current);
      if (parent === current) throw error;
      current = parent;
    }
  }
}

function isMissingPathError(error: unknown): boolean {
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
}

function toPathSegments(value: string): string[] {
  return value.replaceAll("\\", "/").split("/").filter(Boolean);
}

function normalizeUriPath(value: string): string {
  const normalized = path.posix.normalize(value);
  return normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
}