github.com/creativeprojects/go-selfupdate@v1.2.0/decompress.go (about) 1 package selfupdate 2 3 import ( 4 "archive/tar" 5 "archive/zip" 6 "bytes" 7 "compress/bzip2" 8 "compress/gzip" 9 "errors" 10 "fmt" 11 "io" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 "github.com/ulikunitz/xz" 17 ) 18 19 var ( 20 fileTypes = []struct { 21 ext string 22 decompress func(src io.Reader, cmd, os, arch string) (io.Reader, error) 23 }{ 24 {".zip", unzip}, 25 {".tar.gz", untar}, 26 {".tgz", untar}, 27 {".gzip", gunzip}, 28 {".gz", gunzip}, 29 {".tar.xz", untarxz}, 30 {".xz", unxz}, 31 {".bz2", unbz2}, 32 } 33 // pattern copied from bottom of the page: https://semver.org/ 34 semverPattern = `(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?` 35 ) 36 37 // DecompressCommand decompresses the given source. Archive and compression format is 38 // automatically detected from 'url' parameter, which represents the URL of asset, 39 // or simply a filename (with an extension). 40 // This returns a reader for the decompressed command given by 'cmd'. '.zip', 41 // '.tar.gz', '.tar.xz', '.tgz', '.gz', '.bz2' and '.xz' are supported. 42 // 43 // These wrapped errors can be returned: 44 // - ErrCannotDecompressFile 45 // - ErrExecutableNotFoundInArchive 46 func DecompressCommand(src io.Reader, url, cmd, os, arch string) (io.Reader, error) { 47 for _, fileType := range fileTypes { 48 if strings.HasSuffix(url, fileType.ext) { 49 return fileType.decompress(src, cmd, os, arch) 50 } 51 } 52 log.Print("File is not compressed") 53 return src, nil 54 } 55 56 func unzip(src io.Reader, cmd, os, arch string) (io.Reader, error) { 57 log.Print("Decompressing zip file") 58 59 // Zip format requires its file size for Decompressing. 60 // So we need to read the HTTP response into a buffer at first. 61 buf, err := io.ReadAll(src) 62 if err != nil { 63 return nil, fmt.Errorf("%w zip file: %v", ErrCannotDecompressFile, err) 64 } 65 66 r := bytes.NewReader(buf) 67 z, err := zip.NewReader(r, r.Size()) 68 if err != nil { 69 return nil, fmt.Errorf("%w zip file: %s", ErrCannotDecompressFile, err) 70 } 71 72 for _, file := range z.File { 73 _, name := filepath.Split(file.Name) 74 if !file.FileInfo().IsDir() && matchExecutableName(cmd, os, arch, name) { 75 log.Printf("Executable file %q was found in zip archive", file.Name) 76 return file.Open() 77 } 78 } 79 80 return nil, fmt.Errorf("%w in zip file: %q", ErrExecutableNotFoundInArchive, cmd) 81 } 82 83 func untar(src io.Reader, cmd, os, arch string) (io.Reader, error) { 84 log.Print("Decompressing tar.gz file") 85 86 gz, err := gzip.NewReader(src) 87 if err != nil { 88 return nil, fmt.Errorf("%w tar.gz file: %s", ErrCannotDecompressFile, err) 89 } 90 91 return unarchiveTar(gz, cmd, os, arch) 92 } 93 94 func gunzip(src io.Reader, cmd, os, arch string) (io.Reader, error) { 95 log.Print("Decompressing gzip file") 96 97 r, err := gzip.NewReader(src) 98 if err != nil { 99 return nil, fmt.Errorf("%w gzip file: %s", ErrCannotDecompressFile, err) 100 } 101 102 name := r.Header.Name 103 if !matchExecutableName(cmd, os, arch, name) { 104 return nil, fmt.Errorf("%w: expected %q but found %q", ErrExecutableNotFoundInArchive, cmd, name) 105 } 106 107 log.Printf("Executable file %q was found in gzip file", name) 108 return r, nil 109 } 110 111 func untarxz(src io.Reader, cmd, os, arch string) (io.Reader, error) { 112 log.Print("Decompressing tar.xz file") 113 114 xzip, err := xz.NewReader(src) 115 if err != nil { 116 return nil, fmt.Errorf("%w tar.xz file: %s", ErrCannotDecompressFile, err) 117 } 118 119 return unarchiveTar(xzip, cmd, os, arch) 120 } 121 122 func unxz(src io.Reader, cmd, os, arch string) (io.Reader, error) { 123 log.Print("Decompressing xzip file") 124 125 xzip, err := xz.NewReader(src) 126 if err != nil { 127 return nil, fmt.Errorf("%w xzip file: %s", ErrCannotDecompressFile, err) 128 } 129 130 log.Printf("Decompressed file from xzip is assumed to be an executable: %s", cmd) 131 return xzip, nil 132 } 133 134 func unbz2(src io.Reader, cmd, os, arch string) (io.Reader, error) { 135 log.Print("Decompressing bzip2 file") 136 137 bz2 := bzip2.NewReader(src) 138 139 log.Printf("Decompressed file from bzip2 is assumed to be an executable: %s", cmd) 140 return bz2, nil 141 } 142 143 func matchExecutableName(cmd, os, arch, target string) bool { 144 cmd = strings.TrimSuffix(cmd, ".exe") 145 pattern := regexp.MustCompile( 146 fmt.Sprintf( 147 `^%s([_-]%s)?([_-]%s[_-]%s)?(\.exe)?$`, 148 regexp.QuoteMeta(cmd), 149 semverPattern, 150 regexp.QuoteMeta(os), 151 regexp.QuoteMeta(arch), 152 ), 153 ) 154 return pattern.MatchString(target) 155 } 156 157 func unarchiveTar(src io.Reader, cmd, os, arch string) (io.Reader, error) { 158 t := tar.NewReader(src) 159 for { 160 h, err := t.Next() 161 if errors.Is(err, io.EOF) { 162 break 163 } 164 if err != nil { 165 return nil, fmt.Errorf("%w tar file: %s", ErrCannotDecompressFile, err) 166 } 167 _, name := filepath.Split(h.Name) 168 if matchExecutableName(cmd, os, arch, name) { 169 log.Printf("Executable file %q was found in tar archive", h.Name) 170 return t, nil 171 } 172 } 173 return nil, fmt.Errorf("%w in tar: %q", ErrExecutableNotFoundInArchive, cmd) 174 }