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' 0 { 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 }