/* Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // This is yoinked from https://github.com/golang/build/blob/master/internal/untar/untar.go // which is unfortunately an internal package we can't import, but it's exactly what we need. // So just copy paste it. package main 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 }