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  }