22package update
33
44import (
5+ "archive/tar"
6+ "compress/gzip"
57 "crypto/sha256"
68 "encoding/hex"
79 "encoding/json"
@@ -101,6 +103,15 @@ func (u *Updater) DownloadRelease(release *Release) (string, error) {
101103 return "" , fmt .Errorf ("no asset found for %s/%s" , runtime .GOOS , runtime .GOARCH )
102104 }
103105
106+ checksums , err := u .downloadChecksums (release )
107+ if err != nil {
108+ return "" , err
109+ }
110+ expectedChecksum := checksums [asset .Name ]
111+ if expectedChecksum == "" {
112+ return "" , fmt .Errorf ("checksum not found for %s" , asset .Name )
113+ }
114+
104115 req , err := http .NewRequest (http .MethodGet , asset .DownloadURL , nil )
105116 if err != nil {
106117 return "" , fmt .Errorf ("create request: %w" , err )
@@ -138,6 +149,22 @@ func (u *Updater) DownloadRelease(release *Release) (string, error) {
138149 return "" , fmt .Errorf ("size mismatch: expected %d, got %d" , asset .Size , written )
139150 }
140151
152+ actualChecksum := hex .EncodeToString (hasher .Sum (nil ))
153+ if ! strings .EqualFold (actualChecksum , expectedChecksum ) {
154+ os .Remove (tmpFile .Name ())
155+ return "" , fmt .Errorf ("checksum mismatch for %s" , asset .Name )
156+ }
157+
158+ if strings .HasSuffix (strings .ToLower (asset .Name ), ".tar.gz" ) {
159+ path , err := extractArchiveBinary (tmpFile .Name (), u .BinaryName )
160+ if err != nil {
161+ os .Remove (tmpFile .Name ())
162+ return "" , err
163+ }
164+ os .Remove (tmpFile .Name ())
165+ return path , nil
166+ }
167+
141168 return tmpFile .Name (), nil
142169}
143170
@@ -263,12 +290,14 @@ func (u *Updater) findAsset(release *Release) *Asset {
263290 aliases = []string {archName }
264291 }
265292
293+ var fallback * Asset
266294 for i := range release .Assets {
267295 asset := & release .Assets [i ]
268296 name := strings .ToLower (asset .Name )
269297
270298 // Skip checksums and signatures
271- if strings .HasSuffix (name , ".sha256" ) ||
299+ if name == "checksums.txt" ||
300+ strings .HasSuffix (name , ".sha256" ) ||
272301 strings .HasSuffix (name , ".sig" ) ||
273302 strings .HasSuffix (name , ".asc" ) {
274303 continue
@@ -280,12 +309,18 @@ func (u *Updater) findAsset(release *Release) *Asset {
280309
281310 for _ , arch := range aliases {
282311 if strings .Contains (name , arch ) {
283- return asset
312+ if strings .HasSuffix (name , ".tar.gz" ) {
313+ return asset
314+ }
315+ if fallback == nil {
316+ fallback = asset
317+ }
318+ break
284319 }
285320 }
286321 }
287322
288- return nil
323+ return fallback
289324}
290325
291326func (u * Updater ) httpClient () HTTPClient {
@@ -387,3 +422,106 @@ func ComputeChecksum(path string) (string, error) {
387422
388423 return hex .EncodeToString (h .Sum (nil )), nil
389424}
425+
426+ func (u * Updater ) downloadChecksums (release * Release ) (map [string ]string , error ) {
427+ asset := findChecksumsAsset (release )
428+ if asset == nil {
429+ return nil , errors .New ("checksums.txt not found in release assets" )
430+ }
431+
432+ req , err := http .NewRequest (http .MethodGet , asset .DownloadURL , nil )
433+ if err != nil {
434+ return nil , fmt .Errorf ("create checksums request: %w" , err )
435+ }
436+ req .Header .Set ("User-Agent" , fmt .Sprintf ("%s-updater" , u .BinaryName ))
437+
438+ client := u .httpClient ()
439+ resp , err := client .Do (req )
440+ if err != nil {
441+ return nil , fmt .Errorf ("download checksums: %w" , err )
442+ }
443+ defer resp .Body .Close ()
444+
445+ if resp .StatusCode != http .StatusOK {
446+ return nil , fmt .Errorf ("checksums status: %d" , resp .StatusCode )
447+ }
448+
449+ data , err := io .ReadAll (resp .Body )
450+ if err != nil {
451+ return nil , fmt .Errorf ("read checksums: %w" , err )
452+ }
453+
454+ return parseChecksums (data ), nil
455+ }
456+
457+ func findChecksumsAsset (release * Release ) * Asset {
458+ for i := range release .Assets {
459+ asset := & release .Assets [i ]
460+ if strings .EqualFold (asset .Name , "checksums.txt" ) {
461+ return asset
462+ }
463+ }
464+ return nil
465+ }
466+
467+ func parseChecksums (data []byte ) map [string ]string {
468+ out := make (map [string ]string )
469+ lines := strings .Split (string (data ), "\n " )
470+ for _ , line := range lines {
471+ fields := strings .Fields (line )
472+ if len (fields ) < 2 {
473+ continue
474+ }
475+ out [fields [1 ]] = fields [0 ]
476+ }
477+ return out
478+ }
479+
480+ func extractArchiveBinary (archivePath , binaryName string ) (string , error ) {
481+ f , err := os .Open (archivePath )
482+ if err != nil {
483+ return "" , fmt .Errorf ("open archive: %w" , err )
484+ }
485+ defer f .Close ()
486+
487+ gz , err := gzip .NewReader (f )
488+ if err != nil {
489+ return "" , fmt .Errorf ("read gzip: %w" , err )
490+ }
491+ defer gz .Close ()
492+
493+ tr := tar .NewReader (gz )
494+ for {
495+ hdr , err := tr .Next ()
496+ if err == io .EOF {
497+ break
498+ }
499+ if err != nil {
500+ return "" , fmt .Errorf ("read tar: %w" , err )
501+ }
502+ if hdr .Typeflag != tar .TypeReg {
503+ continue
504+ }
505+ if filepath .Base (hdr .Name ) != binaryName {
506+ continue
507+ }
508+
509+ tmpFile , err := os .CreateTemp ("" , fmt .Sprintf ("%s-extract-*" , binaryName ))
510+ if err != nil {
511+ return "" , fmt .Errorf ("create temp file: %w" , err )
512+ }
513+ defer tmpFile .Close ()
514+
515+ if _ , err := io .Copy (tmpFile , tr ); err != nil {
516+ os .Remove (tmpFile .Name ())
517+ return "" , fmt .Errorf ("extract binary: %w" , err )
518+ }
519+ if err := tmpFile .Chmod (0755 ); err != nil {
520+ os .Remove (tmpFile .Name ())
521+ return "" , fmt .Errorf ("chmod extracted binary: %w" , err )
522+ }
523+ return tmpFile .Name (), nil
524+ }
525+
526+ return "" , fmt .Errorf ("binary %s not found in archive" , binaryName )
527+ }
0 commit comments