golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/untar/untar.go (about)

     1  // Copyright 2017 The Go 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 untar untars a tarball to disk.
     6  package untar
     7  
     8  import (
     9  	"archive/tar"
    10  	"compress/gzip"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"io/fs"
    15  	"log"
    16  	"os"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strings"
    20  	"time"
    21  )
    22  
    23  // TODO(bradfitz): this was copied from x/build/cmd/buildlet/buildlet.go
    24  // but there were some buildlet-specific bits in there, so the code is
    25  // forked for now.  Unfork and add some opts arguments here, so the
    26  // buildlet can use this code somehow.
    27  
    28  // Untar reads the gzip-compressed tar file from r and writes it into dir.
    29  func Untar(r io.Reader, dir string) error {
    30  	return untar(r, dir)
    31  }
    32  
    33  func untar(r io.Reader, dir string) (err error) {
    34  	t0 := time.Now()
    35  	nFiles := 0
    36  	madeDir := map[string]bool{}
    37  	defer func() {
    38  		td := time.Since(t0)
    39  		if err == nil {
    40  			log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
    41  		} else {
    42  			log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
    43  		}
    44  	}()
    45  	zr, err := gzip.NewReader(r)
    46  	if err != nil {
    47  		return fmt.Errorf("requires gzip-compressed body: %v", err)
    48  	}
    49  	tr := tar.NewReader(zr)
    50  	loggedChtimesError := false
    51  	for {
    52  		f, err := tr.Next()
    53  		if err == io.EOF {
    54  			break
    55  		}
    56  		if err != nil {
    57  			log.Printf("tar reading error: %v", err)
    58  			return fmt.Errorf("tar error: %v", err)
    59  		}
    60  		if !validRelPath(f.Name) {
    61  			return fmt.Errorf("tar contained invalid name error %q", f.Name)
    62  		}
    63  		rel := filepath.FromSlash(f.Name)
    64  		abs := filepath.Join(dir, rel)
    65  
    66  		mode := f.FileInfo().Mode()
    67  		switch f.Typeflag {
    68  		case tar.TypeReg:
    69  			// Make the directory. This is redundant because it should
    70  			// already be made by a directory entry in the tar
    71  			// beforehand. Thus, don't check for errors; the next
    72  			// write will fail with the same error.
    73  			dir := filepath.Dir(abs)
    74  			if !madeDir[dir] {
    75  				if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
    76  					return err
    77  				}
    78  				madeDir[dir] = true
    79  			}
    80  			if runtime.GOOS == "darwin" && mode&0111 != 0 {
    81  				// The darwin kernel caches binary signatures
    82  				// and SIGKILLs binaries with mismatched
    83  				// signatures. Overwriting a binary with
    84  				// O_TRUNC does not clear the cache, rendering
    85  				// the new copy unusable. Removing the original
    86  				// file first does clear the cache. See #54132.
    87  				err := os.Remove(abs)
    88  				if err != nil && !errors.Is(err, fs.ErrNotExist) {
    89  					return err
    90  				}
    91  			}
    92  			wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
    93  			if err != nil {
    94  				return err
    95  			}
    96  			n, err := io.Copy(wf, tr)
    97  			if closeErr := wf.Close(); closeErr != nil && err == nil {
    98  				err = closeErr
    99  			}
   100  			if err != nil {
   101  				return fmt.Errorf("error writing to %s: %v", abs, err)
   102  			}
   103  			if n != f.Size {
   104  				return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
   105  			}
   106  			modTime := f.ModTime
   107  			if modTime.After(t0) {
   108  				// Clamp modtimes at system time. See
   109  				// golang.org/issue/19062 when clock on
   110  				// buildlet was behind the gitmirror server
   111  				// doing the git-archive.
   112  				modTime = t0
   113  			}
   114  			if !modTime.IsZero() {
   115  				if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
   116  					// benign error. Gerrit doesn't even set the
   117  					// modtime in these, and we don't end up relying
   118  					// on it anywhere (the gomote push command relies
   119  					// on digests only), so this is a little pointless
   120  					// for now.
   121  					log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
   122  					loggedChtimesError = true // once is enough
   123  				}
   124  			}
   125  			nFiles++
   126  		case tar.TypeDir:
   127  			if err := os.MkdirAll(abs, 0755); err != nil {
   128  				return err
   129  			}
   130  			madeDir[abs] = true
   131  		case tar.TypeXGlobalHeader:
   132  			// git archive generates these. Ignore them.
   133  		default:
   134  			return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
   135  		}
   136  	}
   137  	return nil
   138  }
   139  
   140  func validRelPath(p string) bool {
   141  	if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
   142  		return false
   143  	}
   144  	return true
   145  }