register.go (7126B)
1 //go:build windows 2 3 package main 4 5 import ( 6 "errors" 7 "fmt" 8 "log/slog" 9 "os" 10 "unsafe" 11 12 "github.com/jackmordaunt/icns/cmd/shell-extension/internal/provider" 13 "golang.org/x/sys/windows" 14 "golang.org/x/sys/windows/registry" 15 ) 16 17 // Registry layout, relative to HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE: 18 // 19 // Software\Classes 20 // CLSID\{CLSID} (Default) = friendly name 21 // InprocServer32 (Default) = <path to dll> 22 // ThreadingModel = Apartment 23 // .icns\ShellEx\{IID_IThumbnailProvider} (Default) = {CLSID} 24 // 25 // Registration goes to HKLM when the process is elevated and to HKCU otherwise. 26 // 27 // The two differ in how the handler is hosted. Explorer normally runs 28 // thumbnail providers in an isolated surrogate process (dllhost.exe), and that 29 // surrogate only resolves handler CLSIDs from HKLM: a CLSID registered solely 30 // under HKCU fails there with REGDB_E_CLASSNOTREG, and a stale HKLM entry 31 // shadows any per-user one. A per-user registration therefore sets 32 // DisableProcessIsolation so the provider is loaded into Explorer itself, 33 // which does honour HKCU. Machine-wide registration keeps the default, 34 // isolated hosting. 35 const ( 36 friendlyName = "ICNS Thumbnail Provider" 37 clsidString = "{E21C95C5-5086-4F9F-8876-7FF4CE4AC6EC}" 38 iidThumbnailProvider = "{E357FCCD-A995-4576-B01F-234630154E96}" 39 40 clsidKey = `Software\Classes\CLSID\` + clsidString 41 inprocKey = clsidKey + `\InprocServer32` 42 shellExKey = `Software\Classes\.icns\ShellEx` 43 handlerKey = shellExKey + `\` + iidThumbnailProvider 44 approvedKey = `Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved` 45 ) 46 47 func init() { 48 // Keep the string constants and the GUIDs the provider answers to in sync. 49 if got := provider.CLSID.String(); got != clsidString { 50 panic(fmt.Sprintf("CLSID mismatch: registry uses %s, provider uses %s", clsidString, got)) 51 } 52 if got := provider.IID_IThumbnailProvider.String(); got != iidThumbnailProvider { 53 panic(fmt.Sprintf("IID mismatch: registry uses %s, provider uses %s", iidThumbnailProvider, got)) 54 } 55 } 56 57 // hive is a registry root together with its conventional name for messages. 58 type hive struct { 59 root registry.Key 60 name string 61 } 62 63 var ( 64 currentUser = hive{registry.CURRENT_USER, "HKCU"} 65 localMachine = hive{registry.LOCAL_MACHINE, "HKLM"} 66 ) 67 68 func elevated() bool { 69 return windows.GetCurrentProcessToken().IsElevated() 70 } 71 72 type regValue struct{ key, name, data string } 73 74 func register() error { 75 dll, err := modulePath() 76 if err != nil { 77 return fmt.Errorf("locating dll: %w", err) 78 } 79 h := currentUser 80 values := []regValue{ 81 {clsidKey, "", friendlyName}, 82 {inprocKey, "", dll}, 83 {inprocKey, "ThreadingModel", "Apartment"}, 84 {handlerKey, "", clsidString}, 85 } 86 if elevated() { 87 h = localMachine 88 // A per-user registration takes precedence in Explorer and would 89 // keep the in-process hosting; make the machine-wide one authoritative. 90 if err := unregisterFrom(currentUser); err != nil { 91 return err 92 } 93 // Only consulted when the EnforceShellExtensionSecurity policy is on, 94 // but it lives in HKLM so this is the only chance to write it. 95 values = append(values, regValue{approvedKey, clsidString, friendlyName}) 96 } 97 for _, v := range values { 98 if err := h.setValue(v.key, v.name, v.data); err != nil { 99 return err 100 } 101 } 102 if h == currentUser { 103 if err := h.setDWord(clsidKey, "DisableProcessIsolation", 1); err != nil { 104 return err 105 } 106 } 107 notifyAssocChanged() 108 return nil 109 } 110 111 func unregister() error { 112 hives := []hive{currentUser} 113 if elevated() { 114 hives = append(hives, localMachine) 115 } 116 for _, h := range hives { 117 if err := unregisterFrom(h); err != nil { 118 return err 119 } 120 } 121 notifyAssocChanged() 122 return nil 123 } 124 125 func unregisterFrom(h hive) error { 126 if h == localMachine { 127 if err := h.deleteValue(approvedKey, clsidString); err != nil { 128 return err 129 } 130 } 131 for _, key := range []string{handlerKey, shellExKey, inprocKey, clsidKey} { 132 err := registry.DeleteKey(h.root, key) 133 switch { 134 case err == nil, errors.Is(err, registry.ErrNotExist): 135 case key == shellExKey: 136 // Other handlers may live under ShellEx; leave it in place. 137 default: 138 return fmt.Errorf("deleting %s\\%s: %w", h.name, key, err) 139 } 140 } 141 return nil 142 } 143 144 func (h hive) setValue(key, name, data string) error { 145 k, _, err := registry.CreateKey(h.root, key, registry.SET_VALUE) 146 if err != nil { 147 return fmt.Errorf("creating %s\\%s: %w", h.name, key, err) 148 } 149 defer k.Close() 150 if err := k.SetStringValue(name, data); err != nil { 151 return fmt.Errorf("setting %s\\%s\\%q: %w", h.name, key, name, err) 152 } 153 return nil 154 } 155 156 func (h hive) setDWord(key, name string, data uint32) error { 157 k, _, err := registry.CreateKey(h.root, key, registry.SET_VALUE) 158 if err != nil { 159 return fmt.Errorf("creating %s\\%s: %w", h.name, key, err) 160 } 161 defer k.Close() 162 if err := k.SetDWordValue(name, data); err != nil { 163 return fmt.Errorf("setting %s\\%s\\%q: %w", h.name, key, name, err) 164 } 165 return nil 166 } 167 168 func (h hive) deleteValue(key, name string) error { 169 k, err := registry.OpenKey(h.root, key, registry.SET_VALUE) 170 if errors.Is(err, registry.ErrNotExist) { 171 return nil 172 } 173 if err != nil { 174 return fmt.Errorf("opening %s\\%s: %w", h.name, key, err) 175 } 176 defer k.Close() 177 if err := k.DeleteValue(name); err != nil && !errors.Is(err, registry.ErrNotExist) { 178 return fmt.Errorf("deleting %s\\%s\\%q: %w", h.name, key, name, err) 179 } 180 return nil 181 } 182 183 // anchor is any symbol that lives inside this DLL's image; its address lets 184 // us ask the loader which module we are. 185 var anchor byte 186 187 // modulePath returns the absolute path of the loaded DLL. 188 func modulePath() (string, error) { 189 var module windows.Handle 190 err := windows.GetModuleHandleEx( 191 windows.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS|windows.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, 192 (*uint16)(unsafe.Pointer(&anchor)), 193 &module, 194 ) 195 if err != nil { 196 return "", fmt.Errorf("GetModuleHandleEx: %w", err) 197 } 198 buf := make([]uint16, windows.MAX_LONG_PATH) 199 n, err := windows.GetModuleFileName(module, &buf[0], uint32(len(buf))) 200 if err != nil { 201 return "", fmt.Errorf("GetModuleFileName: %w", err) 202 } 203 return windows.UTF16ToString(buf[:n]), nil 204 } 205 206 var ( 207 shell32 = windows.NewLazySystemDLL("shell32.dll") 208 procSHChangeNotify = shell32.NewProc("SHChangeNotify") 209 ) 210 211 const ( 212 shcneAssocChanged = 0x08000000 213 shcnfIDList = 0x0000 214 shcnfFlush = 0x1000 215 ) 216 217 // notifyAssocChanged tells the shell that file associations changed so 218 // Explorer picks up the new handler without a restart. 219 func notifyAssocChanged() { 220 procSHChangeNotify.Call(shcneAssocChanged, shcnfIDList|shcnfFlush, 0, 0) 221 } 222 223 // logError reports a failure to stderr, which regsvr32 discards, and to the 224 // file named by ICNS_SHELLEXT_LOG when set. 225 func logError(msg string, err error) { 226 if path := os.Getenv("ICNS_SHELLEXT_LOG"); path != "" { 227 if f, ferr := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); ferr == nil { 228 defer f.Close() 229 slog.New(slog.NewTextHandler(f, nil)).Error(msg, "err", err) 230 } 231 } 232 slog.Error(msg, "err", err) 233 }