返回 DeepSeek-Reasonix
identity_windows.go
根目录 / internal / appidentity / identity_windows.go
1 //go:build windows
2
3 package appidentity
4
5 import (
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "runtime"
11 "strings"
12 "syscall"
13 "unsafe"
14
15 "golang.org/x/sys/windows"
16
17 "reasonix/internal/installlayout"
18 )
19
20 const (
21 clsctxInprocServer = 0x1
22 coinitApartmentThread = 0x2
23 rpcEChangedMode = 0x80010106
24 slgpRawPath = 0x4
25 stgmReadWrite = 0x2
26 vtLPWSTR = 31
27 windowsPathBuffer = 32768
28 )
29
30 var (
31 clsidShellLink = windows.GUID{
32 Data1: 0x00021401,
33 Data4: [8]byte{0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46},
34 }
35 iidIShellLinkW = windows.GUID{
36 Data1: 0x000214f9,
37 Data4: [8]byte{0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46},
38 }
39 iidIPersistFile = windows.GUID{
40 Data1: 0x0000010b,
41 Data4: [8]byte{0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46},
42 }
43 iidIPropertyStore = windows.GUID{
44 Data1: 0x886d8eeb,
45 Data2: 0x8cf2,
46 Data3: 0x4446,
47 Data4: [8]byte{0x8d, 0x02, 0xcd, 0xba, 0x1d, 0xbd, 0xcf, 0x99},
48 }
49 pkeyAppUserModelID = propertyKey{
50 FormatID: windows.GUID{
51 Data1: 0x9f4c2855,
52 Data2: 0x9f79,
53 Data3: 0x4b39,
54 Data4: [8]byte{0xa8, 0xd0, 0xe1, 0xd4, 0x2d, 0xe1, 0xd5, 0xf3},
55 },
56 PropertyID: 5,
57 }
58
59 ole32 = windows.NewLazySystemDLL("ole32.dll")
60 shell32 = windows.NewLazySystemDLL("shell32.dll")
61
62 procCoCreateInstance = ole32.NewProc("CoCreateInstance")
63 procCoInitializeEx = ole32.NewProc("CoInitializeEx")
64 procCoUninitialize = ole32.NewProc("CoUninitialize")
65 procPropVariantClear = ole32.NewProc("PropVariantClear")
66 procPropVariantCopy = ole32.NewProc("PropVariantCopy")
67 procSetCurrentProcessExplicitAppUserModelID = shell32.NewProc("SetCurrentProcessExplicitAppUserModelID")
68 procSHChangeNotify = shell32.NewProc("SHChangeNotify")
69
70 knownFolderPath = windows.KnownFolderPath
71 )
72
73 type propertyKey struct {
74 FormatID windows.GUID
75 PropertyID uint32
76 }
77
78 type propVariant struct {
79 VariantType uint16
80 Reserved1 uint16
81 Reserved2 uint16
82 Reserved3 uint16
83 Value *uint16
84 Value2 uintptr
85 }
86
87 type unknownVTable struct {
88 QueryInterface uintptr
89 AddRef uintptr
90 Release uintptr
91 }
92
93 type shellLinkW struct {
94 VTable *shellLinkWVTable
95 }
96
97 type shellLinkWVTable struct {
98 unknownVTable
99 GetPath uintptr
100 GetIDList uintptr
101 SetIDList uintptr
102 GetDescription uintptr
103 SetDescription uintptr
104 GetWorkingDirectory uintptr
105 SetWorkingDirectory uintptr
106 GetArguments uintptr
107 SetArguments uintptr
108 GetHotkey uintptr
109 SetHotkey uintptr
110 GetShowCmd uintptr
111 SetShowCmd uintptr
112 GetIconLocation uintptr
113 SetIconLocation uintptr
114 SetRelativePath uintptr
115 Resolve uintptr
116 SetPath uintptr
117 }
118
119 type persistFile struct {
120 VTable *persistFileVTable
121 }
122
123 type persistFileVTable struct {
124 unknownVTable
125 GetClassID uintptr
126 IsDirty uintptr
127 Load uintptr
128 Save uintptr
129 SaveCompleted uintptr
130 GetCurFile uintptr
131 }
132
133 type propertyStore struct {
134 VTable *propertyStoreVTable
135 }
136
137 type propertyStoreVTable struct {
138 unknownVTable
139 GetCount uintptr
140 GetAt uintptr
141 GetValue uintptr
142 SetValue uintptr
143 Commit uintptr
144 }
145
146 type loadedShortcut struct {
147 link *shellLinkW
148 persist *persistFile
149 store *propertyStore
150 path string
151 }
152
153 func ApplyToCurrentProcess() error {
154 id, err := windows.UTF16PtrFromString(AppUserModelID)
155 if err != nil {
156 return err
157 }
158 hr, _, _ := procSetCurrentProcessExplicitAppUserModelID.Call(uintptr(unsafe.Pointer(id)))
159 return checkHRESULT("SetCurrentProcessExplicitAppUserModelID", hr)
160 }
161
162 func RepairOwnedShortcuts(installRoot string) error {
163 installRoot = filepath.Clean(strings.TrimSpace(installRoot))
164 if installRoot == "." || installRoot == "" {
165 return nil
166 }
167 paths, discoveryErr := shortcutCandidates(installRoot)
168 return errors.Join(discoveryErr, RepairShortcuts(installRoot, paths))
169 }
170
171 // RepairShortcuts repairs only existing links whose targets belong to installRoot.
172 // Installer callers pass the exact paths created in their selected shell context.
173 func RepairShortcuts(installRoot string, paths []string) error {
174 if len(paths) == 0 {
175 return nil
176 }
177 if err := validateShortcutPaths(installRoot, paths); err != nil {
178 return err
179 }
180
181 runtime.LockOSThread()
182 defer runtime.UnlockOSThread()
183 uninitialize, err := initializeCOM()
184 if err != nil {
185 return err
186 }
187 defer uninitialize()
188
189 var repairErr error
190 for _, path := range paths {
191 info, err := os.Lstat(path)
192 if err != nil {
193 if !os.IsNotExist(err) {
194 repairErr = errors.Join(repairErr, err)
195 }
196 continue
197 }
198 if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
199 continue
200 }
201 changed, err := repairOwnedShortcut(path, installRoot)
202 if err != nil {
203 repairErr = errors.Join(repairErr, fmt.Errorf("%s: %w", path, err))
204 continue
205 }
206 if changed {
207 notifyShortcutChanged(path)
208 }
209 }
210 return repairErr
211 }
212
213 func shortcutCandidates(installRoot string) ([]string, error) {
214 seen := make(map[string]struct{})
215 paths := make([]string, 0, 8)
216 add := func(path string) {
217 path = filepath.Clean(strings.TrimSpace(path))
218 if path == "." || path == "" {
219 return
220 }
221 key := strings.ToLower(path)
222 if _, ok := seen[key]; ok {
223 return
224 }
225 seen[key] = struct{}{}
226 paths = append(paths, path)
227 }
228 addReasonixLinks := func(dir string) error {
229 entries, err := os.ReadDir(dir)
230 if err != nil {
231 if os.IsNotExist(err) {
232 return nil
233 }
234 return err
235 }
236 for _, entry := range entries {
237 if !entry.IsDir() && reasonixShortcutName(entry.Name()) {
238 add(filepath.Join(dir, entry.Name()))
239 }
240 }
241 return nil
242 }
243
244 add(filepath.Join(installRoot, "Reasonix.lnk"))
245 var resultErr error
246 resultErr = errors.Join(resultErr, addReasonixLinks(installRoot))
247 for _, folderID := range []*windows.KNOWNFOLDERID{
248 windows.FOLDERID_Desktop, windows.FOLDERID_Programs,
249 windows.FOLDERID_PublicDesktop, windows.FOLDERID_CommonPrograms,
250 } {
251 folder, err := knownFolderPath(folderID, windows.KF_FLAG_DEFAULT)
252 if err != nil {
253 resultErr = errors.Join(resultErr, err)
254 continue
255 }
256 resultErr = errors.Join(resultErr, addReasonixLinks(folder))
257 if folderID == windows.FOLDERID_Programs || folderID == windows.FOLDERID_CommonPrograms {
258 resultErr = errors.Join(resultErr, addReasonixLinks(filepath.Join(folder, "Reasonix")))
259 }
260 }
261 roaming, err := knownFolderPath(windows.FOLDERID_RoamingAppData, windows.KF_FLAG_DEFAULT)
262 if err != nil {
263 resultErr = errors.Join(resultErr, err)
264 } else {
265 pinned := filepath.Join(roaming, "Microsoft", "Internet Explorer", "Quick Launch", "User Pinned", "TaskBar")
266 resultErr = errors.Join(resultErr, addReasonixLinks(pinned))
267 }
268 return paths, resultErr
269 }
270
271 func reasonixShortcutName(path string) bool {
272 name := filepath.Base(strings.TrimSpace(path))
273 return strings.EqualFold(filepath.Ext(name), ".lnk") &&
274 strings.HasPrefix(strings.ToLower(strings.TrimSuffix(name, filepath.Ext(name))), "reasonix")
275 }
276
277 func repairOwnedShortcut(path, installRoot string) (bool, error) {
278 shortcut, err := loadShortcut(path, stgmReadWrite)
279 if err != nil {
280 return false, err
281 }
282 defer shortcut.release()
283
284 target, err := shortcut.targetPath()
285 if err != nil {
286 return false, err
287 }
288 if !ownedShortcutTarget(target, installRoot) {
289 return false, nil
290 }
291 currentID, err := shortcut.appUserModelID()
292 if err != nil {
293 return false, err
294 }
295 icon, iconIndex, err := shortcut.iconLocation()
296 if err != nil {
297 return false, err
298 }
299 plan := planShortcutRepair(target, icon, currentID, installRoot, installlayout.HasCurrent(installRoot))
300 // A non-default icon index is a user choice even when its resource happens
301 // to be one of our executables. Never replace it with the product icon.
302 if iconIndex != 0 {
303 plan.icon = ""
304 }
305 if plan == (shortcutRepair{}) {
306 return false, nil
307 }
308 if plan.target != "" {
309 workingDir, err := shortcut.workingDirectory()
310 if err != nil {
311 return false, err
312 }
313 if err := shortcut.setString(shortcut.link.VTable.SetPath, plan.target); err != nil {
314 return false, err
315 }
316 if repairShortcutWorkingDirectory(workingDir, target, installRoot) {
317 if err := shortcut.setString(shortcut.link.VTable.SetWorkingDirectory, installRoot); err != nil {
318 return false, err
319 }
320 }
321 }
322 if plan.icon != "" {
323 if err := shortcut.setIconLocation(plan.icon, 0); err != nil {
324 return false, err
325 }
326 }
327 if plan.identity != "" {
328 if err := shortcut.writeAppUserModelID(plan.identity); err != nil {
329 return false, err
330 }
331 }
332 if err := shortcut.save(); err != nil {
333 return false, err
334 }
335 return true, nil
336 }
337
338 func loadShortcut(path string, mode uint32) (*loadedShortcut, error) {
339 var link *shellLinkW
340 hr, _, _ := procCoCreateInstance.Call(
341 uintptr(unsafe.Pointer(&clsidShellLink)),
342 0,
343 clsctxInprocServer,
344 uintptr(unsafe.Pointer(&iidIShellLinkW)),
345 uintptr(unsafe.Pointer(&link)),
346 )
347 if err := checkHRESULT("CoCreateInstance(CLSID_ShellLink)", hr); err != nil {
348 return nil, err
349 }
350 shortcut := &loadedShortcut{link: link, path: path}
351 if err := queryInterface(unsafe.Pointer(link), &iidIPersistFile, unsafe.Pointer(&shortcut.persist)); err != nil {
352 shortcut.release()
353 return nil, err
354 }
355 pathPtr, err := windows.UTF16PtrFromString(path)
356 if err != nil {
357 shortcut.release()
358 return nil, err
359 }
360 hr, _, _ = syscall.SyscallN(
361 shortcut.persist.VTable.Load,
362 uintptr(unsafe.Pointer(shortcut.persist)),
363 uintptr(unsafe.Pointer(pathPtr)),
364 uintptr(mode),
365 )
366 if err := checkHRESULT("IPersistFile.Load", hr); err != nil {
367 shortcut.release()
368 return nil, err
369 }
370 if err := queryInterface(unsafe.Pointer(link), &iidIPropertyStore, unsafe.Pointer(&shortcut.store)); err != nil {
371 shortcut.release()
372 return nil, err
373 }
374 return shortcut, nil
375 }
376
377 func (s *loadedShortcut) targetPath() (string, error) {
378 buffer := make([]uint16, windowsPathBuffer)
379 hr, _, _ := syscall.SyscallN(
380 s.link.VTable.GetPath,
381 uintptr(unsafe.Pointer(s.link)),
382 uintptr(unsafe.Pointer(&buffer[0])),
383 uintptr(len(buffer)),
384 0,
385 slgpRawPath,
386 )
387 if err := checkHRESULT("IShellLinkW.GetPath", hr); err != nil {
388 return "", err
389 }
390 return windows.UTF16ToString(buffer), nil
391 }
392
393 func (s *loadedShortcut) workingDirectory() (string, error) {
394 buffer := make([]uint16, windowsPathBuffer)
395 hr, _, _ := syscall.SyscallN(s.link.VTable.GetWorkingDirectory,
396 uintptr(unsafe.Pointer(s.link)), uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)))
397 if err := checkHRESULT("IShellLinkW.GetWorkingDirectory", hr); err != nil {
398 return "", err
399 }
400 return windows.UTF16ToString(buffer), nil
401 }
402
403 func (s *loadedShortcut) appUserModelID() (string, error) {
404 var value propVariant
405 hr, _, _ := syscall.SyscallN(
406 s.store.VTable.GetValue,
407 uintptr(unsafe.Pointer(s.store)),
408 uintptr(unsafe.Pointer(&pkeyAppUserModelID)),
409 uintptr(unsafe.Pointer(&value)),
410 )
411 if err := checkHRESULT("IPropertyStore.GetValue", hr); err != nil {
412 return "", err
413 }
414 defer clearPropVariant(&value)
415 if value.VariantType == 0 {
416 return "", nil
417 }
418 if value.VariantType != vtLPWSTR || value.Value == nil {
419 return "", fmt.Errorf("unsupported shortcut AppUserModelID type %d", value.VariantType)
420 }
421 return windows.UTF16PtrToString(value.Value), nil
422 }
423
424 func (s *loadedShortcut) setAppUserModelID(id string) error {
425 if err := s.writeAppUserModelID(id); err != nil {
426 return err
427 }
428 return s.save()
429 }
430
431 func (s *loadedShortcut) writeAppUserModelID(id string) error {
432 idPtr, err := windows.UTF16PtrFromString(id)
433 if err != nil {
434 return err
435 }
436 source := propVariant{VariantType: vtLPWSTR, Value: idPtr}
437 var value propVariant
438 hr, _, _ := procPropVariantCopy.Call(
439 uintptr(unsafe.Pointer(&value)),
440 uintptr(unsafe.Pointer(&source)),
441 )
442 runtime.KeepAlive(idPtr)
443 if err := checkHRESULT("PropVariantCopy", hr); err != nil {
444 return err
445 }
446 defer clearPropVariant(&value)
447 hr, _, _ = syscall.SyscallN(
448 s.store.VTable.SetValue,
449 uintptr(unsafe.Pointer(s.store)),
450 uintptr(unsafe.Pointer(&pkeyAppUserModelID)),
451 uintptr(unsafe.Pointer(&value)),
452 )
453 if err := checkHRESULT("IPropertyStore.SetValue", hr); err != nil {
454 return err
455 }
456 return nil
457 }
458
459 func (s *loadedShortcut) save() error {
460 hr, _, _ := syscall.SyscallN(s.store.VTable.Commit, uintptr(unsafe.Pointer(s.store)))
461 if err := checkHRESULT("IPropertyStore.Commit", hr); err != nil {
462 return err
463 }
464 pathPtr, err := windows.UTF16PtrFromString(s.path)
465 if err != nil {
466 return err
467 }
468 hr, _, _ = syscall.SyscallN(
469 s.persist.VTable.Save,
470 uintptr(unsafe.Pointer(s.persist)),
471 uintptr(unsafe.Pointer(pathPtr)),
472 1,
473 )
474 return checkHRESULT("IPersistFile.Save", hr)
475 }
476
477 func (s *loadedShortcut) release() {
478 if s.store != nil {
479 releaseInterface(unsafe.Pointer(s.store))
480 s.store = nil
481 }
482 if s.persist != nil {
483 releaseInterface(unsafe.Pointer(s.persist))
484 s.persist = nil
485 }
486 if s.link != nil {
487 releaseInterface(unsafe.Pointer(s.link))
488 s.link = nil
489 }
490 }
491
492 func initializeCOM() (func(), error) {
493 hr, _, _ := procCoInitializeEx.Call(0, coinitApartmentThread)
494 if uint32(hr) == rpcEChangedMode {
495 return func() {}, nil
496 }
497 if err := checkHRESULT("CoInitializeEx", hr); err != nil {
498 return nil, err
499 }
500 return func() { procCoUninitialize.Call() }, nil
501 }
502
503 func queryInterface(object unsafe.Pointer, iid *windows.GUID, result unsafe.Pointer) error {
504 vtable := (*unknownVTable)(*(*unsafe.Pointer)(object))
505 hr, _, _ := syscall.SyscallN(
506 vtable.QueryInterface,
507 uintptr(object),
508 uintptr(unsafe.Pointer(iid)),
509 uintptr(result),
510 )
511 return checkHRESULT("IUnknown.QueryInterface", hr)
512 }
513
514 func releaseInterface(object unsafe.Pointer) {
515 vtable := (*unknownVTable)(*(*unsafe.Pointer)(object))
516 _, _, _ = syscall.SyscallN(vtable.Release, uintptr(object))
517 }
518
519 func clearPropVariant(value *propVariant) {
520 _, _, _ = procPropVariantClear.Call(uintptr(unsafe.Pointer(value)))
521 }
522
523 func notifyShortcutChanged(path string) {
524 pathPtr, err := windows.UTF16PtrFromString(path)
525 if err != nil {
526 return
527 }
528 const (
529 shcneUpdateItem = 0x00002000
530 shcnfFlush = 0x1000
531 shcnfPathW = 0x0005
532 )
533 _, _, _ = procSHChangeNotify.Call(shcneUpdateItem, shcnfPathW|shcnfFlush, uintptr(unsafe.Pointer(pathPtr)), 0)
534 }
535
536 func checkHRESULT(operation string, result uintptr) error {
537 if int32(uint32(result)) >= 0 {
538 return nil
539 }
540 return fmt.Errorf("%s failed with HRESULT 0x%08X", operation, uint32(result))
541 }
542
542 lines GO