Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
217 lines (191 sloc)
5.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } |