From f6a8c8dc87089360002c87e637164b04ab088773 Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Tue, 13 Feb 2024 09:55:49 +0100 Subject: [PATCH] feat(windows): parse app exec links --- src/platform/shell.go | 5 ++-- src/platform/shell_unix.go | 6 ++--- src/platform/shell_windows.go | 16 ++++++++++++ src/platform/win32_windows.go | 46 +++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/platform/shell.go b/src/platform/shell.go index 05f5ebfd..f70a801a 100644 --- a/src/platform/shell.go +++ b/src/platform/shell.go @@ -12,7 +12,6 @@ import ( "net/http" "net/http/httputil" "os" - "os/exec" "path/filepath" "runtime" "strconv" @@ -609,12 +608,14 @@ func (env *Shell) CommandPath(command string) string { env.Debug(path) return path } - path, err := exec.LookPath(command) + + path, err := env.LookPath(command) if err == nil { env.cmdCache.set(command, path) env.Debug(path) return path } + env.Error(err) return "" } diff --git a/src/platform/shell_unix.go b/src/platform/shell_unix.go index 156f1453..36105aa5 100644 --- a/src/platform/shell_unix.go +++ b/src/platform/shell_unix.go @@ -3,8 +3,8 @@ package platform import ( - "errors" "os" + "os/exec" "strconv" "strings" "time" @@ -156,8 +156,8 @@ func (env *Shell) ConvertToLinuxPath(path string) string { return path } -func (env *Shell) LookWinAppPath(_ string) (string, error) { - return "", errors.New("not relevant") +func (env *Shell) LookPath(command string) (string, error) { + return exec.LookPath(command) } func (env *Shell) DirIsWritable(path string) bool { diff --git a/src/platform/shell_windows.go b/src/platform/shell_windows.go index ae8d699c..9a94608b 100644 --- a/src/platform/shell_windows.go +++ b/src/platform/shell_windows.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "os" + "os/exec" + "path/filepath" "strings" "syscall" "time" @@ -249,3 +251,17 @@ func (env *Shell) Connection(connectionType ConnectionType) (*Connection, error) env.Error(fmt.Errorf("Network type '%s' not found", connectionType)) return nil, &NotImplemented{} } + +func (env *Shell) LookPath(command string) (string, error) { + winAppPath := filepath.Join(env.Getenv("LOCALAPPDATA"), `\Microsoft\WindowsApps\`, command) + if !strings.HasSuffix(winAppPath, ".exe") { + winAppPath += ".exe" + } + + path, err := exec.LookPath(command) + if err == nil && path != winAppPath { + return path, nil + } + + return readWinAppLink(winAppPath) +} diff --git a/src/platform/win32_windows.go b/src/platform/win32_windows.go index 8705e8f9..1986d1be 100644 --- a/src/platform/win32_windows.go +++ b/src/platform/win32_windows.go @@ -350,3 +350,49 @@ func (env *Shell) Memory() (*Memory, error) { PhysicalPercentUsed: float64(memStat.MemoryLoad), }, nil } + +// openSymlink calls CreateFile Windows API with FILE_FLAG_OPEN_REPARSE_POINT +// parameter, so that Windows does not follow symlink, if path is a symlink. +// openSymlink returns opened file handle. +func openSymlink(path string) (syscall.Handle, error) { + p, err := syscall.UTF16PtrFromString(path) + if err != nil { + return 0, err + } + + attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) + // Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink. + // See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted + attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT + h, err := syscall.CreateFile(p, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0) + if err != nil { + return 0, err + } + + return h, nil +} + +func readWinAppLink(path string) (string, error) { + h, err := openSymlink(path) + if err != nil { + return "", err + } + + defer syscall.CloseHandle(h) //nolint: errcheck + + rdbbuf := make([]byte, syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE) + var bytesReturned uint32 + err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil) + if err != nil { + return "", err + } + + rdb := (*REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0])) + rb := (*GenericDataBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)) + appExecLink := (*AppExecLinkReparseBuffer)(unsafe.Pointer(&rb.DataBuffer)) + if appExecLink.Version != 3 { + return "", errors.New("unknown AppExecLink version") + } + + return appExecLink.Path() +}