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 }