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