From 1e797acf77a917f6cc2aed84e38648cd81ae3b43 Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Mon, 7 Feb 2022 18:50:52 +0100 Subject: [PATCH] feat(windows): support store app CLI tools resolves #1703 --- src/environment/shell.go | 6 ++- src/environment/unix.go | 4 ++ src/environment/windows.go | 13 +++++ src/environment/windows_win32.go | 82 ++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/environment/shell.go b/src/environment/shell.go index 7452f386..40ed1db7 100644 --- a/src/environment/shell.go +++ b/src/environment/shell.go @@ -397,7 +397,7 @@ func (env *ShellEnvironment) RunCommand(command string, args ...string) (string, env.log(Error, "RunCommand", errorStr) return output, cmdErr } - output := strings.TrimSuffix(out.String(), "\n") + output := strings.TrimSpace(out.String()) env.log(Debug, "RunCommand", output) return output, nil } @@ -418,6 +418,10 @@ func (env *ShellEnvironment) HasCommand(command string) bool { env.cmdCache.set(command, path) return true } + if path, err := env.LookWinAppPath(command); err == nil { + env.cmdCache.set(command, path) + return true + } env.log(Error, "HasCommand", err.Error()) return false } diff --git a/src/environment/unix.go b/src/environment/unix.go index 39ec54d1..eeb381f6 100644 --- a/src/environment/unix.go +++ b/src/environment/unix.go @@ -119,3 +119,7 @@ func (env *ShellEnvironment) ConvertToLinuxPath(path string) string { func (env *ShellEnvironment) WifiNetwork() (*WifiInfo, error) { return nil, errors.New("not implemented") } + +func (env *ShellEnvironment) LookWinAppPath(file string) (string, error) { + return "", errors.New("not relevant") +} diff --git a/src/environment/windows.go b/src/environment/windows.go index b71da0dd..38613c80 100644 --- a/src/environment/windows.go +++ b/src/environment/windows.go @@ -116,6 +116,19 @@ func (env *ShellEnvironment) CachePath() string { return env.Home() } +func (env *ShellEnvironment) LookWinAppPath(file string) (string, error) { + winAppPath := env.Home() + `\AppData\Local\Microsoft\WindowsApps\` + command := file + ".exe" + isWinStoreApp := func() bool { + return env.HasFilesInDir(winAppPath, command) + } + if isWinStoreApp() { + commandFile := winAppPath + command + return readWinAppLink(commandFile) + } + return "", errors.New("no Windows Store App") +} + // // Takes a registry path to a key like // "HKLM\Software\Microsoft\Windows NT\CurrentVersion\EditionID" diff --git a/src/environment/windows_win32.go b/src/environment/windows_win32.go index 05baf65c..6dc2524b 100644 --- a/src/environment/windows_win32.go +++ b/src/environment/windows_win32.go @@ -3,10 +3,12 @@ package environment import ( + "errors" "fmt" "oh-my-posh/regex" "strings" "syscall" + "unicode/utf16" "unsafe" "golang.org/x/sys/windows" @@ -199,3 +201,83 @@ func getHKEYHandleFromAbbrString(abbr string) windows.Handle { return 0 } + +type REPARSE_DATA_BUFFER struct { // nolint: revive + ReparseTag uint32 + ReparseDataLength uint16 + Reserved uint16 + DUMMYUNIONNAME byte +} + +type GenericDataBuffer struct { + DataBuffer [1]uint8 +} + +type AppExecLinkReparseBuffer struct { + Version uint32 + StringList [1]uint16 +} + +func (rb *AppExecLinkReparseBuffer) Path() string { + UTF16ToStringPosition := func(s []uint16) (string, int) { + for i, v := range s { + if v == 0 { + s = s[0:i] + return string(utf16.Decode(s)), i + } + } + return "", 0 + } + s := (*[0xffff]uint16)(unsafe.Pointer(&rb.StringList[0]))[0:] + var link string + position := 0 + for i := 0; i <= 2; i++ { + link, position = UTF16ToStringPosition(s) + position++ + s = s[position:] + } + return link +} + +// 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 appexec link version") + } + link := appExecLink.Path() + return link, nil +}