返回 DeepSeek-Reasonix
windows_payload.go
根目录 / desktop / internal / update / windows_payload.go
1 package update
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "io"
10 "maps"
11 "slices"
12 "strings"
13
14 "reasonix/internal/installlayout"
15 )
16
17 const (
18 // WindowsPayloadManifestSchemaVersion lists the flat release unit plus every
19 // file of the app/ shell tree; schema 1 manifests carry the flat list only.
20 WindowsPayloadManifestSchemaVersion = 2
21 windowsPayloadFlatSchemaVersion = 1
22 WindowsPayloadManifestName = "reasonix-payload.json"
23 WindowsPayloadSignatureName = WindowsPayloadManifestName + ".minisig"
24 // WindowsPayloadTreePrefix starts every shell tree entry name.
25 WindowsPayloadTreePrefix = installlayout.AppShellDirName + "/"
26 )
27
28 var windowsPayloadFileNames = [...]string{
29 "reasonix-desktop.exe",
30 "reasonix-guard.exe",
31 "reasonix-launcher.exe",
32 "reasonix-update-helper.exe",
33 "reasonix-cli.exe",
34 }
35
36 var windowsPayloadVersionFileNames = [...]string{
37 "reasonix-desktop.exe",
38 "reasonix-update-helper.exe",
39 "reasonix-cli.exe",
40 }
41
42 type WindowsPayloadManifest struct {
43 SchemaVersion int `json:"schemaVersion"`
44 Version string `json:"version"`
45 Files []WindowsPayloadManifestFile `json:"files"`
46 }
47
48 type WindowsPayloadManifestFile struct {
49 Name string `json:"name"`
50 SHA256 string `json:"sha256"`
51 }
52
53 func WindowsPayloadFileNames() []string {
54 return append([]string(nil), windowsPayloadFileNames[:]...)
55 }
56
57 // ValidWindowsPayloadTreeName reports whether name is a shell tree entry.
58 func ValidWindowsPayloadTreeName(name string) bool {
59 return strings.HasPrefix(name, WindowsPayloadTreePrefix) && installlayout.ValidateMemberName(name) == nil
60 }
61
62 // WindowsPayloadVersionMembers lists the manifest entries published under
63 // versions/<version>/, sorted: desktop, CLI, update helper and the app/ tree.
64 func WindowsPayloadVersionMembers(hashes map[string]string) []string {
65 members := make([]string, 0, len(hashes))
66 for name := range hashes {
67 if slices.Contains(windowsPayloadVersionFileNames[:], name) || ValidWindowsPayloadTreeName(name) {
68 members = append(members, name)
69 }
70 }
71 slices.Sort(members)
72 return members
73 }
74
75 func EncodeWindowsPayloadManifest(version string, hashes map[string]string) ([]byte, error) {
76 version = strings.TrimSpace(version)
77 if version == "" {
78 return nil, fmt.Errorf("Windows payload manifest version is empty")
79 }
80 manifest := WindowsPayloadManifest{
81 SchemaVersion: WindowsPayloadManifestSchemaVersion,
82 Version: version,
83 Files: make([]WindowsPayloadManifestFile, 0, len(hashes)),
84 }
85 for _, name := range slices.Sorted(maps.Keys(hashes)) {
86 if !windowsPayloadMemberAllowed(name, WindowsPayloadManifestSchemaVersion) {
87 return nil, fmt.Errorf("Windows payload manifest contains unexpected file %q", name)
88 }
89 hash := strings.ToLower(strings.TrimSpace(hashes[name]))
90 if !validWindowsPayloadSHA256(hash) {
91 return nil, fmt.Errorf("Windows payload manifest hash for %s is invalid", name)
92 }
93 manifest.Files = append(manifest.Files, WindowsPayloadManifestFile{
94 Name: name,
95 SHA256: hash,
96 })
97 }
98 if err := requireWindowsPayloadFlatMembers(hashes); err != nil {
99 return nil, err
100 }
101 b, err := json.MarshalIndent(manifest, "", " ")
102 if err != nil {
103 return nil, err
104 }
105 return append(b, '\n'), nil
106 }
107
108 func DecodeWindowsPayloadManifest(data []byte, expectedVersion string) (map[string]string, error) {
109 var manifest WindowsPayloadManifest
110 dec := json.NewDecoder(bytes.NewReader(data))
111 dec.DisallowUnknownFields()
112 if err := dec.Decode(&manifest); err != nil {
113 return nil, fmt.Errorf("decode Windows payload manifest: %w", err)
114 }
115 var trailing any
116 if err := dec.Decode(&trailing); err != io.EOF {
117 if err == nil {
118 return nil, fmt.Errorf("decode Windows payload manifest: trailing JSON value")
119 }
120 return nil, fmt.Errorf("decode Windows payload manifest: %w", err)
121 }
122 expectedVersion = strings.TrimSpace(expectedVersion)
123 if !windowsPayloadSchemaSupported(manifest.SchemaVersion) ||
124 expectedVersion == "" ||
125 manifest.Version != expectedVersion {
126 return nil, fmt.Errorf("Windows payload manifest identity does not match the pending update")
127 }
128 hashes := make(map[string]string, len(manifest.Files))
129 for _, file := range manifest.Files {
130 name := file.Name
131 hash := file.SHA256
132 if !windowsPayloadMemberAllowed(name, manifest.SchemaVersion) || !validWindowsPayloadSHA256(hash) {
133 return nil, fmt.Errorf("Windows payload manifest member is invalid")
134 }
135 if _, duplicate := hashes[name]; duplicate {
136 return nil, fmt.Errorf("Windows payload manifest contains duplicate members")
137 }
138 hashes[name] = hash
139 }
140 if err := requireWindowsPayloadFlatMembers(hashes); err != nil {
141 return nil, err
142 }
143 return hashes, nil
144 }
145
146 func windowsPayloadSchemaSupported(schemaVersion int) bool {
147 return schemaVersion == windowsPayloadFlatSchemaVersion || schemaVersion == WindowsPayloadManifestSchemaVersion
148 }
149
150 func windowsPayloadMemberAllowed(name string, schemaVersion int) bool {
151 if slices.Contains(windowsPayloadFileNames[:], name) {
152 return true
153 }
154 return schemaVersion >= WindowsPayloadManifestSchemaVersion && ValidWindowsPayloadTreeName(name)
155 }
156
157 func requireWindowsPayloadFlatMembers(hashes map[string]string) error {
158 for _, name := range windowsPayloadFileNames {
159 if _, ok := hashes[name]; !ok {
160 return fmt.Errorf("Windows payload manifest is incomplete")
161 }
162 }
163 return nil
164 }
165
166 func WindowsPayloadSHA256(data []byte) string {
167 sum := sha256.Sum256(data)
168 return hex.EncodeToString(sum[:])
169 }
170
171 func validWindowsPayloadSHA256(value string) bool {
172 if len(value) != sha256.Size*2 {
173 return false
174 }
175 if value != strings.ToLower(value) {
176 return false
177 }
178 _, err := hex.DecodeString(value)
179 return err == nil
180 }
181
181 lines GO