| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package untar untars a tarball to disk. |
| package untar |
| |
| import ( |
| "archive/tar" |
| "compress/gzip" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "path" |
| "path/filepath" |
| "strings" |
| "time" |
| ) |
| |
| // TODO(bradfitz): this was copied from x/build/cmd/buildlet/buildlet.go |
| // but there were some buildlet-specific bits in there, so the code is |
| // forked for now. Unfork and add some opts arguments here, so the |
| // buildlet can use this code somehow. |
| |
| // Untar reads the gzip-compressed tar file from r and writes it into dir. |
| func Untar(r io.Reader, dir string) error { |
| return untar(r, dir) |
| } |
| |
| func untar(r io.Reader, dir string) (err error) { |
| t0 := time.Now() |
| nFiles := 0 |
| madeDir := map[string]bool{} |
| defer func() { |
| td := time.Since(t0) |
| if err == nil { |
| log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td) |
| } else { |
| log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err) |
| } |
| }() |
| zr, err := gzip.NewReader(r) |
| if err != nil { |
| return fmt.Errorf("requires gzip-compressed body: %v", err) |
| } |
| tr := tar.NewReader(zr) |
| loggedChtimesError := false |
| for { |
| f, err := tr.Next() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| log.Printf("tar reading error: %v", err) |
| return fmt.Errorf("tar error: %v", err) |
| } |
| if !validRelPath(f.Name) { |
| return fmt.Errorf("tar contained invalid name error %q", f.Name) |
| } |
| rel := filepath.FromSlash(f.Name) |
| abs := filepath.Join(dir, rel) |
| |
| fi := f.FileInfo() |
| mode := fi.Mode() |
| switch { |
| case mode.IsRegular(): |
| // Make the directory. This is redundant because it should |
| // already be made by a directory entry in the tar |
| // beforehand. Thus, don't check for errors; the next |
| // write will fail with the same error. |
| dir := filepath.Dir(abs) |
| if !madeDir[dir] { |
| if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { |
| return err |
| } |
| madeDir[dir] = true |
| } |
| wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) |
| if err != nil { |
| return err |
| } |
| n, err := io.Copy(wf, tr) |
| if closeErr := wf.Close(); closeErr != nil && err == nil { |
| err = closeErr |
| } |
| if err != nil { |
| return fmt.Errorf("error writing to %s: %v", abs, err) |
| } |
| if n != f.Size { |
| return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size) |
| } |
| modTime := f.ModTime |
| if modTime.After(t0) { |
| // Clamp modtimes at system time. See |
| // golang.org/issue/19062 when clock on |
| // buildlet was behind the gitmirror server |
| // doing the git-archive. |
| modTime = t0 |
| } |
| if !modTime.IsZero() { |
| if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError { |
| // benign error. Gerrit doesn't even set the |
| // modtime in these, and we don't end up relying |
| // on it anywhere (the gomote push command relies |
| // on digests only), so this is a little pointless |
| // for now. |
| log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err) |
| loggedChtimesError = true // once is enough |
| } |
| } |
| nFiles++ |
| case mode.IsDir(): |
| if err := os.MkdirAll(abs, 0755); err != nil { |
| return err |
| } |
| madeDir[abs] = true |
| default: |
| return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode) |
| } |
| } |
| return nil |
| } |
| |
| func validRelativeDir(dir string) bool { |
| if strings.Contains(dir, `\`) || path.IsAbs(dir) { |
| return false |
| } |
| dir = path.Clean(dir) |
| if strings.HasPrefix(dir, "../") || strings.HasSuffix(dir, "/..") || dir == ".." { |
| return false |
| } |
| return true |
| } |
| |
| func validRelPath(p string) bool { |
| if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") { |
| return false |
| } |
| return true |
| } |