返回 DeepSeek-Reasonix
plan.go
1 package winuninstall
2
3 import (
4 "fmt"
5 "strings"
6
7 "golang.org/x/mod/semver"
8 )
9
10 const (
11 CurrentKeyName = "ReasonixReasonix"
12 LegacyKeyName = "Reasonix"
13 )
14
15 type Registration struct {
16 DisplayName string
17 DisplayVersion string
18 Publisher string
19 InstallLocation string
20 DisplayIcon string
21 UninstallString string
22 QuietUninstallString string
23 }
24
25 type ReconcilePlan struct {
26 Managed bool
27 Desired Registration
28 DeleteLegacy bool
29 }
30
31 // Plan returns the safe per-user uninstall-registration mutation for an
32 // installed Reasonix tree. Portable trees have no owned registration and no
33 // root uninstaller, so they deliberately produce a no-op plan.
34 func Plan(current, legacy *Registration, installRoot, version string, uninstallerPresent bool) (ReconcilePlan, error) {
35 root := cleanWindowsPath(installRoot)
36 if root == "" {
37 return ReconcilePlan{}, fmt.Errorf("Windows install root is empty")
38 }
39 version = strings.TrimSpace(version)
40 if !strings.HasPrefix(version, "v") {
41 version = "v" + version
42 }
43 if !semver.IsValid(version) {
44 return ReconcilePlan{}, fmt.Errorf("Windows install version is invalid")
45 }
46 if !uninstallerPresent {
47 return ReconcilePlan{}, nil
48 }
49
50 currentOwned := registrationOwnsRoot(current, root)
51 legacyOwned := registrationOwnsRoot(legacy, root)
52 // A legacy-only key may still point at the Tauri 0.53 uninstaller. The full
53 // signed installer replaces that binary before migrating the key; the
54 // update helper must not promote it into the current Wails identity.
55 if !currentOwned {
56 return ReconcilePlan{}, nil
57 }
58
59 uninstaller := joinWindowsPath(root, "uninstall.exe")
60 return ReconcilePlan{
61 Managed: true,
62 Desired: Registration{
63 DisplayName: "Reasonix",
64 DisplayVersion: strings.TrimPrefix(version, "v"),
65 Publisher: "Reasonix",
66 InstallLocation: root,
67 DisplayIcon: joinWindowsPath(root, "reasonix-launcher.exe"),
68 UninstallString: quoteWindowsPath(uninstaller),
69 QuietUninstallString: quoteWindowsPath(uninstaller) + " /S",
70 },
71 DeleteLegacy: legacyOwned,
72 }, nil
73 }
74
75 func registrationOwnsRoot(reg *Registration, root string) bool {
76 if reg == nil || !strings.EqualFold(strings.TrimSpace(reg.DisplayName), "Reasonix") {
77 return false
78 }
79 if sameWindowsPath(reg.InstallLocation, root) {
80 return true
81 }
82 return sameWindowsPath(uninstallExecutable(reg.UninstallString), joinWindowsPath(root, "uninstall.exe"))
83 }
84
85 func uninstallExecutable(command string) string {
86 command = strings.TrimSpace(command)
87 if command == "" {
88 return ""
89 }
90 if command[0] == '"' {
91 if end := strings.Index(command[1:], `"`); end >= 0 {
92 return command[1 : end+1]
93 }
94 }
95 if end := strings.IndexAny(command, " \t"); end >= 0 {
96 return command[:end]
97 }
98 return command
99 }
100
101 func sameWindowsPath(left, right string) bool {
102 left = strings.ToLower(cleanWindowsPath(left))
103 right = strings.ToLower(cleanWindowsPath(right))
104 return left != "" && left == right
105 }
106
107 func cleanWindowsPath(value string) string {
108 value = strings.TrimSpace(value)
109 if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
110 value = value[1 : len(value)-1]
111 }
112 value = strings.ReplaceAll(strings.TrimSpace(value), "/", `\`)
113 for len(value) > 3 && strings.HasSuffix(value, `\`) {
114 value = strings.TrimSuffix(value, `\`)
115 }
116 return value
117 }
118
119 func joinWindowsPath(root, name string) string {
120 return cleanWindowsPath(root) + `\` + strings.TrimLeft(strings.ReplaceAll(name, "/", `\`), `\`)
121 }
122
123 func quoteWindowsPath(path string) string {
124 return `"` + cleanWindowsPath(path) + `"`
125 }
126
126 lines GO