github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/integrations/yum/repo.go (about)

     1  package yum
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"context"
     7  	"crypto/sha256"
     8  	"encoding/hex"
     9  	"encoding/xml"
    10  	"errors"
    11  	"io/fs"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/cavaliergopher/rpm"
    19  
    20  	"github.com/kubri/kubri/pkg/crypto/pgp"
    21  	"github.com/kubri/kubri/target"
    22  )
    23  
    24  type repo struct {
    25  	primary   *MetaData
    26  	filelists *FileLists
    27  	other     *Other
    28  
    29  	dir   string
    30  	files []string
    31  }
    32  
    33  func openRepo(ctx context.Context, t target.Target) (*repo, error) {
    34  	dir, err := os.MkdirTemp("", "")
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	res := &repo{dir: dir}
    40  
    41  	var r RepoMD
    42  	if err := readXML(ctx, t, "repodata/repomd.xml", &r); err != nil {
    43  		if errors.Is(err, fs.ErrNotExist) {
    44  			res.primary = &MetaData{}
    45  			res.filelists = &FileLists{}
    46  			res.other = &Other{}
    47  			return res, nil
    48  		}
    49  		return nil, err
    50  	}
    51  
    52  	for _, v := range r.Data {
    53  		var r any
    54  		switch v.Type {
    55  		case "primary":
    56  			r = &res.primary
    57  		case "filelists":
    58  			r = &res.filelists
    59  		case "other":
    60  			r = &res.other
    61  		default:
    62  			continue
    63  		}
    64  		if err := readXML(ctx, t, v.Location.HREF, r); err != nil {
    65  			return nil, err
    66  		}
    67  		res.files = append(res.files, v.Location.HREF)
    68  	}
    69  
    70  	if res.primary == nil || res.filelists == nil || res.other == nil {
    71  		return nil, errors.New("invalid repomd.xml")
    72  	}
    73  
    74  	return res, nil
    75  }
    76  
    77  //nolint:funlen
    78  func (r *repo) Add(b []byte) error {
    79  	h, err := rpm.Read(bytes.NewReader(b))
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	id := h.String()
    85  	checksum := sha256.Sum256(b) // TODO: add checksum
    86  	start, end := h.HeaderRange()
    87  	files := h.Files()
    88  
    89  	p := Package{
    90  		Type: "rpm",
    91  		Name: h.Name(),
    92  		Arch: h.Architecture(),
    93  		Version: Version{
    94  			Ver:   h.Version(),
    95  			Rel:   h.Release(),
    96  			Epoch: strconv.Itoa(h.Epoch()),
    97  		},
    98  		Checksum: Checksum{
    99  			Type:  "sha256",
   100  			PkgID: "YES",
   101  			Value: hex.EncodeToString(checksum[:]),
   102  		},
   103  		Summary:     h.Summary(),
   104  		Description: h.Description(),
   105  		Packager:    h.Packager(),
   106  		URL:         h.URL(),
   107  		Time: Time{
   108  			File:  timeNow(),
   109  			Build: int(h.BuildTime().Unix()),
   110  		},
   111  		Size: Size{
   112  			Package:   len(b),
   113  			Archive:   int(h.ArchiveSize()),
   114  			Installed: int(h.Size()),
   115  		},
   116  		Location: Location{
   117  			HREF: "Packages/" + id[0:1] + "/" + id + ".rpm",
   118  		},
   119  		Format: Format{
   120  			License:     h.License(),
   121  			Vendor:      h.Vendor(),
   122  			Group:       h.Groups()[0],
   123  			BuildHost:   h.BuildHost(),
   124  			SourceRPM:   h.SourceRPM(),
   125  			HeaderRange: HeaderRange{Start: start, End: end},
   126  			Provides:    getEntries(h.Provides()),
   127  			Obsoletes:   getEntries(h.Obsoletes()),
   128  			Requires:    getEntries(h.Requires()),
   129  			Conflicts:   getEntries(h.Conflicts()),
   130  			Files:       filterPackageFiles(files),
   131  		},
   132  	}
   133  
   134  	r.primary.Package = append(r.primary.Package, p)
   135  
   136  	r.filelists.Package = append(r.filelists.Package, FileListsPackage{
   137  		Name:    p.Name,
   138  		PkgID:   p.Checksum.Value,
   139  		Arch:    p.Arch,
   140  		Version: p.Version,
   141  		Files:   convertPackageFiles(files),
   142  	})
   143  
   144  	r.other.Package = append(r.other.Package, OtherPackage{
   145  		Name:    p.Name,
   146  		PkgID:   p.Checksum.Value,
   147  		Arch:    p.Arch,
   148  		Version: p.Version,
   149  	})
   150  
   151  	return writeFile(filepath.Join(r.dir, p.Location.HREF), b)
   152  }
   153  
   154  //nolint:funlen
   155  func (r *repo) Write(pgpKey *pgp.PrivateKey) error {
   156  	md := &RepoMD{}
   157  
   158  	data := map[string]any{
   159  		"primary":   r.primary,
   160  		"filelists": r.filelists,
   161  		"other":     r.other,
   162  	}
   163  
   164  	for _, name := range []string{"primary", "filelists", "other"} {
   165  		raw, err := xmlMarshal(data[name])
   166  		if err != nil {
   167  			return err
   168  		}
   169  
   170  		gz, err := compress(raw)
   171  		if err != nil {
   172  			return err
   173  		}
   174  
   175  		var d Data
   176  		d.Type = name
   177  		d.Checksum = getChecksum(gz)
   178  		d.OpenChecksum = getChecksum(raw)
   179  		d.Location.HREF = "repodata/" + d.Checksum.Value + "-" + name + ".xml.gz"
   180  		d.Timestamp = timeNow()
   181  		d.Size = len(gz)
   182  		d.OpenSize = len(raw)
   183  
   184  		err = writeFile(filepath.Join(r.dir, d.Location.HREF), gz)
   185  		if err != nil {
   186  			return err
   187  		}
   188  
   189  		md.Data = append(md.Data, d)
   190  	}
   191  
   192  	md.Revision = timeNow()
   193  	filename := filepath.Join(r.dir, "repodata/repomd.xml")
   194  
   195  	b, err := xmlMarshal(md)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	if err = writeFile(filename, b); err != nil {
   200  		return err
   201  	}
   202  
   203  	if pgpKey != nil {
   204  		key, err := pgp.MarshalPublicKey(pgp.Public(pgpKey))
   205  		if err != nil {
   206  			return err
   207  		}
   208  		if err = writeFile(filename+".key", key); err != nil {
   209  			return err
   210  		}
   211  
   212  		sig, err := pgp.Sign(pgpKey, b)
   213  		if err != nil {
   214  			return err
   215  		}
   216  		if err = writeFile(filename+".asc", sig); err != nil {
   217  			return err
   218  		}
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  func readXML(ctx context.Context, t target.Target, path string, res any) error {
   225  	rd, err := t.NewReader(ctx, path)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	defer rd.Close()
   230  
   231  	r := rd
   232  
   233  	if strings.HasSuffix(path, ".gz") {
   234  		r, err = gzip.NewReader(rd)
   235  		if err != nil {
   236  			return err
   237  		}
   238  		defer r.Close()
   239  	}
   240  
   241  	return xml.NewDecoder(r).Decode(res)
   242  }
   243  
   244  func filterPackageFiles(files []rpm.FileInfo) []string {
   245  	var res []string
   246  	for _, f := range files {
   247  		if !f.IsDir() && strings.HasPrefix(f.Name(), "/etc/") || strings.Contains(f.Name(), "/bin/") {
   248  			res = append(res, f.Name())
   249  		}
   250  	}
   251  	return res
   252  }
   253  
   254  func convertPackageFiles(files []rpm.FileInfo) []File {
   255  	res := make([]File, len(files))
   256  	for i, f := range files {
   257  		var typ string
   258  		if f.IsDir() {
   259  			typ = "dir"
   260  		}
   261  		res[i] = File{Type: typ, Path: f.Name()}
   262  	}
   263  	return res
   264  }
   265  
   266  func getEntries(d []rpm.Dependency) *Entries {
   267  	if len(d) == 0 {
   268  		return nil
   269  	}
   270  
   271  	entries := make([]Entry, 0, len(d))
   272  
   273  	for _, d := range d {
   274  		e := Entry{
   275  			Name: d.Name(),
   276  			Ver:  d.Version(),
   277  			Rel:  d.Release(),
   278  		}
   279  		if e.Ver != "" {
   280  			e.Epoch = strconv.Itoa(d.Epoch())
   281  		}
   282  		entries = append(entries, e)
   283  	}
   284  
   285  	return &Entries{entries}
   286  }
   287  
   288  func getChecksum(b []byte) Checksum {
   289  	sum := sha256.Sum256(b)
   290  	return Checksum{
   291  		Type:  "sha256",
   292  		Value: hex.EncodeToString(sum[:]),
   293  	}
   294  }
   295  
   296  func compress(p []byte) ([]byte, error) {
   297  	var b bytes.Buffer
   298  	w := gzip.NewWriter(&b)
   299  	if _, err := w.Write(p); err != nil {
   300  		return nil, err
   301  	}
   302  	if err := w.Close(); err != nil {
   303  		return nil, err
   304  	}
   305  	return b.Bytes(), nil
   306  }
   307  
   308  //nolint:gochecknoglobals
   309  var replacer = strings.NewReplacer("></version>", "/>", "></time>", "/>", "></size>", "/>",
   310  	"></location>", "/>", "></rpm:entry>", "/>", "></rpm:header-range>", "/>")
   311  
   312  func xmlMarshal(v any) ([]byte, error) {
   313  	w := bytes.NewBufferString(xml.Header)
   314  	b, err := xml.MarshalIndent(v, "", "\t")
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	if _, err = replacer.WriteString(w, string(b)); err != nil {
   319  		return nil, err
   320  	}
   321  	return w.Bytes(), nil
   322  }
   323  
   324  func writeFile(path string, data []byte) error {
   325  	if err := os.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
   326  		return err
   327  	}
   328  	return os.WriteFile(path, data, 0o600)
   329  }
   330  
   331  //nolint:gochecknoglobals
   332  var timeNow = func() int { return int(time.Now().Unix()) }