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 }