aboutsummaryrefslogtreecommitdiff
path: root/pkgtools.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkgtools.go')
-rw-r--r--pkgtools.go530
1 files changed, 530 insertions, 0 deletions
diff --git a/pkgtools.go b/pkgtools.go
new file mode 100644
index 0000000..350ab6a
--- /dev/null
+++ b/pkgtools.go
@@ -0,0 +1,530 @@
+package pkgtools
+
+import (
+ "archive/tar"
+ "bufio"
+ "bytes"
+ "compress/bzip2"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/xi2/xz"
+)
+
+const (
+ // InstallLockDir is used to prevent install script collisions.
+ InstallLockDir = "/run/lock/pkgtools"
+
+ // PackageInstallPath is the place for special package files.
+ PackageInstallPath = "install"
+ PackageInstallScript = "install/doinst.sh"
+ PackageUninstallScript = "install/douinst.sh"
+ PackageSlackDescFile = "install/slack-desc"
+
+ InstalledScriptPath = "var/lib/pkgtools/scripts"
+ InstalledPackagePath = "var/lib/pkgtools/packages"
+ // ADMDir is the package database directories (packages, scripts).
+ ADMDir = "var/lib/pkgtools"
+ // Is the removed packages/scripts log files.
+ TmpDir = "var/lib/pkgtools/setup/tmp"
+ LogDir = "var/log/pkgtools"
+ InstalledRemovedPackagePath = "var/log/pkgtools/removed_packages"
+ InstalledRemovedScriptsPath = "var/log/pkgtools/removed_scripts"
+ InstalledRemovedUninstallScriptPath = "var/log/pkgtools/removed_uninstall_scripts"
+)
+
+type PackageType string
+
+const (
+ UnsupportedPackageFormat = "unsupported package format"
+ TGZ = "tgz"
+ TBZ = "tbz"
+ TLZ = "tlz"
+ TXZ = "txz"
+)
+
+var SupportedPackageTypes = []string{
+ TGZ,
+ TBZ,
+ TXZ,
+}
+
+func IsSlackwarePkg(fp string) bool {
+ ext := filepath.Ext(filepath.Base(fp))
+ for _, v := range SupportedPackageTypes {
+ if "."+v == ext {
+ return true
+ }
+ }
+ return false
+}
+
+func FileLockPath(dir string, f string) string {
+ return filepath.Join(dir, f) + ".lock"
+}
+
+func TargetPackagePath(root string, pkg string) string {
+ return filepath.Join(root, InstalledPackagePath, pkg)
+}
+
+func TargetRemovedPackagePath(root string, pkg string) string {
+ return filepath.Join(root, InstalledRemovedPackagePath, pkg)
+}
+
+func TargetRemovedUninstalllScriptPath(root string, pkg string) string {
+ return filepath.Join(root, InstalledRemovedUninstallScriptPath, pkg)
+}
+
+func TargetTmpDir(root string) string {
+ return filepath.Join(root, ADMDir, "setup", "tmp")
+}
+
+func TargetPackageInfoPath(root string, pkg string) string {
+ return filepath.Join(root, InstalledPackagePath, pkg)
+}
+
+func TargetScriptPath(root string, pkg string) string {
+ return filepath.Join(root, InstalledScriptPath, pkg)
+}
+
+func NewCompressedFileReader(
+ r io.Reader,
+ format PackageType,
+) (io.Reader, error) {
+ switch format {
+ case TBZ:
+ dec := bzip2.NewReader(r)
+ return dec, nil
+ case TGZ:
+ dec, err := gzip.NewReader(r)
+ return dec, err
+ case TXZ:
+ dec, err := xz.NewReader(r, 0)
+ return dec, err
+ default:
+ return nil, errors.New(UnsupportedPackageFormat)
+ }
+}
+
+func GetFullPackageName(
+ root string,
+ name string,
+) (string, bool, error) {
+ pkgs, err := ListPackages(root)
+ if err != nil {
+ return "", false, err
+ }
+ for _, pkg := range pkgs {
+ v, err := PackageName(pkg)
+ if err != nil {
+ return "", false, err
+ }
+ if v == name {
+ return pkg, true, nil
+ } else if pkg == name {
+ return pkg, true, nil
+ }
+ }
+ return "", false, nil
+}
+
+func GetPackageSoftLinks(root string, pkg string) ([]string, error) {
+ p := filepath.Join(root, InstalledScriptPath, pkg)
+ if ok, _ := IsFile(p); !ok {
+ return nil, nil
+ }
+ links, err := ReadPackageSoftLinks(p)
+ return links, err
+}
+
+func ListPackages(
+ root string,
+) ([]string, error) {
+ entries, err := os.ReadDir(filepath.Join(root, InstalledPackagePath))
+ if err != nil {
+ return nil, err
+ }
+ var pkgs []string
+ for _, entry := range entries {
+ ok, err := IsFile(filepath.Join(root, InstalledPackagePath, entry.Name()))
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ pkgs = append(pkgs, entry.Name())
+ }
+ }
+
+ return pkgs, nil
+}
+
+// Original from Slackware's 'removepkg'
+// sed -n 's,^[ ]*( [ ]*cd[ ]* \(.*\) [ ]*; [ ]*rm [ ]*-rf[ ]* \(.*\) [ ]*)[ ]*$,\1/\2,p' </var/lib/pkgtools/scripts/acl-2.3.1-x86_64-1
+var regExpSoftLinks = regexp.MustCompile("(\\s*cd\\s*(.*)\\s*; rm -rf (.*) )")
+
+func ParsePackageSoftLinks(
+ str string,
+) ([]string, error) {
+ var ret []string
+ matches := regExpSoftLinks.FindAllStringSubmatch(str, -1)
+ for _, m := range matches {
+ if len(m) != 4 {
+ continue
+ }
+ ret = append(ret, filepath.Join(
+ strings.TrimSpace(m[2]),
+ strings.TrimSpace(m[3]),
+ ))
+ }
+ return ret, nil
+}
+
+func ReadPackageSoftLinks(
+ path string,
+) ([]string, error) {
+ pkgScript := filepath.Join(path)
+ f, err := os.Open(pkgScript)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ buf, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+
+ links, err := ParsePackageSoftLinks(string(buf))
+ if err != nil {
+ return nil, errors.Wrap(err, "getting package generated links")
+ }
+ return links, err
+}
+
+type SlackwarePkg struct {
+ Format PackageType
+ file *os.File
+
+ tarR *tar.Reader
+ r io.Reader
+
+ uncompressedSize int64
+ pkgName string
+ pkgInfo PackageInfo
+}
+
+var _ ArchiveReader = (*SlackwarePkg)(nil)
+
+func OpenSlackwarePkg(fp string) (*SlackwarePkg, error) {
+ var pkg SlackwarePkg
+ var err error
+ pkg.Format = PackageType(filepath.Ext(fp)[1:])
+ pkg.file, err = os.Open(fp)
+ if err != nil {
+ return nil, err
+ }
+ r, err := NewCompressedFileReader(
+ pkg.file,
+ pkg.Format,
+ )
+ if err != nil {
+ return nil, err
+ }
+ pkg.tarR = tar.NewReader(r)
+ if err != nil {
+ return nil, err
+ }
+ pkg.r = pkg.tarR
+
+ pkg.pkgName, err = PackageName(fp)
+ if err != nil {
+ return nil, err
+ }
+
+ pkg.pkgInfo.Name = PackageBase(fp)
+ pkg.pkgInfo.PackageLocation = fp
+
+ size, err := FileSize(fp)
+ if err != nil {
+ return nil, err
+ }
+ pkg.pkgInfo.CompressedPackageSize = HumanReadableSize(size)
+
+ return &pkg, nil
+}
+
+func (s *SlackwarePkg) PkgInfo() *PackageInfo {
+ ret := s.pkgInfo
+ ret.UncompressedPackageSize = HumanReadableSize(s.uncompressedSize)
+ return &ret
+}
+
+func (s *SlackwarePkg) Close() error {
+ return s.file.Close()
+}
+
+func (s *SlackwarePkg) Next() (*tar.Header, error) {
+ h, err := s.tarR.Next()
+ if err != nil {
+ return nil, err
+ }
+ s.r = s.tarR
+
+ s.pkgInfo.FileList = append(s.pkgInfo.FileList, h.Name)
+ if h.Name == PackageSlackDescFile {
+ var buf bytes.Buffer
+ _, err := io.Copy(&buf, s.r)
+ if err != nil {
+ return nil, err
+ }
+ s.pkgInfo.PackageDescription = parseSlackDesc(buf.String(), s.pkgName)
+ s.r = bytes.NewReader(buf.Bytes())
+ }
+ return h, nil
+}
+
+func (s *SlackwarePkg) Read(b []byte) (int, error) {
+ n, err := s.r.Read(b)
+ s.uncompressedSize += int64(n)
+ return n, err
+}
+
+// PackageBase is the package's filename without the (supported) extension.
+func PackageBase(fp string) string {
+ filename := filepath.Base(fp)
+ ext := filepath.Ext(filename)
+ for _, v := range SupportedPackageTypes {
+ if "."+v == ext {
+ return filename[:(len(filename) - len(ext))]
+ }
+ }
+ return filename
+}
+
+func PackageName(fp string) (string, error) {
+ base := PackageBase(fp)
+ strs := strings.Split(base, "-")
+ if len(strs) < 4 {
+ return "", errors.Errorf("badly formatted package name '%s'", fp)
+ }
+ name := strings.Join(strs[:(len(strs)-3)], "-")
+ return name, nil
+}
+
+type PackageInfo struct {
+ Name string
+ // TODO: Should be size
+ CompressedPackageSize string
+ UncompressedPackageSize string
+ PackageLocation string
+ PackageDescription string
+ MD5Sum string
+ FileList []string
+}
+
+func changeExt(fp string, ext string) string {
+ oldExt := filepath.Ext(fp)
+ ret := fp[:(len(fp) - len(oldExt))]
+ ret = ret + "." + ext
+ return ret
+}
+
+func parseSlackDesc(str string, shortname string) string {
+ // TODO: What does this do?
+ //echo "PACKAGE DESCRIPTION:" >> $ADM_DIR/packages/$shortname
+ //grep "^$packagebase:" $DESCRIPTION >> $ADM_DIR/packages/$shortname 2> /dev/null
+ //if [ "$shortname" != "$packagebase" ]; then
+ // grep "^$shortname:" $DESCRIPTION >> $ADM_DIR/packages/$shortname 2> /dev/null
+ //fi
+ lines := strings.Split(str, "\n")
+ var kept []string
+ for _, l := range lines {
+ if strings.HasPrefix(l, shortname) {
+ kept = append(kept, l)
+ }
+ }
+ return strings.Join(kept, "\n")
+}
+
+func GetUncompressedSize(fp string) (int64, error) {
+ f, err := os.Open(fp)
+ if err != nil {
+ return 0, err
+ }
+ defer f.Close()
+
+ ext := filepath.Ext(fp)[1:]
+ r, err := NewCompressedFileReader(f, PackageType(ext))
+ if err != nil {
+ return 0, err
+ }
+
+ var size int64
+ var buf [1024]byte
+ for {
+ n, err := r.Read(buf[:])
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return 0, err
+ }
+ size += int64(n)
+ }
+ return size, nil
+}
+
+// Use package name or package base for name
+func GetPackageInfo(root string, pkgName string) (*PackageInfo, error) {
+ infoPath := TargetPackageInfoPath(root, pkgName)
+ info, err := ReadPackageInfo(infoPath)
+ return info, errors.Wrap(err, "getting package information")
+}
+
+func ReadPackageInfo(fp string) (*PackageInfo, error) {
+ f, err := os.Open(fp)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ buf, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+ info, err := ParsePackageInfo(string(buf))
+ if err != nil {
+ return nil, err
+ }
+
+ return info, nil
+}
+
+func ParsePackageInfo(text string) (*PackageInfo, error) {
+ keys := map[string]struct{}{
+ "COMPRESSED PACKAGE SIZE": {},
+ "FILE LIST": {},
+ "MD5SUM": {},
+ "PACKAGE DESCRIPTION": {},
+ "PACKAGE LOCATION": {},
+ "PACKAGE NAME": {},
+ "UNCOMPRESSED PACKAGE SIZE": {},
+ }
+
+ pkg := PackageInfo{}
+ scanner := bufio.NewScanner(bytes.NewBufferString(text))
+ for scanner.Scan() {
+ var k string
+
+ l := scanner.Text()
+ strs := strings.Split(l, ":")
+ if len(strs) != 2 {
+ continue
+ }
+ k = strings.TrimSpace(strs[0])
+ v := strings.TrimSpace(strs[1])
+ _ = keys
+
+ loop:
+ for {
+ switch k {
+ case "PACKAGE NAME":
+ pkg.Name = v
+ case "COMPRESSED PACKAGE SIZE":
+ pkg.CompressedPackageSize = v
+ case "UNCOMPRESSED PACKAGE SIZE":
+ pkg.UncompressedPackageSize = v
+ case "MD5SUM":
+ pkg.MD5Sum = v
+ case "PACKAGE LOCATION":
+ pkg.PackageLocation = v
+ case "PACKAGE DESCRIPTION":
+ for scanner.Scan() {
+ l := scanner.Text()
+ strs := strings.Split(l, ":")
+ if len(strs) == 2 {
+ k = strings.TrimSpace(strs[0])
+ v = strings.TrimSpace(strs[1])
+ if _, ok := keys[k]; ok {
+ break
+ }
+ }
+ if len(pkg.PackageDescription) > 0 {
+ pkg.PackageDescription += "\n"
+ }
+ pkg.PackageDescription += scanner.Text()
+ }
+ continue loop
+ case "FILE LIST":
+ for scanner.Scan() {
+ pkg.FileList = append(pkg.FileList, scanner.Text())
+ }
+ default:
+ return nil, fmt.Errorf("unknown key '%s'", k)
+ }
+ break
+ }
+ }
+ return &pkg, nil
+}
+
+type PackageFormatKey string
+
+type Encoder struct {
+ w io.Writer
+ MD5Sum bool
+}
+
+func NewEncoder(w io.Writer) *Encoder {
+ return &Encoder{w: w}
+}
+
+func (s *Encoder) EnableMD5Sum(arg bool) {
+ s.MD5Sum = arg
+}
+
+func (s *Encoder) Encode(pkg *PackageInfo) error {
+ _, err := s.w.Write([]byte(fmt.Sprintf("PACKAGE NAME: %s\n", pkg.Name)))
+ if err != nil {
+ return err
+ }
+ _, err = s.w.Write([]byte(fmt.Sprintf("COMPRESSED PACKAGE SIZE: %s\n", pkg.CompressedPackageSize)))
+ if err != nil {
+ return err
+ }
+ _, err = s.w.Write([]byte(fmt.Sprintf("UNCOMPRESSED PACKAGE SIZE: %s\n", pkg.UncompressedPackageSize)))
+ if err != nil {
+ return err
+ }
+ _, err = s.w.Write([]byte(fmt.Sprintf("PACKAGE LOCATION: %s\n", pkg.PackageLocation)))
+ if err != nil {
+ return err
+ }
+ if s.MD5Sum {
+ _, err = s.w.Write([]byte(fmt.Sprintf("PACKAGE MD5SUM: %s\n", pkg.MD5Sum)))
+ if err != nil {
+ return err
+ }
+ }
+
+ _, err = s.w.Write([]byte(fmt.Sprintf("PACKAGE DESCRIPTION:\n%s\n", pkg.PackageDescription)))
+ if err != nil {
+ return err
+ }
+ _, err = s.w.Write([]byte("FILE LIST:\n"))
+ if err != nil {
+ return err
+ }
+ for _, f := range pkg.FileList {
+ _, err = s.w.Write([]byte(f + "\n"))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}