From 5d4066dd1e90fa704b67384327af72b3ec121102 Mon Sep 17 00:00:00 2001
From: Slack Coder <slackcoder@server.ky>
Date: Sat, 23 Apr 2022 09:46:41 -0500
Subject: installpkg: Avoid using /install on target dir

Use a temporary randomly named directory named like 'installpkg-2052825695'
for the package's metadata in its '/install' directory.  It will help allow
running multiple instances on the same target root directory by removing
a point of conflict.

Attempt to place this directory in the host's default temporary directory.  If
it does not exist, use the host's root path.
---
 archive.go    | 124 +++++++++++++++++++++++++++++++++++++++++-----------------
 filesystem.go |  15 +++++++
 installpkg.go |  88 +++++++++++++++++++++++++++++------------
 pkgtools.go   |   2 +-
 4 files changed, 167 insertions(+), 62 deletions(-)

diff --git a/archive.go b/archive.go
index fa1886f..6610936 100644
--- a/archive.go
+++ b/archive.go
@@ -12,7 +12,7 @@ import (
 	"golang.org/x/sys/unix"
 )
 
-type ArchiveReader interface {
+type TarReader interface {
 	Next() (*tar.Header, error)
 	Read([]byte) (int, error)
 }
@@ -107,15 +107,78 @@ func tarCreateSymlink(root string, header *tar.Header) error {
 	return err
 }
 
-type InstallArchiveCfg struct {
+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
 }
 
-func InstallArchive(
-	r ArchiveReader,
-	cfg *InstallArchiveCfg,
+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)
+
+	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()
@@ -127,40 +190,29 @@ func InstallArchive(
 		if header == nil {
 			continue
 		}
-		target := path.Join(cfg.Root, header.Name)
-
-		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(cfg.Root, header)
-		case tar.TypeSymlink:
-			err = tarCreateSymlink(cfg.Root, header)
-		default:
-			err = errors.Errorf("unhandled file type '%c'", header.Typeflag)
-		}
-		if err != nil {
-			return errors.Wrap(err, "unpacking tar archive")
-		}
-		if cfg.Chown {
-			if err = os.Chown(target, header.Uid, header.Gid); err != nil {
-				fmt.Fprintln(os.Stderr, err)
-			}
-		}
-		if cfg.Chmod {
-			if err = os.Chmod(target, header.FileInfo().Mode()); err != nil {
-				fmt.Fprintln(os.Stderr, err)
+
+		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")
+}
diff --git a/filesystem.go b/filesystem.go
index c33df15..dec07f3 100644
--- a/filesystem.go
+++ b/filesystem.go
@@ -4,7 +4,9 @@ import (
 	"io/fs"
 	"math"
 	"os"
+	"path/filepath"
 	"strconv"
+	"strings"
 	"time"
 )
 
@@ -46,6 +48,19 @@ func IsDir(path string) (bool, error) {
 	return info.IsDir(), nil
 }
 
+func IsParentDir(parent, sub string) (bool, error) {
+	up := ".." + string(os.PathSeparator)
+
+	rel, err := filepath.Rel(parent, sub)
+	if err != nil {
+		return false, err
+	}
+	if !strings.HasPrefix(rel, up) && rel != ".." {
+		return true, nil
+	}
+	return false, nil
+}
+
 func IsSymlink(path string) (bool, error) {
 	info, err := os.Stat(path)
 	if err != nil {
diff --git a/installpkg.go b/installpkg.go
index 7354ff0..7e10e61 100644
--- a/installpkg.go
+++ b/installpkg.go
@@ -1,12 +1,14 @@
 package pkgtools
 
 import (
+	"archive/tar"
 	"fmt"
 	"io"
 	"os"
 	"os/exec"
 	"path"
 	"path/filepath"
+	"strings"
 	"time"
 
 	"github.com/juju/fslock"
@@ -99,7 +101,12 @@ func (s *InstallPkgFlags) SetEnvValues() {
 	}
 }
 
-func runInstallScript(flags *InstallPkgFlags) error {
+func runInstallScript(flags *InstallPkgFlags, installPath string) error {
+	installPath, err := filepath.Abs(installPath)
+	if err != nil {
+		return err
+	}
+
 	l := fslock.New(FileLockPath(flags.LockDir, filepath.Base(PackageInstallScript)))
 	if err := l.Lock(); err != nil {
 		return err
@@ -116,7 +123,7 @@ func runInstallScript(flags *InstallPkgFlags) error {
 	}
 	defer os.Chdir(cwd)
 
-	installScript := path.Join(flags.Root, PackageInstallScript)
+	installScript := path.Join(installPath, filepath.Base(PackageInstallScript))
 	if _, err := os.Stat(installScript); !os.IsNotExist(err) {
 		cmd := exec.Command("/bin/bash", installScript)
 		cmd.Stdin = os.Stdout
@@ -287,19 +294,65 @@ func writeToDatabase(target string, pkg string) error {
 	return nil
 }
 
+// ExtractSlackwarePkg unarchives the slackware package into the given
+// directory.  The package metadata, under the archives '/install' path, is put
+// into a temporary directory and returned.
+func ExtractSlackwarePkg(flags *InstallPkgFlags, fp string) (string, error) {
+	slackPkg, err := OpenSlackwarePkg(fp)
+	if err != nil {
+		return "", errors.Wrap(err, "extracting package")
+	}
+	defer slackPkg.Close()
+
+	toRoot := NewTarExtractor(&TarCfg{
+		Root:  flags.Root,
+		Chmod: flags.chmod,
+		Chown: flags.chown,
+	})
+
+	tmpDir := os.TempDir()
+	if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
+		tmpDir = "/"
+	} else if err != nil {
+		return "", errors.Wrap(err, "extracting package")
+	}
+	tempInstallDir, err := os.MkdirTemp(tmpDir, "installpkg-*")
+	if err != nil {
+		return "", errors.Wrap(err, "extracting package")
+	}
+	toTempInstallDir := NewTarExtractor(&TarCfg{
+		Root:  tempInstallDir,
+		Chmod: flags.chmod,
+		Chown: flags.chown,
+	})
+
+	err = FilterTar(
+		slackPkg,
+		TarFilterFunc(func(h *tar.Header, r io.Reader) error {
+			if ok, err := IsParentDir(PackageInstallPath, h.Name); ok {
+				h.Name = strings.TrimPrefix(h.Name, PackageInstallPath+"/")
+				if h.Name == "" {
+					h.Name = "."
+				}
+				return toTempInstallDir.FilterTar(h, r)
+			} else if err != nil {
+				return errors.Wrap(err, "installing package")
+			}
+
+			return toRoot.FilterTar(h, r)
+		}),
+	)
+	return tempInstallDir, errors.Wrap(err, "installing package")
+}
+
 func InstallPkg(flags *InstallPkgFlags, pkgs ...string) error {
-	// Apply default flag values
+	// TODO: Apply default flag values
 
 	err := initializeDirectories(flags)
 	if err != nil {
 		return err
 	}
 
-	installPath := path.Join(flags.Root, PackageInstallPath)
-	if _, err := os.Stat(installPath); !errors.Is(err, os.ErrNotExist) {
-		return errors.Errorf("The '%s' directory exists and would be used by installpkg and removed.  Please consider renaming the directory or using Slackware's installpkg.", installPath)
-	}
-
 	for _, pkg := range pkgs {
 		fmt.Printf("installing %s\n", PackageBase(pkg))
 		err = writeToDatabase(flags.Root, pkg)
@@ -307,22 +360,7 @@ func InstallPkg(flags *InstallPkgFlags, pkgs ...string) error {
 			return errors.Wrap(err, "writing package to database")
 		}
 
-		err = func() error {
-			slackPkg, err := OpenSlackwarePkg(pkg)
-			if err != nil {
-				return err
-			}
-			defer slackPkg.Close()
-			err = InstallArchive(
-				slackPkg,
-				&InstallArchiveCfg{
-					Root:  flags.Root,
-					Chmod: flags.chmod,
-					Chown: flags.chown,
-				},
-			)
-			return errors.Wrap(err, "installing package")
-		}()
+		installPath, err := ExtractSlackwarePkg(flags, pkg)
 		if err != nil {
 			return errors.Wrap(err, "writing package to database")
 		}
@@ -336,7 +374,7 @@ func InstallPkg(flags *InstallPkgFlags, pkgs ...string) error {
 		_ = runLDConfig(flags.LockDir)
 
 		// TODO: command should still run after failed 'install script' execution
-		err = runInstallScript(flags)
+		err = runInstallScript(flags, installPath)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, errors.Wrap(err, "running install script"))
 		}
diff --git a/pkgtools.go b/pkgtools.go
index aaffb75..5c8e928 100644
--- a/pkgtools.go
+++ b/pkgtools.go
@@ -219,7 +219,7 @@ type SlackwarePkg struct {
 	pkgInfo          PackageInfo
 }
 
-var _ ArchiveReader = (*SlackwarePkg)(nil)
+var _ TarReader = (*SlackwarePkg)(nil)
 
 func OpenSlackwarePkg(fp string) (*SlackwarePkg, error) {
 	var pkg SlackwarePkg
-- 
cgit v1.2.3