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  }