返回 DeepSeek-Reasonix
release_unit.go
根目录 / desktop / cmd / update-helper / release_unit.go
1 package main
2
3 import (
4 "fmt"
5 "io"
6 "os"
7 "path/filepath"
8 "sort"
9 "strings"
10
11 "reasonix/desktop/internal/update"
12 "reasonix/internal/repair"
13 )
14
15 const maxWindowsPayloadMetadataSize = 64 << 10
16
17 var verifyWindowsPayloadManifestFn = update.Verify
18
19 type stagedFileUpdateMember struct {
20 targetPath string
21 content []byte
22 mode os.FileMode
23 }
24
25 // loadWindowsStagedReleaseUnit validates and reads the complete NSIS payload
26 // before any live release-unit member is moved. An existing Reasonix.exe is the
27 // portable alias of reasonix-launcher.exe and reuses those staged bytes; an
28 // installed package that did not have the alias remains unchanged.
29 func loadWindowsStagedReleaseUnit(claimed *repair.UpdateTransaction, stagingDir string) ([]stagedFileUpdateMember, error) {
30 if claimed == nil || claimed.TargetKind != "file" || len(claimed.Files) == 0 {
31 return nil, fmt.Errorf("load staged release unit: transaction identity is incomplete")
32 }
33 if err := validateWindowsClaimedReleaseUnit(claimed); err != nil {
34 return nil, fmt.Errorf("load staged release unit: %w", err)
35 }
36 stagingDir = filepath.Clean(strings.TrimSpace(stagingDir))
37 if stagingDir == "" || stagingDir == "." || !filepath.IsAbs(stagingDir) {
38 return nil, fmt.Errorf("load staged release unit: staging directory is invalid")
39 }
40 info, err := os.Lstat(stagingDir)
41 if err != nil {
42 return nil, fmt.Errorf("load staged release unit: inspect staging directory: %w", err)
43 }
44 if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
45 return nil, fmt.Errorf("load staged release unit: staging path is not a directory")
46 }
47 hashes, err := loadWindowsPayloadManifest(stagingDir, claimed.ToVersion)
48 if err != nil {
49 return nil, fmt.Errorf("load staged release unit: %w", err)
50 }
51
52 contents := make(map[string][]byte)
53 members := make([]stagedFileUpdateMember, 0, len(claimed.Files))
54 seenTargets := make(map[string]struct{}, len(claimed.Files))
55 for _, file := range claimed.Files {
56 targetPath := filepath.Clean(strings.TrimSpace(file.TargetPath))
57 targetKey := strings.ToLower(targetPath)
58 if targetPath == "" || targetPath == "." {
59 return nil, fmt.Errorf("load staged release unit: target path is invalid")
60 }
61 if _, ok := seenTargets[targetKey]; ok {
62 return nil, fmt.Errorf("load staged release unit: duplicate target %s", filepath.Base(targetPath))
63 }
64 seenTargets[targetKey] = struct{}{}
65 if strings.EqualFold(filepath.Base(targetPath), "Reasonix.exe") && file.MissingBefore {
66 continue
67 }
68
69 sourceName, err := windowsStagedSourceName(filepath.Base(targetPath))
70 if err != nil {
71 return nil, err
72 }
73 sourcePath := filepath.Join(stagingDir, sourceName)
74 content, ok := contents[sourceName]
75 if !ok {
76 sourceInfo, statErr := os.Lstat(sourcePath)
77 if statErr != nil {
78 return nil, fmt.Errorf("load staged release unit: inspect %s: %w", sourceName, statErr)
79 }
80 if !sourceInfo.Mode().IsRegular() {
81 return nil, fmt.Errorf("load staged release unit: %s is not a regular file", sourceName)
82 }
83 content, err = readVerifiedWindowsStagedPayloadFn(sourcePath)
84 if err != nil {
85 return nil, fmt.Errorf("load staged release unit: read %s: %w", sourceName, err)
86 }
87 if !strings.EqualFold(update.WindowsPayloadSHA256(content), hashes[sourceName]) {
88 return nil, fmt.Errorf("load staged release unit: %s does not match the signed release manifest", sourceName)
89 }
90 contents[sourceName] = content
91 }
92 members = append(members, stagedFileUpdateMember{
93 targetPath: targetPath,
94 content: content,
95 mode: 0o700,
96 })
97 }
98
99 // Publish the running desktop last. If an earlier member fails, the old
100 // desktop remains the executable entry point that can report/retry recovery.
101 sort.SliceStable(members, func(i, j int) bool {
102 iPrimary := strings.EqualFold(members[i].targetPath, claimed.TargetPath)
103 jPrimary := strings.EqualFold(members[j].targetPath, claimed.TargetPath)
104 return !iPrimary && jPrimary
105 })
106 return members, nil
107 }
108
109 func validateWindowsClaimedReleaseUnit(claimed *repair.UpdateTransaction) error {
110 if claimed == nil ||
111 !strings.EqualFold(filepath.Base(claimed.TargetPath), "reasonix-desktop.exe") {
112 return fmt.Errorf("claimed release unit primary executable is invalid")
113 }
114 required := map[string]bool{
115 "reasonix-desktop.exe": false,
116 "reasonix-guard.exe": false,
117 "reasonix-launcher.exe": false,
118 "reasonix-update-helper.exe": false,
119 "reasonix-cli.exe": false,
120 "reasonix.exe": false,
121 }
122 installDir := filepath.Clean(filepath.Dir(claimed.TargetPath))
123 for _, file := range claimed.Files {
124 target := filepath.Clean(strings.TrimSpace(file.TargetPath))
125 if target == "" || target == "." ||
126 !strings.EqualFold(filepath.Dir(target), installDir) {
127 return fmt.Errorf("claimed release unit target is outside the installation directory")
128 }
129 name := strings.ToLower(filepath.Base(target))
130 seen, ok := required[name]
131 if !ok {
132 return fmt.Errorf("claimed release unit contains an unexpected target")
133 }
134 if seen {
135 return fmt.Errorf("claimed release unit contains a duplicate target")
136 }
137 required[name] = true
138 }
139 for name, seen := range required {
140 if !seen {
141 return fmt.Errorf("claimed release unit omits %s", name)
142 }
143 }
144 if len(claimed.Files) != len(required) {
145 return fmt.Errorf("claimed release unit contains an unexpected target")
146 }
147 return nil
148 }
149
150 func loadWindowsPayloadManifest(stagingDir, expectedVersion string) (map[string]string, error) {
151 manifest, err := readWindowsPayloadMetadata(filepath.Join(stagingDir, update.WindowsPayloadManifestName))
152 if err != nil {
153 return nil, fmt.Errorf("read signed release manifest: %w", err)
154 }
155 signature, err := readWindowsPayloadMetadata(filepath.Join(stagingDir, update.WindowsPayloadSignatureName))
156 if err != nil {
157 return nil, fmt.Errorf("read signed release manifest signature: %w", err)
158 }
159 if err := verifyWindowsPayloadManifestFn(manifest, signature); err != nil {
160 return nil, fmt.Errorf("verify signed release manifest: %w", err)
161 }
162 hashes, err := update.DecodeWindowsPayloadManifest(manifest, expectedVersion)
163 if err != nil {
164 return nil, err
165 }
166 return hashes, nil
167 }
168
169 func readWindowsPayloadMetadata(path string) ([]byte, error) {
170 pathInfo, err := os.Lstat(path)
171 if err != nil {
172 return nil, err
173 }
174 if !pathInfo.Mode().IsRegular() {
175 return nil, fmt.Errorf("%s is not a regular file", filepath.Base(path))
176 }
177 file, err := os.Open(path)
178 if err != nil {
179 return nil, err
180 }
181 defer file.Close()
182 info, err := file.Stat()
183 if err != nil {
184 return nil, err
185 }
186 if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maxWindowsPayloadMetadataSize {
187 return nil, fmt.Errorf("%s is not a bounded regular file", filepath.Base(path))
188 }
189 if !os.SameFile(pathInfo, info) {
190 return nil, fmt.Errorf("%s changed before it was opened", filepath.Base(path))
191 }
192 data, err := io.ReadAll(io.LimitReader(file, maxWindowsPayloadMetadataSize+1))
193 if err != nil {
194 return nil, err
195 }
196 if len(data) == 0 || len(data) > maxWindowsPayloadMetadataSize {
197 return nil, fmt.Errorf("%s changed size while it was read", filepath.Base(path))
198 }
199 return data, nil
200 }
201
202 func windowsStagedSourceName(targetBase string) (string, error) {
203 switch strings.ToLower(strings.TrimSpace(targetBase)) {
204 case "reasonix-desktop.exe":
205 return "reasonix-desktop.exe", nil
206 case "reasonix-guard.exe":
207 return "reasonix-guard.exe", nil
208 case "reasonix-launcher.exe":
209 return "reasonix-launcher.exe", nil
210 case "reasonix-update-helper.exe":
211 return "reasonix-update-helper.exe", nil
212 case "reasonix-cli.exe":
213 return "reasonix-cli.exe", nil
214 case "reasonix.exe":
215 return "reasonix-launcher.exe", nil
216 default:
217 return "", fmt.Errorf("load staged release unit: unsupported target %q", targetBase)
218 }
219 }
220
221 func publishLoadedFileUpdateReleaseUnit(
222 claimed *repair.UpdateTransaction,
223 members []stagedFileUpdateMember,
224 publish func(*repair.UpdateTransaction, string, []byte, os.FileMode) (repair.FileUpdateInstallReceipt, error),
225 ) ([]repair.FileUpdateInstallReceipt, error) {
226 if publish == nil || len(members) == 0 {
227 return nil, fmt.Errorf("publish staged release unit: payload is incomplete")
228 }
229 receipts := make([]repair.FileUpdateInstallReceipt, 0, len(members))
230 for _, member := range members {
231 receipt, err := publish(claimed, member.targetPath, member.content, member.mode)
232 if err != nil {
233 return receipts, fmt.Errorf("publish staged release unit %s: %w", filepath.Base(member.targetPath), err)
234 }
235 receipts = append(receipts, receipt)
236 }
237 return receipts, nil
238 }
239
239 lines GO