| 1 | export interface WslDistro { |
| 2 | readonly name: string; |
| 3 | readonly isDefault: boolean; |
| 4 | readonly version: 1 | 2; |
| 5 | } |
| 6 | |
| 7 | export 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. |
| 13 | const DISTRO_NAME_PATTERN = /^\w(?:[\w \-.]*\w)?$/; |
| 14 | |
| 15 | export 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. |
| 44 | export 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 | |
| 51 | export 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 | |
| 64 | function 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 | |
| 69 | export 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 | |
| 124 | export function isValidDistroName(value: string): boolean { |
| 125 | return DISTRO_NAME_PATTERN.test(value); |
| 126 | } |