| 1 | //go:build windows |
| 2 | |
| 3 | package desktoplauncher |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | |
| 11 | "golang.org/x/sys/windows" |
| 12 | ) |
| 13 | |
| 14 | const maxFinalPathUTF16 = 1 << 16 |
| 15 | |
| 16 | type launchLocation string |
| 17 | |
| 18 | const ( |
| 19 | launchLocationLocal launchLocation = "local" |
| 20 | launchLocationUNC launchLocation = "unc" |
| 21 | launchLocationRemoteDrive launchLocation = "remote_drive" |
| 22 | ) |
| 23 | |
| 24 | // resolveExecutablePath opens the launcher and asks Windows for the final DOS |
| 25 | // path represented by that handle. Unlike filepath.EvalSymlinks, this resolves |
| 26 | // directory junctions such as Scoop's stable current entry. |
| 27 | func resolveExecutablePath(path string) (string, error) { |
| 28 | file, err := os.Open(path) |
| 29 | if err != nil { |
| 30 | return "", fmt.Errorf("open executable: %w", err) |
| 31 | } |
| 32 | defer file.Close() |
| 33 | |
| 34 | handle := windows.Handle(file.Fd()) |
| 35 | size := uint32(256) |
| 36 | for { |
| 37 | buf := make([]uint16, size) |
| 38 | n, err := windows.GetFinalPathNameByHandle(handle, &buf[0], size, 0) |
| 39 | if err != nil { |
| 40 | return "", fmt.Errorf("get final executable path: %w", err) |
| 41 | } |
| 42 | if n < size { |
| 43 | return normalizeFinalWindowsPath(windows.UTF16ToString(buf[:n])), nil |
| 44 | } |
| 45 | if n >= maxFinalPathUTF16 { |
| 46 | return "", fmt.Errorf("get final executable path: required buffer is too large: %d", n) |
| 47 | } |
| 48 | size = n + 1 |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | func normalizeFinalWindowsPath(path string) string { |
| 53 | const ( |
| 54 | extendedPrefix = `\\?\` |
| 55 | extendedUNC = `\\?\UNC\` |
| 56 | ) |
| 57 | if len(path) >= len(extendedUNC) && strings.EqualFold(path[:len(extendedUNC)], extendedUNC) { |
| 58 | return `\\` + path[len(extendedUNC):] |
| 59 | } |
| 60 | if len(path) >= 7 && strings.EqualFold(path[:len(extendedPrefix)], extendedPrefix) && |
| 61 | isASCIILetter(path[4]) && path[5] == ':' && path[6] == '\\' { |
| 62 | return path[len(extendedPrefix):] |
| 63 | } |
| 64 | return path |
| 65 | } |
| 66 | |
| 67 | func isASCIILetter(ch byte) bool { |
| 68 | return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' |
| 69 | } |
| 70 | |
| 71 | func classifyLaunchLocation(path string) (launchLocation, error) { |
| 72 | return classifyLaunchLocationWith(path, windows.GetDriveType) |
| 73 | } |
| 74 | |
| 75 | func classifyLaunchLocationWith(path string, getDriveType func(*uint16) uint32) (launchLocation, error) { |
| 76 | volume := filepath.VolumeName(filepath.Clean(path)) |
| 77 | if volume == "" { |
| 78 | return "", fmt.Errorf("determine executable volume: path has no volume") |
| 79 | } |
| 80 | if strings.HasPrefix(volume, `\\`) { |
| 81 | return launchLocationUNC, nil |
| 82 | } |
| 83 | root := volume |
| 84 | if !strings.HasSuffix(root, `\`) { |
| 85 | root += `\` |
| 86 | } |
| 87 | rootPtr, err := windows.UTF16PtrFromString(root) |
| 88 | if err != nil { |
| 89 | return "", fmt.Errorf("determine executable volume: %w", err) |
| 90 | } |
| 91 | switch driveType := getDriveType(rootPtr); driveType { |
| 92 | case windows.DRIVE_REMOTE: |
| 93 | return launchLocationRemoteDrive, nil |
| 94 | case windows.DRIVE_UNKNOWN, windows.DRIVE_NO_ROOT_DIR: |
| 95 | return "", fmt.Errorf("determine executable volume: Windows returned drive type %d", driveType) |
| 96 | default: |
| 97 | return launchLocationLocal, nil |
| 98 | } |
| 99 | } |
| 100 |