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