1export interface WslDistro {
2 readonly name: string;
3 readonly isDefault: boolean;
4 readonly version: 1 | 2;
5}
6
7export interface WslConfig {
8 readonly distro: string | null;
9}
10
11// Literal space — \s would also match \n/\t/\r and corrupt UNC paths like \\wsl.localhost\<distro>\...
12// Trailing char must also be \w so hand-edited config like "Ubuntu " / "Ubuntu-" / "Ubuntu." rejects.
13const DISTRO_NAME_PATTERN = /^\w(?:[\w \-.]*\w)?$/;
14
15export function parseWslDistroList(stdout: Buffer): readonly WslDistro[] {
16 const hasUtf16Bom = stdout.length >= 2 && stdout[0] === 0xff && stdout[1] === 0xfe;
17 const likelyUtf16Le =
18 hasUtf16Bom ||
19 Array.from(stdout.subarray(0, Math.min(stdout.length, 80))).filter(
20 (byte, index) => index % 2 === 1 && byte === 0,
21 ).length > 10;
22 let text = stdout.toString(likelyUtf16Le ? "utf16le" : "utf8");
23 if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
24
25 const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
26 const distros: WslDistro[] = [];
27 for (let i = 1; i < lines.length; i++) {
28 const line = lines[i]!;
29 const isDefault = line.startsWith("*");
30 const cleaned = isDefault ? line.slice(1).trim() : line.trim();
31 const fields = cleaned.split(/\s{2,}/);
32 if (fields.length < 3) continue;
33 const name = fields[0]!.trim();
34 const versionNum = parseInt(fields[2]!, 10);
35 if (!name || (versionNum !== 1 && versionNum !== 2)) continue;
36 distros.push({ name, isDefault, version: versionNum as 1 | 2 });
37 }
38 return distros;
39}
40
41// Recognizes \\wsl.localhost\<distro>\... and the legacy \\wsl$\<distro>\... so
42// `wslpath` can be invoked inside the distro that actually owns the path,
43// rather than whichever distro is configured for the desktop backend.
44export function extractDistroFromUncPath(windowsPath: string): string | null {
45 const match = /^\\\\(?:wsl\.localhost|wsl\$)\\([^\\]+)/i.exec(windowsPath);
46 if (!match) return null;
47 const candidate = match[1]!;
48 return DISTRO_NAME_PATTERN.test(candidate) ? candidate : null;
49}
50
51export function wslUncPathToLinuxPath(windowsPath: string): string | null {
52 const match = /^\\\\(?:wsl\.localhost|wsl\$)\\([^\\]+)(?:\\(.*))?$/i.exec(windowsPath.trim());
53 if (!match) return null;
54
55 const distro = match[1]!;
56 if (!DISTRO_NAME_PATTERN.test(distro)) return null;
57
58 const rest = match[2] ?? "";
59 if (rest.length === 0) return "/";
60
61 return `/${rest.split("\\").filter(Boolean).join("/")}`;
62}
63
64function resolveWslHomeUncPath(config: WslConfig, distros: readonly WslDistro[]): string | null {
65 const distroName = config.distro ?? distros.find((distro) => distro.isDefault)?.name ?? null;
66 return distroName ? `\\\\wsl.localhost\\${distroName}\\home` : null;
67}
68
69export function resolveWslPickFolderDefaultPath(
70 rawOptions: unknown,
71 config: WslConfig,
72 distros: readonly WslDistro[],
73 // Absolute Linux path of the user's home dir inside the chosen distro
74 // (e.g. "/home/josh"). When known, `~` and `~/...` expand against this so
75 // we don't open the picker at a non-existent `/home/<rest>`. When null we
76 // fall back to the `/home` parent — wrong directory but at least it exists.
77 userHome: string | null = null,
78): string | null {
79 const homePath = resolveWslHomeUncPath(config, distros);
80 if (typeof rawOptions !== "object" || rawOptions === null) {
81 return homePath;
82 }
83
84 const { initialPath } = rawOptions as { initialPath?: unknown };
85 if (typeof initialPath !== "string") {
86 return homePath;
87 }
88
89 const trimmedPath = initialPath.trim();
90 if (trimmedPath.length === 0) {
91 return homePath;
92 }
93 if (trimmedPath.startsWith("\\\\")) {
94 return trimmedPath;
95 }
96
97 const distroName = config.distro ?? distros.find((distro) => distro.isDefault)?.name ?? null;
98 if (!distroName) {
99 return homePath;
100 }
101
102 const toUncPath = (linuxPath: string) =>
103 `\\\\wsl.localhost\\${distroName}${linuxPath.replaceAll("/", "\\")}`;
104
105 const normalizedUserHome = userHome && userHome.startsWith("/") ? userHome : null;
106
107 if (trimmedPath === "~") {
108 return normalizedUserHome ? toUncPath(normalizedUserHome) : homePath;
109 }
110 if (trimmedPath.startsWith("~/")) {
111 const remainder = trimmedPath.slice(2);
112 if (normalizedUserHome) {
113 return toUncPath(`${normalizedUserHome}/${remainder}`);
114 }
115 return homePath ? `${homePath}\\${remainder.replaceAll("/", "\\")}` : null;
116 }
117 if (trimmedPath.startsWith("/")) {
118 return toUncPath(trimmedPath);
119 }
120
121 return homePath;
122}
123
124export function isValidDistroName(value: string): boolean {
125 return DISTRO_NAME_PATTERN.test(value);
126}