...

Source file src/sigs.k8s.io/release-utils/tar/tar.go

Documentation: sigs.k8s.io/release-utils/tar

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tar
    18  
    19  import (
    20  	"archive/tar"
    21  	"compress/gzip"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  
    29  	"github.com/sirupsen/logrus"
    30  )
    31  
    32  // Compress the provided  `tarContentsPath` into the `tarFilePath` while
    33  // excluding the `exclude` regular expression patterns. This function will
    34  // preserve path between `tarFilePath` and `tarContentsPath` directories inside
    35  // the archive (see `CompressWithoutPreservingPath` as an alternative).
    36  func Compress(tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error {
    37  	return compress(true, tarFilePath, tarContentsPath, excludes...)
    38  }
    39  
    40  // Compress the provided  `tarContentsPath` into the `tarFilePath` while
    41  // excluding the `exclude` regular expression patterns. This function will
    42  // not preserve path leading to the `tarContentsPath` directory in the archive.
    43  func CompressWithoutPreservingPath(tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error {
    44  	return compress(false, tarFilePath, tarContentsPath, excludes...)
    45  }
    46  
    47  func compress(preserveRootDirStructure bool, tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error {
    48  	tarFile, err := os.Create(tarFilePath)
    49  	if err != nil {
    50  		return fmt.Errorf("create tar file %q: %w", tarFilePath, err)
    51  	}
    52  	defer tarFile.Close()
    53  
    54  	gzipWriter := gzip.NewWriter(tarFile)
    55  	defer gzipWriter.Close()
    56  
    57  	tarWriter := tar.NewWriter(gzipWriter)
    58  	defer tarWriter.Close()
    59  
    60  	if err := filepath.Walk(tarContentsPath, func(filePath string, fileInfo os.FileInfo, err error) error {
    61  		if err != nil {
    62  			return err
    63  		}
    64  
    65  		var link string
    66  		isLink := fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink
    67  		if isLink {
    68  			link, err = os.Readlink(filePath)
    69  			if err != nil {
    70  				return fmt.Errorf("read file link of %s: %w", filePath, err)
    71  			}
    72  		}
    73  
    74  		header, err := tar.FileInfoHeader(fileInfo, link)
    75  		if err != nil {
    76  			return fmt.Errorf("create file info header for %q: %w", filePath, err)
    77  		}
    78  
    79  		if fileInfo.IsDir() || filePath == tarFilePath {
    80  			logrus.Tracef("Skipping: %s", filePath)
    81  			return nil
    82  		}
    83  
    84  		for _, re := range excludes {
    85  			if re != nil && re.MatchString(filePath) {
    86  				logrus.Tracef("Excluding: %s", filePath)
    87  				return nil
    88  			}
    89  		}
    90  
    91  		// Make the path inside the tar relative to the archive path if
    92  		// necessary.
    93  		//
    94  		// The default way this works is that we preserve the path between
    95  		// `tarFilePath` and `tarContentsPath` directories inside the archive.
    96  		// This might not work well if `tarFilePath` and `tarContentsPath`
    97  		// are on different levels in the file system (e.g. they don't have
    98  		// common parent directory).
    99  		// In such case we can disable `preserveRootDirStructure` flag which
   100  		// will make paths inside the archive relative to `tarContentsPath`.
   101  		dropPath := filepath.Dir(tarFilePath)
   102  		if !preserveRootDirStructure {
   103  			dropPath = tarContentsPath
   104  		}
   105  		header.Name = strings.TrimLeft(
   106  			strings.TrimPrefix(filePath, dropPath),
   107  			string(filepath.Separator),
   108  		)
   109  		header.Linkname = filepath.ToSlash(header.Linkname)
   110  
   111  		if err := tarWriter.WriteHeader(header); err != nil {
   112  			return fmt.Errorf("writing tar header: %w", err)
   113  		}
   114  
   115  		if !isLink {
   116  			file, err := os.Open(filePath)
   117  			if err != nil {
   118  				return fmt.Errorf("open file %q: %w", filePath, err)
   119  			}
   120  
   121  			if _, err := io.Copy(tarWriter, file); err != nil {
   122  				return fmt.Errorf("writing file to tar writer: %w", err)
   123  			}
   124  
   125  			file.Close()
   126  		}
   127  
   128  		return nil
   129  	}); err != nil {
   130  		return fmt.Errorf("walking tree in %q: %w", tarContentsPath, err)
   131  	}
   132  
   133  	return nil
   134  }
   135  
   136  // Extract can be used to extract the provided `tarFilePath` into the
   137  // `destinationPath`.
   138  func Extract(tarFilePath, destinationPath string) error {
   139  	return iterateTarball(
   140  		tarFilePath,
   141  		func(reader *tar.Reader, header *tar.Header) (stop bool, err error) {
   142  			switch header.Typeflag {
   143  			case tar.TypeDir:
   144  				targetDir, err := SanitizeArchivePath(destinationPath, header.Name)
   145  				if err != nil {
   146  					return false, fmt.Errorf("SanitizeArchivePath: %w", err)
   147  				}
   148  				logrus.Tracef("Creating directory %s", targetDir)
   149  				if err := os.MkdirAll(targetDir, os.FileMode(0o755)); err != nil {
   150  					return false, fmt.Errorf("create target directory: %w", err)
   151  				}
   152  			case tar.TypeSymlink:
   153  				targetFile, err := SanitizeArchivePath(destinationPath, header.Name)
   154  				if err != nil {
   155  					return false, fmt.Errorf("SanitizeArchivePath: %w", err)
   156  				}
   157  				logrus.Tracef(
   158  					"Creating symlink %s -> %s", header.Linkname, targetFile,
   159  				)
   160  				if err := os.MkdirAll(
   161  					filepath.Dir(targetFile), os.FileMode(0o755),
   162  				); err != nil {
   163  					return false, fmt.Errorf("create target directory: %w", err)
   164  				}
   165  				if err := os.Symlink(header.Linkname, targetFile); err != nil {
   166  					return false, fmt.Errorf("create symlink: %w", err)
   167  				}
   168  				// tar.TypeRegA has been deprecated since Go 1.11
   169  				// should we just remove?
   170  			case tar.TypeReg:
   171  				targetFile, err := SanitizeArchivePath(destinationPath, header.Name)
   172  				if err != nil {
   173  					return false, fmt.Errorf("SanitizeArchivePath: %w", err)
   174  				}
   175  				logrus.Tracef("Creating file %s", targetFile)
   176  
   177  				if err := os.MkdirAll(
   178  					filepath.Dir(targetFile), os.FileMode(0o755),
   179  				); err != nil {
   180  					return false, fmt.Errorf("create target directory: %w", err)
   181  				}
   182  
   183  				outFile, err := os.Create(targetFile)
   184  				if err != nil {
   185  					return false, fmt.Errorf("create target file: %w", err)
   186  				}
   187  				if err := outFile.Chmod(os.FileMode(header.Mode)); err != nil {
   188  					return false, fmt.Errorf("chmod target file: %w", err)
   189  				}
   190  
   191  				if _, err := io.Copy(outFile, reader); err != nil {
   192  					return false, fmt.Errorf("copy file contents %s: %w", targetFile, err)
   193  				}
   194  				outFile.Close()
   195  
   196  			default:
   197  				logrus.Warnf(
   198  					"File %s has unknown type %s",
   199  					header.Name, string(header.Typeflag),
   200  				)
   201  			}
   202  
   203  			return false, nil
   204  		},
   205  	)
   206  }
   207  
   208  // Sanitize archive file pathing from "G305: Zip Slip vulnerability"
   209  // https://security.snyk.io/research/zip-slip-vulnerability
   210  func SanitizeArchivePath(d, t string) (v string, err error) {
   211  	v = filepath.Join(d, t)
   212  	if strings.HasPrefix(v, filepath.Clean(d)) {
   213  		return v, nil
   214  	}
   215  
   216  	return "", fmt.Errorf("%s: %s", "content filepath is tainted", t)
   217  }
   218  
   219  // ReadFileFromGzippedTar opens a tarball and reads contents of a file inside.
   220  func ReadFileFromGzippedTar(
   221  	tarPath, filePath string,
   222  ) (res io.Reader, err error) {
   223  	if err := iterateTarball(
   224  		tarPath,
   225  		func(reader *tar.Reader, header *tar.Header) (stop bool, err error) {
   226  			if header.Name == filePath {
   227  				res = reader
   228  				return true, nil
   229  			}
   230  			return false, nil
   231  		},
   232  	); err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	if res == nil {
   237  		return nil, fmt.Errorf("unable to find file %q in tarball %q: %w", tarPath, filePath, err)
   238  	}
   239  
   240  	return res, nil
   241  }
   242  
   243  // iterateTarball can be used to iterate over the contents of a tarball by
   244  // calling the callback for each entry.
   245  func iterateTarball(
   246  	tarPath string,
   247  	callback func(*tar.Reader, *tar.Header) (stop bool, err error),
   248  ) error {
   249  	file, err := os.Open(tarPath)
   250  	if err != nil {
   251  		return fmt.Errorf("opening tar file %q: %w", tarPath, err)
   252  	}
   253  
   254  	gzipReader, err := gzip.NewReader(file)
   255  	if err != nil {
   256  		return fmt.Errorf("creating gzip reader for file %q: %w", tarPath, err)
   257  	}
   258  	tarReader := tar.NewReader(gzipReader)
   259  
   260  	for {
   261  		tarHeader, err := tarReader.Next()
   262  		if err == io.EOF {
   263  			break // End of archive
   264  		}
   265  
   266  		stop, err := callback(tarReader, tarHeader)
   267  		if err != nil {
   268  			return err
   269  		}
   270  		if stop {
   271  			// User wants to stop
   272  			break
   273  		}
   274  	}
   275  
   276  	return nil
   277  }
   278  

View as plain text