icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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 }