github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/tarutil/tar.go (about)

     1  // Copyright 2019-2021 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package tarutil
     6  
     7  import (
     8  	"archive/tar"
     9  	"bytes"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  
    16  	"github.com/mvdan/u-root-coreutils/pkg/upath"
    17  )
    18  
    19  // Opts contains options for creating and extracting tar files.
    20  type Opts struct {
    21  	// Filters are applied to each file while creating or extracting a tar
    22  	// archive. The filter can modify the tar.Header struct. If the filter
    23  	// returns false, the file is omitted.
    24  	Filters []Filter
    25  
    26  	// By default, when creating a tar archive, all directories are walked
    27  	// to include all sub-directories. Set to true to prevent this
    28  	// behavior.
    29  	NoRecursion bool
    30  
    31  	// Change to this directory before any operations. This is equivalent
    32  	// to "tar -C DIR".
    33  	ChangeDirectory string
    34  }
    35  
    36  // passesFilters returns true if the given file passes all filters, false otherwise.
    37  func passesFilters(hdr *tar.Header, filters []Filter) bool {
    38  	for _, filter := range filters {
    39  		if !filter(hdr) {
    40  			return false
    41  		}
    42  	}
    43  	return true
    44  }
    45  
    46  // applyToArchive applies function f to all files in the given archive
    47  func applyToArchive(tarFile io.Reader, f func(tr *tar.Reader, hdr *tar.Header) error) error {
    48  	tr := tar.NewReader(tarFile)
    49  	for {
    50  		hdr, err := tr.Next()
    51  		if err == io.EOF {
    52  			break
    53  		}
    54  		if err != nil {
    55  			return err
    56  		}
    57  		if err := f(tr, hdr); err != nil {
    58  			return err
    59  		}
    60  	}
    61  	return nil
    62  }
    63  
    64  // ListArchive lists the contents of the given tar archive.
    65  func ListArchive(tarFile io.Reader) error {
    66  	return applyToArchive(tarFile, func(tr *tar.Reader, hdr *tar.Header) error {
    67  		fmt.Println(hdr.Name)
    68  		return nil
    69  	})
    70  }
    71  
    72  // ExtractDir extracts all the contents of the tar file to the given directory.
    73  func ExtractDir(tarFile io.Reader, dir string, opts *Opts) error {
    74  	if opts == nil {
    75  		opts = &Opts{}
    76  	}
    77  
    78  	// Simulate a "cd" to another directory.
    79  	if !filepath.IsAbs(dir) {
    80  		dir = filepath.Join(opts.ChangeDirectory, dir)
    81  	}
    82  
    83  	fi, err := os.Stat(dir)
    84  	if os.IsNotExist(err) {
    85  		if err := os.Mkdir(dir, os.ModePerm); err != nil {
    86  			return fmt.Errorf("could not create directory %s: %v", dir, err)
    87  		}
    88  	} else if err != nil || !fi.IsDir() {
    89  		return fmt.Errorf("could not stat directory %s: %v", dir, err)
    90  	}
    91  
    92  	return applyToArchive(tarFile, func(tr *tar.Reader, hdr *tar.Header) error {
    93  		if !passesFilters(hdr, opts.Filters) {
    94  			return nil
    95  		}
    96  		return createFileInRoot(hdr, tr, dir)
    97  	})
    98  }
    99  
   100  // CreateTar creates a new tar file with all the contents of a directory.
   101  func CreateTar(tarFile io.Writer, files []string, opts *Opts) error {
   102  	if opts == nil {
   103  		opts = &Opts{}
   104  	}
   105  
   106  	tw := tar.NewWriter(tarFile)
   107  	for _, bFile := range files {
   108  		// Simulate a "cd" to another directory. There are 3 parts to
   109  		// the file path:
   110  		// a) The path passed to ChangeDirectory
   111  		// b) The path passed in files
   112  		// c) The path in the current walk
   113  		// I prefixed corresponding a/b/c onto the variable name as an
   114  		// aid. For example abFile is the filepath of a+b.
   115  		abFile := filepath.Join(opts.ChangeDirectory, bFile)
   116  		if filepath.IsAbs(bFile) {
   117  			// "cd" does nothing if the file is absolute.
   118  			abFile = bFile
   119  		}
   120  
   121  		walk := filepath.Walk
   122  		if opts.NoRecursion {
   123  			// This "walk" function does not recurse.
   124  			walk = func(root string, walkFn filepath.WalkFunc) error {
   125  				fi, err := os.Lstat(root)
   126  				return walkFn(root, fi, err)
   127  			}
   128  		}
   129  
   130  		err := walk(abFile, func(abcPath string, info os.FileInfo, err error) error {
   131  			if err != nil {
   132  				return err
   133  			}
   134  
   135  			// The record should not contain the ChangeDirectory
   136  			// path, so we need to derive bc from abc.
   137  			bcPath, err := filepath.Rel(opts.ChangeDirectory, abcPath)
   138  			if err != nil {
   139  				return err
   140  			}
   141  			if filepath.IsAbs(bFile) {
   142  				// "cd" does nothing if the file is absolute.
   143  				bcPath = abcPath
   144  			}
   145  
   146  			var symlink string
   147  			if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   148  				if symlink, err = os.Readlink(abcPath); err != nil {
   149  					return err
   150  				}
   151  			}
   152  			hdr, err := tar.FileInfoHeader(info, symlink)
   153  			if err != nil {
   154  				return err
   155  			}
   156  			hdr.Name = bcPath
   157  			if !passesFilters(hdr, opts.Filters) {
   158  				return nil
   159  			}
   160  			switch hdr.Typeflag {
   161  			case tar.TypeLink, tar.TypeSymlink, tar.TypeChar, tar.TypeBlock, tar.TypeDir, tar.TypeFifo:
   162  				if err := tw.WriteHeader(hdr); err != nil {
   163  					return err
   164  				}
   165  			default:
   166  				f, err := os.Open(abcPath)
   167  				if err != nil {
   168  					return err
   169  				}
   170  
   171  				var r io.Reader = f
   172  				if hdr.Size == 0 {
   173  					// Some files don't report their size correctly
   174  					// (ex: procfs), so we use an intermediary
   175  					// buffer to determine size.
   176  					b := &bytes.Buffer{}
   177  					if _, err := io.Copy(b, f); err != nil {
   178  						f.Close()
   179  						return err
   180  					}
   181  					f.Close()
   182  					hdr.Size = int64(b.Len())
   183  					r = b
   184  				}
   185  
   186  				if err := tw.WriteHeader(hdr); err != nil {
   187  					return err
   188  				}
   189  				if _, err := io.Copy(tw, r); err != nil {
   190  					return err
   191  				}
   192  			}
   193  			return nil
   194  		})
   195  		if err != nil {
   196  			return err
   197  		}
   198  	}
   199  	if err := tw.Close(); err != nil {
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  func createFileInRoot(hdr *tar.Header, r io.Reader, rootDir string) error {
   206  	fi := hdr.FileInfo()
   207  	path, err := upath.SafeFilepathJoin(rootDir, hdr.Name)
   208  	if err != nil {
   209  		// The behavior is to skip files which are unsafe due to
   210  		// zipslip, but continue extracting everything else.
   211  		log.Printf("Warning: Skipping file %q due to: %v", hdr.Name, err)
   212  		return nil
   213  	}
   214  
   215  	switch fi.Mode() & os.ModeType {
   216  	case os.ModeSymlink:
   217  		// TODO: support symlinks
   218  		return fmt.Errorf("symlinks not yet supported: %q", path)
   219  
   220  	case os.FileMode(0):
   221  		f, err := os.Create(path)
   222  		if err != nil {
   223  			return err
   224  		}
   225  		if _, err := io.Copy(f, r); err != nil {
   226  			f.Close()
   227  			return err
   228  		}
   229  		if err := f.Close(); err != nil {
   230  			return err
   231  		}
   232  
   233  	case os.ModeDir:
   234  		if err := os.MkdirAll(path, fi.Mode()&os.ModePerm); err != nil {
   235  			return err
   236  		}
   237  
   238  	case os.ModeDevice:
   239  		// TODO: support block device
   240  		return fmt.Errorf("block device not yet supported: %q", path)
   241  
   242  	case os.ModeCharDevice:
   243  		// TODO: support char device
   244  		return fmt.Errorf("char device not yet supported: %q", path)
   245  
   246  	default:
   247  		return fmt.Errorf("%q: Unknown type %#o", path, fi.Mode()&os.ModeType)
   248  	}
   249  
   250  	if err := os.Chmod(path, fi.Mode()&os.ModePerm); err != nil {
   251  		return fmt.Errorf("error setting mode %#o on %q: %v",
   252  			fi.Mode()&os.ModePerm, path, err)
   253  	}
   254  	// TODO: also set ownership, etc...
   255  	return nil
   256  }
   257  
   258  // Filter is applied to each file while creating or extracting a tar archive.
   259  // The filter can modify the tar.Header struct. If the filter returns false,
   260  // the file is omitted.
   261  type Filter func(hdr *tar.Header) bool
   262  
   263  // NoFilter does not filter or modify any files.
   264  func NoFilter(hdr *tar.Header) bool {
   265  	return true
   266  }
   267  
   268  // VerboseFilter prints the name of every file.
   269  func VerboseFilter(hdr *tar.Header) bool {
   270  	fmt.Println(hdr.Name)
   271  	return true
   272  }
   273  
   274  // VerboseLogFilter logs the name of every file.
   275  func VerboseLogFilter(hdr *tar.Header) bool {
   276  	log.Println(hdr.Name)
   277  	return true
   278  }
   279  
   280  // SafeFilter filters out all files which are not regular and not directories.
   281  // It also sets appropriate permissions.
   282  func SafeFilter(hdr *tar.Header) bool {
   283  	if hdr.Typeflag == tar.TypeDir {
   284  		hdr.Mode = 0o770
   285  		return true
   286  	}
   287  	if hdr.Typeflag == tar.TypeReg {
   288  		hdr.Mode = 0o660
   289  		return true
   290  	}
   291  	return false
   292  }