github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/integrations/sparkle/build.go (about) 1 package sparkle 2 3 import ( 4 "context" 5 "crypto/sha1" 6 "encoding/base64" 7 "encoding/xml" 8 "path" 9 "strings" 10 "time" 11 "unsafe" 12 13 "github.com/go-xmlfmt/xmlfmt" 14 "github.com/russross/blackfriday/v2" 15 16 "github.com/kubri/kubri/pkg/crypto/dsa" 17 "github.com/kubri/kubri/pkg/crypto/ed25519" 18 "github.com/kubri/kubri/source" 19 ) 20 21 func Build(ctx context.Context, c *Config) error { 22 items := read(ctx, c) 23 24 version := c.Version 25 if v := getVersionConstraint(items); v != "" { 26 version += "," + v 27 } 28 29 releases, err := c.Source.ListReleases(ctx, &source.ListOptions{ 30 Version: version, 31 Prerelease: c.Prerelease, 32 }) 33 if err == source.ErrNoReleaseFound { 34 return nil 35 } 36 if err != nil { 37 return err 38 } 39 40 i, err := getItems(ctx, c, releases) 41 if err != nil { 42 return err 43 } 44 items = append(i, items...) 45 46 link, err := c.Target.URL(ctx, c.FileName) 47 if err != nil { 48 return err 49 } 50 51 rss := &RSS{Channels: []*Channel{{ 52 Title: c.Title, 53 Description: c.Description, 54 Link: link, 55 Items: items, 56 }}} 57 58 return write(ctx, c, rss) 59 } 60 61 func read(ctx context.Context, c *Config) []*Item { 62 r, err := c.Target.NewReader(ctx, c.FileName) 63 if err != nil { 64 return nil 65 } 66 defer r.Close() 67 var rss RSS 68 if err = xml.NewDecoder(r).Decode(&rss); err != nil || len(rss.Channels) == 0 { 69 return nil 70 } 71 return rss.Channels[0].Items 72 } 73 74 func getVersionConstraint(items []*Item) string { 75 if len(items) == 0 { 76 return "" 77 } 78 79 v := make([]byte, 0, len(items)*len("!=0.0.0,")) 80 for _, item := range items { 81 v = append(v, '!', '=') 82 v = append(v, item.Version...) 83 v = append(v, ',') 84 } 85 86 return unsafe.String(unsafe.SliceData(v), len(v)-1) 87 } 88 89 func getItems(ctx context.Context, c *Config, releases []*source.Release) ([]*Item, error) { 90 var items []*Item 91 for _, release := range releases { 92 item, err := getReleaseItems(ctx, c, release) 93 if err != nil { 94 return nil, err 95 } 96 items = append(items, item...) 97 } 98 return items, nil 99 } 100 101 //nolint:funlen 102 func getReleaseItems(ctx context.Context, c *Config, release *source.Release) ([]*Item, error) { 103 var description *CdataString 104 if release.Description != "" { 105 desc := string(blackfriday.Run([]byte(release.Description))) 106 desc = strings.TrimSpace(xmlfmt.FormatXML(desc, "\t\t\t\t", "\t")) 107 description = &CdataString{Value: "\n\t\t\t\t" + desc + "\n\t\t\t"} 108 } 109 110 items := make([]*Item, 0, len(release.Assets)) 111 for _, asset := range release.Assets { 112 detect := c.DetectOS 113 if detect == nil { 114 detect = detectOS 115 } 116 os := detect(asset.Name) 117 if os == Unknown { 118 continue 119 } 120 121 opt, err := getSettings(c.Settings, release.Version, os) 122 if err != nil { 123 return nil, err 124 } 125 126 var b []byte 127 if (os == MacOS && c.Ed25519Key != nil) || c.DSAKey != nil || c.UploadPackages { 128 b, err = c.Source.DownloadAsset(ctx, release.Version, asset.Name) 129 if err != nil { 130 return nil, err 131 } 132 } 133 134 edSig, dsaSig, err := signAsset(c, os, b) 135 if err != nil { 136 return nil, err 137 } 138 139 url := asset.URL 140 if c.UploadPackages { 141 url, err = uploadAsset(ctx, c, release.Version+"/"+asset.Name, b) 142 if err != nil { 143 return nil, err 144 } 145 } 146 147 version := strings.TrimPrefix(release.Version, "v") 148 149 items = append(items, &Item{ 150 Title: release.Name, 151 PubDate: release.Date.UTC().Format(time.RFC1123), 152 Description: description, 153 Version: version, 154 CriticalUpdate: getCriticalUpdate(opt.CriticalUpdateBelowVersion), 155 Tags: getTags(opt.CriticalUpdate), 156 IgnoreSkippedUpgradesBelowVersion: opt.IgnoreSkippedUpgradesBelowVersion, 157 MinimumAutoupdateVersion: opt.MinimumAutoupdateVersion, 158 Enclosure: &Enclosure{ 159 Version: version, 160 URL: url, 161 InstallerArguments: opt.InstallerArguments, 162 MinimumSystemVersion: opt.MinimumSystemVersion, 163 Type: getFileType(asset.Name), 164 OS: os.String(), 165 Length: asset.Size, 166 DSASignature: dsaSig, 167 EDSignature: edSig, 168 }, 169 }) 170 } 171 172 return items, nil 173 } 174 175 //nolint:nonamedreturns 176 func signAsset(c *Config, os OS, b []byte) (edSig, dsaSig string, err error) { 177 if os == MacOS && c.Ed25519Key != nil { 178 sig, err := ed25519.Sign(c.Ed25519Key, b) 179 if err != nil { 180 return "", "", err 181 } 182 edSig = base64.StdEncoding.EncodeToString(sig) 183 } else if c.DSAKey != nil { 184 sum := sha1.Sum(b) 185 sig, err := dsa.Sign(c.DSAKey, sum[:]) 186 if err != nil { 187 return "", "", err 188 } 189 dsaSig = base64.StdEncoding.EncodeToString(sig) 190 } 191 return 192 } 193 194 func uploadAsset(ctx context.Context, c *Config, name string, b []byte) (string, error) { 195 w, err := c.Target.NewWriter(ctx, name) 196 if err != nil { 197 return "", err 198 } 199 if _, err := w.Write(b); err != nil { 200 return "", err 201 } 202 if err = w.Close(); err != nil { 203 return "", err 204 } 205 return c.Target.URL(ctx, name) 206 } 207 208 func getCriticalUpdate(version string) *CriticalUpdate { 209 if version != "" { 210 return &CriticalUpdate{Version: version} 211 } 212 return nil 213 } 214 215 func getTags(critical bool) *Tags { 216 if critical { 217 return &Tags{CriticalUpdate: true} 218 } 219 return nil 220 } 221 222 func getFileType(s string) string { 223 ext := path.Ext(s) 224 switch ext { 225 default: 226 return "application/octet-stream" 227 case ".pkg", ".mpkg": 228 return "application/vnd.apple.installer+xml" 229 case ".dmg": 230 return "application/x-apple-diskimage" 231 case ".msi": 232 return "application/x-msi" 233 case ".exe": 234 return "application/vnd.microsoft.portable-executable" 235 236 // See https://learn.microsoft.com/en-us/windows/msix/app-installer/web-install-iis#step-7---configure-the-web-app-for-app-package-mime-types 237 case ".msix", ".msixbundle", ".appx", ".appxbundle", ".appinstaller": 238 return "application/" + ext[1:] 239 } 240 } 241 242 //nolint:gochecknoglobals 243 var replacer = strings.NewReplacer("></sparkle:criticalUpdate>", " />", "></enclosure>", " />") 244 245 func write(ctx context.Context, c *Config, rss *RSS) error { 246 w, err := c.Target.NewWriter(ctx, c.FileName) 247 if err != nil { 248 return err 249 } 250 251 _, err = w.Write([]byte(xml.Header)) 252 if err != nil { 253 return err 254 } 255 256 b, err := xml.MarshalIndent(rss, "", "\t") 257 if err != nil { 258 return err 259 } 260 261 _, err = replacer.WriteString(w, string(b)) 262 if err != nil { 263 return err 264 } 265 266 return w.Close() 267 } 268 269 //nolint:gochecknoinits // Don't use carriage return on windows. 270 func init() { 271 xmlfmt.NL = "\n" 272 }