github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/util_windows.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  //go:build windows
     5  // +build windows
     6  
     7  package libkb
     8  
     9  import (
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"strings"
    14  	"syscall"
    15  	"time"
    16  	"unsafe"
    17  
    18  	"unicode/utf16"
    19  
    20  	"github.com/keybase/client/go/utils"
    21  	"golang.org/x/sys/windows"
    22  	"golang.org/x/sys/windows/registry"
    23  )
    24  
    25  type GUID struct {
    26  	Data1 uint32
    27  	Data2 uint16
    28  	Data3 uint16
    29  	Data4 [8]byte
    30  }
    31  
    32  // 3EB685DB-65F9-4CF6-A03A-E3EF65729F3D
    33  var (
    34  	FOLDERIDRoamingAppData = GUID{0x3EB685DB, 0x65F9, 0x4CF6, [8]byte{0xA0, 0x3A, 0xE3, 0xEF, 0x65, 0x72, 0x9F, 0x3D}}
    35  	// F1B32785-6FBA-4FCF-9D55-7B8E7F157091
    36  
    37  	FOLDERIDLocalAppData = GUID{0xF1B32785, 0x6FBA, 0x4FCF, [8]byte{0x9D, 0x55, 0x7B, 0x8E, 0x7F, 0x15, 0x70, 0x91}}
    38  	FOLDERIDSystem       = GUID{0x1AC14E77, 0x02E7, 0x4E5D, [8]byte{0xB7, 0x44, 0x2E, 0xB1, 0xAE, 0x51, 0x98, 0xB7}}
    39  )
    40  
    41  var (
    42  	modShell32               = windows.NewLazySystemDLL("Shell32.dll")
    43  	modOle32                 = windows.NewLazySystemDLL("Ole32.dll")
    44  	procSHGetKnownFolderPath = modShell32.NewProc("SHGetKnownFolderPath")
    45  	procCoTaskMemFree        = modOle32.NewProc("CoTaskMemFree")
    46  	shChangeNotifyProc       = modShell32.NewProc("SHChangeNotify")
    47  )
    48  
    49  // LookPath searches for an executable binary named file
    50  // in the directories named by the PATH environment variable.
    51  // If file contains a slash, it is tried directly and the PATH is not consulted.
    52  
    53  func canExec(s string) error {
    54  	if strings.IndexAny(s, `:\/`) == -1 {
    55  		s = s + "/"
    56  	}
    57  	_, err := exec.LookPath(s)
    58  	return err
    59  }
    60  
    61  func PosixLineEndings(arg string) string {
    62  	return strings.Replace(arg, "\r", "", -1)
    63  }
    64  
    65  func coTaskMemFree(pv unsafe.Pointer) {
    66  	syscall.Syscall(procCoTaskMemFree.Addr(), 1, uintptr(pv), 0, 0)
    67  	return
    68  }
    69  
    70  func GetDataDir(id GUID, name, envname string) (string, error) {
    71  	var pszPath unsafe.Pointer
    72  	// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762188(v=vs.85).aspx
    73  	// When this method returns, pszPath contains the address of a pointer to a null-terminated
    74  	// Unicode string that specifies the path of the known folder. The calling process
    75  	// is responsible for freeing this resource once it is no longer needed by calling
    76  	// coTaskMemFree.
    77  	//
    78  	// It's safe for pszPath to point to memory not managed by Go:
    79  	// see
    80  	// https://groups.google.com/d/msg/golang-nuts/ls7Eg7Ye9pU/ye1GLs8dBwAJ
    81  	// for details.
    82  	r0, _, _ := procSHGetKnownFolderPath.Call(uintptr(unsafe.Pointer(&id)), uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&pszPath)))
    83  	if uintptr(pszPath) != 0 {
    84  		defer coTaskMemFree(pszPath)
    85  	}
    86  	// Sometimes r0 == 0 and there still isn't a valid string returned
    87  	if r0 != 0 || uintptr(pszPath) == 0 {
    88  		return "", fmt.Errorf("can't get %s; HRESULT=%d, pszPath=%x", name, r0, pszPath)
    89  	}
    90  
    91  	var rawUnicode []uint16
    92  	for i := uintptr(0); ; i++ {
    93  		u16 := *(*uint16)(unsafe.Pointer(uintptr(pszPath) + 2*i))
    94  		if u16 == 0 {
    95  			break
    96  		}
    97  		if i == 1<<16 {
    98  			return "", fmt.Errorf("%s path has more than 65535 characters", name)
    99  		}
   100  
   101  		rawUnicode = append(rawUnicode, u16)
   102  	}
   103  
   104  	folder := string(utf16.Decode(rawUnicode))
   105  
   106  	if len(folder) == 0 {
   107  		// Try the environment as a backup
   108  		folder = os.Getenv(envname)
   109  		if len(folder) == 0 {
   110  			return "", fmt.Errorf("can't get %s directory", envname)
   111  		}
   112  	}
   113  
   114  	return folder, nil
   115  }
   116  
   117  func AppDataDir() (string, error) {
   118  	return GetDataDir(FOLDERIDRoamingAppData, "FOLDERIDRoamingAppData", "APPDATA")
   119  }
   120  
   121  func LocalDataDir() (string, error) {
   122  	return GetDataDir(FOLDERIDLocalAppData, "FOLDERIDLocalAppData", "LOCALAPPDATA")
   123  }
   124  
   125  func SystemDir() (string, error) {
   126  	return GetDataDir(FOLDERIDSystem, "FOLDERIDSystem", "")
   127  }
   128  
   129  // SafeWriteToFile retries safeWriteToFileOnce a few times on Windows,
   130  // in case AV programs interfere with 2 writes in quick succession.
   131  func SafeWriteToFile(g SafeWriteLogger, t SafeWriter, mode os.FileMode) error {
   132  
   133  	var err error
   134  	for i := 0; i < 5; i++ {
   135  		if err != nil {
   136  			g.Debug("Retrying failed safeWriteToFileOnce - %s", err)
   137  			time.Sleep(10 * time.Millisecond)
   138  		}
   139  		err = safeWriteToFileOnce(g, t, mode)
   140  		if err == nil {
   141  			break
   142  		}
   143  	}
   144  	return err
   145  }
   146  
   147  // renameFile performs some retries on Windows,
   148  // similar to SafeWriteToFile
   149  func renameFile(g *GlobalContext, src string, dest string) error {
   150  	var err error
   151  	for i := 0; i < 5; i++ {
   152  		if err != nil {
   153  			g.Log.Debug("Retrying failed os.Rename - %s", err)
   154  			time.Sleep(10 * time.Millisecond)
   155  		}
   156  		err = os.Rename(src, dest)
   157  		if err == nil {
   158  			break
   159  		}
   160  	}
   161  	return err
   162  }
   163  
   164  // Notify the shell that the thing located at path has changed
   165  func notifyShell(path string) {
   166  	pathEncoded := utf16.Encode([]rune(path))
   167  	if len(pathEncoded) > 0 {
   168  		shChangeNotifyProc.Call(
   169  			uintptr(0x00002000), // SHCNE_UPDATEITEM
   170  			uintptr(0x0005),     // SHCNF_PATHW
   171  			uintptr(unsafe.Pointer(&pathEncoded[0])),
   172  			0)
   173  	}
   174  }
   175  
   176  // Manipulate registry entries to reflect the mount point icon in the shell
   177  func ChangeMountIcon(oldMount string, newMount string) error {
   178  	if oldMount != "" {
   179  		// DeleteKey doesn't work if there are subkeys
   180  		registry.DeleteKey(registry.CURRENT_USER, `SOFTWARE\Classes\Applications\Explorer.exe\Drives\`+oldMount[:1]+`\DefaultIcon`)
   181  		registry.DeleteKey(registry.CURRENT_USER, `SOFTWARE\Classes\Applications\Explorer.exe\Drives\`+oldMount[:1]+`\DefaultLabel`)
   182  		registry.DeleteKey(registry.CURRENT_USER, `SOFTWARE\Classes\Applications\Explorer.exe\Drives\`+oldMount[:1])
   183  		notifyShell(oldMount)
   184  	}
   185  	if newMount == "" {
   186  		return nil
   187  	}
   188  	k, _, err := registry.CreateKey(registry.CURRENT_USER, `SOFTWARE\Classes\Applications\Explorer.exe\Drives\`+newMount[:1]+`\DefaultIcon`, registry.SET_VALUE|registry.CREATE_SUB_KEY|registry.WRITE)
   189  	defer k.Close()
   190  	if err != nil {
   191  		return err
   192  	}
   193  	keybaseExe, err := utils.BinPath()
   194  	if err != nil {
   195  		return err
   196  	}
   197  	// Use the second icon bound into keybase.exe - hence the 1
   198  	err = k.SetStringValue("", keybaseExe+",1")
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	// Also give a nice label
   204  	k2, _, err := registry.CreateKey(registry.CURRENT_USER, `SOFTWARE\Classes\Applications\Explorer.exe\Drives\`+newMount[:1]+`\DefaultLabel`, registry.SET_VALUE|registry.CREATE_SUB_KEY|registry.WRITE)
   205  	defer k2.Close()
   206  	err = k2.SetStringValue("", "Keybase")
   207  	notifyShell(newMount)
   208  	return err
   209  }