+++ /dev/null
-package tar
-
-import (
- "archive/tar"
- "fmt"
- "io"
- "os"
- gopath "path"
- fp "path/filepath"
- "strings"
-)
-
-type Extractor struct {
- Path string
- Progress func(int64) int64
-
- // SanitizePathFunc can be provided if you wish to inspect and/or modify the source path
- // returning an error from this function will abort extraction
- SanitizePathFunc func(path string) (saferPath string, userDefined error)
-
- // LinkFunc can be provided for user specified handling of filesystem links
- // returning an error from this function aborts extraction
- LinkFunc func(Link) error
-}
-
-// Link represents a filesystem link where Name is the link's destination path,
-// Target is what the link actually points to,
-// and Root is the extraction root
-type Link struct {
- Root, Name, Target string
-}
-
-func (te *Extractor) Extract(reader io.Reader) error {
- if isNullDevice(te.Path) {
- return nil
- }
-
- tarReader := tar.NewReader(reader)
-
- // Check if the output path already exists, so we know whether we should
- // create our output with that name, or if we should put the output inside
- // a preexisting directory
- rootExists := true
- rootIsDir := false
- if stat, err := os.Stat(te.Path); err != nil && os.IsNotExist(err) {
- rootExists = false
- } else if err != nil {
- return err
- } else if stat.IsDir() {
- rootIsDir = true
- }
-
- // files come recursively in order (i == 0 is root directory)
- for i := 0; ; i++ {
- header, err := tarReader.Next()
- if err != nil && err != io.EOF {
- return err
- }
- if header == nil || err == io.EOF {
- break
- }
-
- switch header.Typeflag {
- case tar.TypeDir:
- if err := te.extractDir(header, i); err != nil {
- return err
- }
- case tar.TypeReg:
- if err := te.extractFile(header, tarReader, i, rootExists, rootIsDir); err != nil {
- return err
- }
- case tar.TypeSymlink:
- if err := te.extractSymlink(header); err != nil {
- return err
- }
- default:
- return fmt.Errorf("unrecognized tar header type: %d", header.Typeflag)
- }
- }
- return nil
-}
-
-// Sanitize sets up the extractor to use built in sanitation functions
-// (Modify paths to be platform legal, symlinks may not escape extraction root)
-// or unsets any previously set sanitation functions on the extractor
-// (no special rules are applied when extracting)
-func (te *Extractor) Sanitize(toggle bool) {
- if toggle {
- te.SanitizePathFunc = sanitizePath
- te.LinkFunc = func(inLink Link) error {
- if err := childrenOnly(inLink); err != nil {
- return err
- }
- if err := platformLink(inLink); err != nil {
- return err
- }
- return os.Symlink(inLink.Target, inLink.Name)
- }
- } else {
- te.SanitizePathFunc = nil
- te.LinkFunc = nil
- }
-}
-
-// outputPath returns the path at which to place tarPath
-func (te *Extractor) outputPath(tarPath string) (outPath string, err error) {
- elems := strings.Split(tarPath, "/") // break into elems
- elems = elems[1:] // remove original root
- outPath = strings.Join(elems, "/") // join elems
- outPath = gopath.Join(te.Path, outPath) // rebase on to extraction target root
- // sanitize path to be platform legal
- if te.SanitizePathFunc != nil {
- outPath, err = te.SanitizePathFunc(outPath)
- } else {
- outPath = fp.FromSlash(outPath)
- }
- return
-}
-
-func (te *Extractor) extractDir(h *tar.Header, depth int) error {
- path, err := te.outputPath(h.Name)
- if err != nil {
- return err
- }
-
- if depth == 0 {
- // if this is the root directory, use it as the output path for remaining files
- te.Path = path
- }
-
- return os.MkdirAll(path, 0755)
-}
-
-func (te *Extractor) extractSymlink(h *tar.Header) error {
- path, err := te.outputPath(h.Name)
- if err != nil {
- return err
- }
-
- if te.LinkFunc != nil {
- return te.LinkFunc(Link{Root: te.Path, Name: h.Name, Target: h.Linkname})
- }
-
- return os.Symlink(h.Linkname, path)
-}
-
-func (te *Extractor) extractFile(h *tar.Header, r *tar.Reader, depth int, rootExists bool, rootIsDir bool) error {
- path, err := te.outputPath(h.Name)
- if err != nil {
- return err
- }
-
- if depth == 0 { // if depth is 0, this is the only file (we aren't extracting a directory)
- if rootExists && rootIsDir {
- // putting file inside of a root dir.
- fnameo := gopath.Base(h.Name)
- fnamen := fp.Base(path)
- // add back original name if lost.
- if fnameo != fnamen {
- path = fp.Join(path, fnameo)
- }
- } // else if old file exists, just overwrite it.
- }
-
- file, err := os.Create(path)
- if err != nil {
- return err
- }
- defer file.Close()
-
- return copyWithProgress(file, r, te.Progress)
-}
-
-func copyWithProgress(to io.Writer, from io.Reader, cb func(int64) int64) error {
- buf := make([]byte, 4096)
- for {
- n, err := from.Read(buf)
- if n != 0 {
- if cb != nil {
- cb(int64(n))
- }
- _, err2 := to.Write(buf[:n])
- if err2 != nil {
- return err2
- }
- }
- if err != nil {
- if err == io.EOF {
- return nil
- }
- return err
- }
- }
-}
-
-// childrenOnly will return an error if link targets escape their root
-func childrenOnly(inLink Link) error {
- if fp.IsAbs(inLink.Target) {
- return fmt.Errorf("Link target %q is an absolute path (forbidden)", inLink.Target)
- }
-
- resolvedTarget := fp.Join(inLink.Name, inLink.Target)
- rel, err := fp.Rel(inLink.Root, resolvedTarget)
- if err != nil {
- return err
- }
- //disallow symlinks from climbing out of the target root
- if strings.HasPrefix(rel, "..") {
- return fmt.Errorf("Symlink target %q escapes root %q", inLink.Target, inLink.Root)
- }
- //disallow pointing to your own root from above as well
- if strings.HasPrefix(resolvedTarget, inLink.Root) {
- return fmt.Errorf("Symlink target %q escapes and re-enters its own root %q (forbidden)", inLink.Target, inLink.Root)
- }
-
- return nil
-}