github.com/YousefHaggyHeroku/pack@v1.5.5/internal/archive/archive.go (about)

     1  package archive
     2  
     3  import (
     4  	"archive/tar"
     5  	"archive/zip"
     6  	"bytes"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"time"
    13  
    14  	"github.com/docker/docker/pkg/ioutils"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  var NormalizedDateTime time.Time
    19  
    20  func init() {
    21  	NormalizedDateTime = time.Date(1980, time.January, 1, 0, 0, 1, 0, time.UTC)
    22  }
    23  
    24  type TarWriter interface {
    25  	WriteHeader(hdr *tar.Header) error
    26  	Write(b []byte) (int, error)
    27  	Close() error
    28  }
    29  
    30  type TarWriterFactory interface {
    31  	NewWriter(io.Writer) TarWriter
    32  }
    33  
    34  type defaultTarWriterFactory struct{}
    35  
    36  func DefaultTarWriterFactory() TarWriterFactory {
    37  	return defaultTarWriterFactory{}
    38  }
    39  
    40  func (defaultTarWriterFactory) NewWriter(w io.Writer) TarWriter {
    41  	return tar.NewWriter(w)
    42  }
    43  
    44  func ReadDirAsTar(srcDir, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) io.ReadCloser {
    45  	return GenerateTar(func(tw TarWriter) error {
    46  		return WriteDirToTar(tw, srcDir, basePath, uid, gid, mode, normalizeModTime, fileFilter)
    47  	})
    48  }
    49  
    50  func ReadZipAsTar(srcPath, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) io.ReadCloser {
    51  	return GenerateTar(func(tw TarWriter) error {
    52  		return WriteZipToTar(tw, srcPath, basePath, uid, gid, mode, normalizeModTime, fileFilter)
    53  	})
    54  }
    55  
    56  func GenerateTar(genFn func(TarWriter) error) io.ReadCloser {
    57  	return GenerateTarWithWriter(genFn, DefaultTarWriterFactory())
    58  }
    59  
    60  // GenerateTarWithTar returns a reader to a tar from a generator function using a writer from the provided factory.
    61  // Note that the generator will not fully execute until the reader is fully read from. Any errors returned by the
    62  // generator will be returned when reading the reader.
    63  func GenerateTarWithWriter(genFn func(TarWriter) error, twf TarWriterFactory) io.ReadCloser {
    64  	errChan := make(chan error)
    65  	pr, pw := io.Pipe()
    66  
    67  	go func() {
    68  		tw := twf.NewWriter(pw)
    69  		defer func() {
    70  			if r := recover(); r != nil {
    71  				tw.Close()
    72  				pw.CloseWithError(errors.Errorf("panic: %v", r))
    73  			}
    74  		}()
    75  
    76  		err := genFn(tw)
    77  
    78  		closeErr := tw.Close()
    79  		closeErr = aggregateError(closeErr, pw.CloseWithError(err))
    80  
    81  		errChan <- closeErr
    82  	}()
    83  
    84  	closed := false
    85  	return ioutils.NewReadCloserWrapper(pr, func() error {
    86  		if closed {
    87  			return errors.New("reader already closed")
    88  		}
    89  
    90  		var completeErr error
    91  
    92  		// closing the reader ensures that if anything attempts
    93  		// further reading it doesn't block waiting for content
    94  		if err := pr.Close(); err != nil {
    95  			completeErr = aggregateError(completeErr, err)
    96  		}
    97  
    98  		// wait until everything closes properly
    99  		if err := <-errChan; err != nil {
   100  			completeErr = aggregateError(completeErr, err)
   101  		}
   102  
   103  		closed = true
   104  		return completeErr
   105  	})
   106  }
   107  
   108  func aggregateError(base, addition error) error {
   109  	if addition == nil {
   110  		return base
   111  	}
   112  
   113  	if base == nil {
   114  		return addition
   115  	}
   116  
   117  	return errors.Wrap(addition, base.Error())
   118  }
   119  
   120  func CreateSingleFileTarReader(path, txt string) io.ReadCloser {
   121  	tarBuilder := TarBuilder{}
   122  	tarBuilder.AddFile(path, 0644, NormalizedDateTime, []byte(txt))
   123  	return tarBuilder.Reader(DefaultTarWriterFactory())
   124  }
   125  
   126  func CreateSingleFileTar(tarFile, path, txt string) error {
   127  	tarBuilder := TarBuilder{}
   128  	tarBuilder.AddFile(path, 0644, NormalizedDateTime, []byte(txt))
   129  	return tarBuilder.WriteToPath(tarFile, DefaultTarWriterFactory())
   130  }
   131  
   132  // ErrEntryNotExist is an error returned if an entry path doesn't exist
   133  var ErrEntryNotExist = errors.New("not exist")
   134  
   135  // IsEntryNotExist detects whether a given error is of type ErrEntryNotExist
   136  func IsEntryNotExist(err error) bool {
   137  	return err == ErrEntryNotExist || errors.Cause(err) == ErrEntryNotExist
   138  }
   139  
   140  // ReadTarEntry reads and returns a tar file
   141  func ReadTarEntry(rc io.Reader, entryPath string) (*tar.Header, []byte, error) {
   142  	tr := tar.NewReader(rc)
   143  	for {
   144  		header, err := tr.Next()
   145  		if err == io.EOF {
   146  			break
   147  		}
   148  		if err != nil {
   149  			return nil, nil, errors.Wrap(err, "failed to get next tar entry")
   150  		}
   151  
   152  		if path.Clean(header.Name) == entryPath {
   153  			buf, err := ioutil.ReadAll(tr)
   154  			if err != nil {
   155  				return nil, nil, errors.Wrapf(err, "failed to read contents of '%s'", entryPath)
   156  			}
   157  
   158  			return header, buf, nil
   159  		}
   160  	}
   161  
   162  	return nil, nil, errors.Wrapf(ErrEntryNotExist, "could not find entry path '%s'", entryPath)
   163  }
   164  
   165  // WriteDirToTar writes the contents of a directory to a tar writer. `basePath` is the "location" in the tar the
   166  // contents will be placed.
   167  func WriteDirToTar(tw TarWriter, srcDir, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) error {
   168  	return filepath.Walk(srcDir, func(file string, fi os.FileInfo, err error) error {
   169  		if fileFilter != nil && !fileFilter(file) {
   170  			return nil
   171  		}
   172  		if err != nil {
   173  			return err
   174  		}
   175  
   176  		if fi.Mode()&os.ModeSocket != 0 {
   177  			return nil
   178  		}
   179  
   180  		var header *tar.Header
   181  		if fi.Mode()&os.ModeSymlink != 0 {
   182  			target, err := os.Readlink(file)
   183  			if err != nil {
   184  				return err
   185  			}
   186  
   187  			// Ensure that symlinks have Linux link names, independent of source OS
   188  			header, err = tar.FileInfoHeader(fi, filepath.ToSlash(target))
   189  			if err != nil {
   190  				return err
   191  			}
   192  		} else {
   193  			header, err = tar.FileInfoHeader(fi, fi.Name())
   194  			if err != nil {
   195  				return err
   196  			}
   197  		}
   198  
   199  		relPath, err := filepath.Rel(srcDir, file)
   200  		if err != nil {
   201  			return err
   202  		} else if relPath == "." {
   203  			return nil
   204  		}
   205  
   206  		header.Name = filepath.ToSlash(filepath.Join(basePath, relPath))
   207  		finalizeHeader(header, uid, gid, mode, normalizeModTime)
   208  
   209  		if err := tw.WriteHeader(header); err != nil {
   210  			return err
   211  		}
   212  
   213  		if fi.Mode().IsRegular() {
   214  			f, err := os.Open(file)
   215  			if err != nil {
   216  				return err
   217  			}
   218  			defer f.Close()
   219  
   220  			if _, err := io.Copy(tw, f); err != nil {
   221  				return err
   222  			}
   223  		}
   224  
   225  		return nil
   226  	})
   227  }
   228  
   229  // WriteZipToTar writes the contents of a zip file to a tar writer.
   230  func WriteZipToTar(tw TarWriter, srcZip, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) error {
   231  	zipReader, err := zip.OpenReader(srcZip)
   232  	if err != nil {
   233  		return err
   234  	}
   235  	defer zipReader.Close()
   236  
   237  	var fileMode int64
   238  	for _, f := range zipReader.File {
   239  		if fileFilter != nil && !fileFilter(f.Name) {
   240  			continue
   241  		}
   242  
   243  		fileMode = mode
   244  		if isFatFile(f.FileHeader) {
   245  			fileMode = 0777
   246  		}
   247  
   248  		var header *tar.Header
   249  		if f.Mode()&os.ModeSymlink != 0 {
   250  			target, err := func() (string, error) {
   251  				r, err := f.Open()
   252  				if err != nil {
   253  					return "", nil
   254  				}
   255  				defer r.Close()
   256  
   257  				// contents is the target of the symlink
   258  				target, err := ioutil.ReadAll(r)
   259  				if err != nil {
   260  					return "", err
   261  				}
   262  
   263  				return string(target), nil
   264  			}()
   265  
   266  			if err != nil {
   267  				return err
   268  			}
   269  
   270  			header, err = tar.FileInfoHeader(f.FileInfo(), target)
   271  			if err != nil {
   272  				return err
   273  			}
   274  		} else {
   275  			header, err = tar.FileInfoHeader(f.FileInfo(), f.Name)
   276  			if err != nil {
   277  				return err
   278  			}
   279  		}
   280  
   281  		header.Name = filepath.ToSlash(filepath.Join(basePath, f.Name))
   282  		finalizeHeader(header, uid, gid, fileMode, normalizeModTime)
   283  
   284  		if err := tw.WriteHeader(header); err != nil {
   285  			return err
   286  		}
   287  
   288  		if f.Mode().IsRegular() {
   289  			err := func() error {
   290  				fi, err := f.Open()
   291  				if err != nil {
   292  					return err
   293  				}
   294  				defer fi.Close()
   295  
   296  				_, err = io.Copy(tw, fi)
   297  				return err
   298  			}()
   299  
   300  			if err != nil {
   301  				return err
   302  			}
   303  		}
   304  	}
   305  
   306  	return nil
   307  }
   308  
   309  func isFatFile(header zip.FileHeader) bool {
   310  	var (
   311  		creatorFAT  uint16 = 0
   312  		creatorVFAT uint16 = 14
   313  	)
   314  
   315  	// This identifies FAT files, based on the `zip` source: https://golang.org/src/archive/zip/struct.go
   316  	firstByte := header.CreatorVersion >> 8
   317  	return firstByte == creatorFAT || firstByte == creatorVFAT
   318  }
   319  
   320  func finalizeHeader(header *tar.Header, uid, gid int, mode int64, normalizeModTime bool) {
   321  	NormalizeHeader(header, normalizeModTime)
   322  	if mode != -1 {
   323  		header.Mode = mode
   324  	}
   325  	header.Uid = uid
   326  	header.Gid = gid
   327  }
   328  
   329  // NormalizeHeader normalizes a tar.Header
   330  //
   331  // Normalizes the following:
   332  // 	- ModTime
   333  // 	- GID
   334  // 	- UID
   335  // 	- User Name
   336  // 	- Group Name
   337  func NormalizeHeader(header *tar.Header, normalizeModTime bool) {
   338  	if normalizeModTime {
   339  		header.ModTime = NormalizedDateTime
   340  	}
   341  	header.Uid = 0
   342  	header.Gid = 0
   343  	header.Uname = ""
   344  	header.Gname = ""
   345  }
   346  
   347  // IsZip detects whether or not a File is a zip directory
   348  func IsZip(file io.Reader) (bool, error) {
   349  	b := make([]byte, 4)
   350  	_, err := file.Read(b)
   351  	if err != nil && err != io.EOF {
   352  		return false, err
   353  	} else if err == io.EOF {
   354  		return false, nil
   355  	}
   356  
   357  	return bytes.Equal(b, []byte("\x50\x4B\x03\x04")), nil
   358  }