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  }