返回 DeepSeek-Reasonix
shortcut_policy.go
根目录 / internal / appidentity / shortcut_policy.go
1 package appidentity
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 )
8
9 type shortcutRepair struct {
10 target, icon, identity string
11 }
12
13 type shortcutTargetKind uint8
14
15 const (
16 foreignShortcutTarget shortcutTargetKind = iota
17 stableShortcutTarget
18 flatShortcutTarget
19 flatShellShortcutTarget
20 versionedShortcutTarget
21 )
22
23 func planShortcutRepair(target, icon, id, root string, versioned bool) shortcutRepair {
24 kind := classifyShortcutTarget(target, root)
25 if kind == foreignShortcutTarget || (id != "" && id != legacyAppUserModelID && id != AppUserModelID) {
26 return shortcutRepair{}
27 }
28 var plan shortcutRepair
29 if id != AppUserModelID {
30 plan.identity = AppUserModelID
31 }
32 launcher := ""
33 for _, name := range []string{"Reasonix.exe", "reasonix-launcher.exe"} {
34 candidate := filepath.Join(root, name)
35 info, err := os.Lstat(candidate)
36 if err == nil && info.Mode().IsRegular() && classifyShortcutTarget(candidate, root) == stableShortcutTarget {
37 launcher = candidate
38 break
39 }
40 }
41 if launcher == "" {
42 return plan
43 }
44 canonical := strings.EqualFold(filepath.Base(launcher), "Reasonix.exe")
45 needsRepair := func(path string, kind shortcutTargetKind) bool {
46 if staleShortcutEntry(path, kind, versioned) {
47 return true
48 }
49 if canonical && kind == stableShortcutTarget {
50 resolved, err := resolveShortcutTarget(path)
51 return err == nil && strings.EqualFold(filepath.Base(resolved), "reasonix-launcher.exe")
52 }
53 return false
54 }
55 if needsRepair(target, kind) {
56 plan.target = launcher
57 }
58 if needsRepair(icon, classifyShortcutTarget(icon, root)) {
59 plan.icon = launcher
60 }
61 return plan
62 }
63
64 // Preserve custom working directories. Only an empty directory or the old
65 // owned executable directory needs normalization when its target is migrated.
66 func repairShortcutWorkingDirectory(dir, oldTarget, root string) bool {
67 if dir == "" {
68 return true
69 }
70 kind := classifyShortcutTarget(oldTarget, root)
71 if kind != versionedShortcutTarget && kind != flatShellShortcutTarget {
72 return false
73 }
74 resolvedDir, err := resolveShortcutTarget(dir)
75 if err != nil {
76 return false
77 }
78 resolvedTarget, err := resolveShortcutTarget(oldTarget)
79 return err == nil && strings.EqualFold(resolvedDir, filepath.Dir(resolvedTarget))
80 }
81
82 func staleShortcutEntry(path string, kind shortcutTargetKind, versioned bool) bool {
83 if kind == versionedShortcutTarget || kind == flatShellShortcutTarget {
84 return true
85 }
86 if kind != flatShortcutTarget {
87 return false
88 }
89 _, err := os.Lstat(path)
90 return versioned || os.IsNotExist(err)
91 }
92
93 func ownedShortcutTarget(target, root string) bool {
94 return classifyShortcutTarget(target, root) != foreignShortcutTarget
95 }
96
97 func classifyShortcutTarget(target, root string) shortcutTargetKind {
98 if !filepath.IsAbs(target) || !filepath.IsAbs(root) {
99 return foreignShortcutTarget
100 }
101 info, err := os.Stat(root)
102 if err != nil || !info.IsDir() {
103 return foreignShortcutTarget
104 }
105 root, err = existingShortcutPath(root)
106 if err != nil {
107 return foreignShortcutTarget
108 }
109 target, err = resolveShortcutTarget(target)
110 if err != nil {
111 return foreignShortcutTarget
112 }
113 rel, err := filepath.Rel(root, target)
114 if err != nil {
115 return foreignShortcutTarget
116 }
117 parts := strings.Split(strings.ToLower(rel), string(filepath.Separator))
118 if len(parts) == 1 {
119 switch parts[0] {
120 case "reasonix-launcher.exe", "reasonix.exe":
121 return stableShortcutTarget
122 case "reasonix-desktop.exe":
123 return flatShortcutTarget
124 }
125 }
126 if len(parts) == 2 && parts[0] == "app" && parts[1] == "reasonix.exe" {
127 return flatShellShortcutTarget
128 }
129 if len(parts) >= 3 && parts[0] == "versions" {
130 if len(parts) == 3 && parts[2] == "reasonix-desktop.exe" {
131 return versionedShortcutTarget
132 }
133 if len(parts) == 4 && parts[2] == "app" && parts[3] == "reasonix.exe" {
134 return versionedShortcutTarget
135 }
136 }
137 return foreignShortcutTarget
138 }
139
140 // Resolve the existing ancestor even after an updater prunes a version. A
141 // junction inside versions must not make another installation look owned.
142 func resolveShortcutTarget(path string) (string, error) {
143 path = filepath.Clean(path)
144 if _, err := os.Lstat(path); err == nil {
145 return existingShortcutPath(path)
146 } else if !os.IsNotExist(err) {
147 return "", err
148 }
149 parentPath := filepath.Dir(path)
150 if parentPath == path {
151 return "", &os.PathError{Op: "resolve shortcut", Path: path, Err: os.ErrNotExist}
152 }
153 parent, err := resolveShortcutTarget(parentPath)
154 if err != nil {
155 return "", err
156 }
157 return filepath.Join(parent, filepath.Base(path)), nil
158 }
159
159 lines GO