package pkgtools import ( "archive/tar" "fmt" "io" "os" "path" "path/filepath" "github.com/pkg/errors" "golang.org/x/sys/unix" ) type TarReader interface { Next() (*tar.Header, error) Read([]byte) (int, error) } const NewDirMod = 0755 func tarCreateBlockDev(target string, header *tar.Header) error { mode := uint32(header.Mode & 07777) mode |= unix.S_IFBLK dev := unix.Mkdev(uint32(header.Devmajor), uint32(header.Devminor)) err := unix.Mknod(target, mode, int(dev)) return err } func tarCreateCharDev(target string, header *tar.Header) error { mode := uint32(header.Mode & 07777) mode |= unix.S_IFCHR dev := unix.Mkdev(uint32(header.Devmajor), uint32(header.Devminor)) err := unix.Mknod(target, mode, int(dev)) return err } func tarCreateDir(target string) error { err := os.MkdirAll(target, NewDirMod) return err } func tarCreateFifo(target string, header *tar.Header) error { mode := uint32(header.Mode & 07777) mode |= unix.S_IFIFO dev := unix.Mkdev(uint32(header.Devmajor), uint32(header.Devminor)) err := unix.Mknod(target, mode, int(dev)) return err } func tarCreateReg(target string, r io.Reader, header *tar.Header) error { f, err := os.OpenFile( target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode), ) if err != nil { // it could be an error due to writing over a running executable. err = os.Remove(target) if err != nil { return err } f, err = os.OpenFile( target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode), ) if err != nil { return err } } defer f.Close() // copy over contents if _, err := io.Copy(f, r); err != nil { return err } return nil } func tarCreateLink(root string, header *tar.Header) error { oldName := filepath.Join(root, header.Linkname) if filepath.IsAbs(header.Linkname) { filepath.Join(root, oldName) } newName := filepath.Join(root, header.Name) err := os.Link(oldName, newName) if err != nil { _ = os.Remove(newName) err = os.Link(oldName, newName) } return err } func tarCreateSymlink(root string, header *tar.Header) error { oldName := filepath.Join(root, header.Linkname) if filepath.IsAbs(header.Linkname) { filepath.Join(root, oldName) } newName := filepath.Join(root, header.Name) err := os.Symlink(oldName, newName) if err != nil { _ = os.Remove(newName) err = os.Symlink(oldName, newName) } return err } type TarFilter interface { FilterTar(header *tar.Header, r io.Reader) error } type TarFilterFunc func( header *tar.Header, r io.Reader, ) error func (f TarFilterFunc) FilterTar(h *tar.Header, r io.Reader) error { return f(h, r) } type TarCfg struct { Root string Chown bool Chmod bool // Strict bails out when anything unexpected is encountered. Strict bool } type TarExtracter struct { cfg *TarCfg } func NewTarExtractor(cfg *TarCfg) *TarExtracter { return &TarExtracter{cfg} } func (s *TarExtracter) FilterTar( header *tar.Header, r io.Reader, ) error { target := path.Join(s.cfg.Root, header.Name) if s.cfg.Strict { if len(header.PAXRecords) > 0 { return errors.Errorf( "%s: contains unsupported PAXRecords, bailing out", header.Name, ) } if len(header.Xattrs) > 0 { return errors.Errorf( "%s: contains unsupported extended attributes, bailing out", header.Name, ) } } var err error switch header.Typeflag { case tar.TypeBlock: err = tarCreateBlockDev(target, header) case tar.TypeChar: err = tarCreateCharDev(target, header) case tar.TypeFifo: err = tarCreateFifo(target, header) case tar.TypeDir: err = tarCreateDir(target) case tar.TypeReg: err = tarCreateReg(target, r, header) case tar.TypeLink: err = tarCreateLink(s.cfg.Root, header) case tar.TypeSymlink: err = tarCreateSymlink(s.cfg.Root, header) default: err = errors.Errorf("unhandled file type '%c'", header.Typeflag) } if err != nil { return errors.Wrap(err, "unpacking tar archive") } if s.cfg.Chown { if err = os.Chown(target, header.Uid, header.Gid); err != nil { fmt.Fprintln(os.Stderr, err) } } if s.cfg.Chmod { if err = os.Chmod(target, header.FileInfo().Mode()); err != nil { fmt.Fprintln(os.Stderr, err) } } return nil } func FilterTar( r TarReader, filters ...TarFilter, ) error { for { header, err := r.Next() if err == io.EOF { break } else if err != nil { return err } if header == nil { continue } for _, f := range filters { // TODO: how do we dup r? err := f.FilterTar(header, r) if err != nil { return err } } } return nil } func ExtractTar( cfg *TarCfg, fp string, ) error { f, err := os.Open(fp) if err != nil { return err } defer f.Close() r := tar.NewReader(f) err = FilterTar(r, NewTarExtractor(cfg)) return errors.Wrap(err, "extracting tar file") }