summaryrefslogtreecommitdiffstats
path: root/modules/packages/rpm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /modules/packages/rpm
parentInitial commit. (diff)
downloadforgejo-upstream/9.0.0.tar.xz
forgejo-upstream/9.0.0.zip
Adding upstream version 9.0.0.HEADupstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'modules/packages/rpm')
-rw-r--r--modules/packages/rpm/metadata.go298
-rw-r--r--modules/packages/rpm/metadata_test.go164
2 files changed, 462 insertions, 0 deletions
diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go
new file mode 100644
index 0000000..f4f78c2
--- /dev/null
+++ b/modules/packages/rpm/metadata.go
@@ -0,0 +1,298 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/validation"
+
+ "github.com/sassoftware/go-rpmutils"
+)
+
+const (
+ PropertyMetadata = "rpm.metadata"
+ PropertyGroup = "rpm.group"
+ PropertyArchitecture = "rpm.architecture"
+
+ SettingKeyPrivate = "rpm.key.private"
+ SettingKeyPublic = "rpm.key.public"
+
+ RepositoryPackage = "_rpm"
+ RepositoryVersion = "_repository"
+)
+
+const (
+ // Can't use the syscall constants because they are not available for windows build.
+ sIFMT = 0xf000
+ sIFDIR = 0x4000
+ sIXUSR = 0x40
+ sIXGRP = 0x8
+ sIXOTH = 0x1
+)
+
+// https://rpm-software-management.github.io/rpm/manual/spec.html
+// https://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/pkgformat.html
+
+type Package struct {
+ Name string
+ Version string
+ VersionMetadata *VersionMetadata
+ FileMetadata *FileMetadata
+}
+
+type VersionMetadata struct {
+ License string `json:"license,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+}
+
+type FileMetadata struct {
+ Architecture string `json:"architecture,omitempty"`
+ Epoch string `json:"epoch,omitempty"`
+ Version string `json:"version,omitempty"`
+ Release string `json:"release,omitempty"`
+ Vendor string `json:"vendor,omitempty"`
+ Group string `json:"group,omitempty"`
+ Packager string `json:"packager,omitempty"`
+ SourceRpm string `json:"source_rpm,omitempty"`
+ BuildHost string `json:"build_host,omitempty"`
+ BuildTime uint64 `json:"build_time,omitempty"`
+ FileTime uint64 `json:"file_time,omitempty"`
+ InstalledSize uint64 `json:"installed_size,omitempty"`
+ ArchiveSize uint64 `json:"archive_size,omitempty"`
+
+ Provides []*Entry `json:"provide,omitempty"`
+ Requires []*Entry `json:"require,omitempty"`
+ Conflicts []*Entry `json:"conflict,omitempty"`
+ Obsoletes []*Entry `json:"obsolete,omitempty"`
+
+ Files []*File `json:"files,omitempty"`
+
+ Changelogs []*Changelog `json:"changelogs,omitempty"`
+}
+
+type Entry struct {
+ Name string `json:"name" xml:"name,attr"`
+ Flags string `json:"flags,omitempty" xml:"flags,attr,omitempty"`
+ Version string `json:"version,omitempty" xml:"ver,attr,omitempty"`
+ Epoch string `json:"epoch,omitempty" xml:"epoch,attr,omitempty"`
+ Release string `json:"release,omitempty" xml:"rel,attr,omitempty"`
+}
+
+type File struct {
+ Path string `json:"path" xml:",chardata"`
+ Type string `json:"type,omitempty" xml:"type,attr,omitempty"`
+ IsExecutable bool `json:"is_executable" xml:"-"`
+}
+
+type Changelog struct {
+ Author string `json:"author,omitempty" xml:"author,attr"`
+ Date timeutil.TimeStamp `json:"date,omitempty" xml:"date,attr"`
+ Text string `json:"text,omitempty" xml:",chardata"`
+}
+
+// ParsePackage parses the RPM package file
+func ParsePackage(r io.Reader) (*Package, error) {
+ rpm, err := rpmutils.ReadRpm(r)
+ if err != nil {
+ return nil, err
+ }
+
+ nevra, err := rpm.Header.GetNEVRA()
+ if err != nil {
+ return nil, err
+ }
+
+ version := fmt.Sprintf("%s-%s", nevra.Version, nevra.Release)
+ if nevra.Epoch != "" && nevra.Epoch != "0" {
+ version = fmt.Sprintf("%s-%s", nevra.Epoch, version)
+ }
+
+ p := &Package{
+ Name: nevra.Name,
+ Version: version,
+ VersionMetadata: &VersionMetadata{
+ Summary: getString(rpm.Header, rpmutils.SUMMARY),
+ Description: getString(rpm.Header, rpmutils.DESCRIPTION),
+ License: getString(rpm.Header, rpmutils.LICENSE),
+ ProjectURL: getString(rpm.Header, rpmutils.URL),
+ },
+ FileMetadata: &FileMetadata{
+ Architecture: nevra.Arch,
+ Epoch: nevra.Epoch,
+ Version: nevra.Version,
+ Release: nevra.Release,
+ Vendor: getString(rpm.Header, rpmutils.VENDOR),
+ Group: getString(rpm.Header, rpmutils.GROUP),
+ Packager: getString(rpm.Header, rpmutils.PACKAGER),
+ SourceRpm: getString(rpm.Header, rpmutils.SOURCERPM),
+ BuildHost: getString(rpm.Header, rpmutils.BUILDHOST),
+ BuildTime: getUInt64(rpm.Header, rpmutils.BUILDTIME),
+ FileTime: getUInt64(rpm.Header, rpmutils.FILEMTIMES),
+ InstalledSize: getUInt64(rpm.Header, rpmutils.SIZE),
+ ArchiveSize: getUInt64(rpm.Header, rpmutils.SIG_PAYLOADSIZE),
+
+ Provides: getEntries(rpm.Header, rpmutils.PROVIDENAME, rpmutils.PROVIDEVERSION, rpmutils.PROVIDEFLAGS),
+ Requires: getEntries(rpm.Header, rpmutils.REQUIRENAME, rpmutils.REQUIREVERSION, rpmutils.REQUIREFLAGS),
+ Conflicts: getEntries(rpm.Header, rpmutils.CONFLICTNAME, rpmutils.CONFLICTVERSION, rpmutils.CONFLICTFLAGS),
+ Obsoletes: getEntries(rpm.Header, rpmutils.OBSOLETENAME, rpmutils.OBSOLETEVERSION, rpmutils.OBSOLETEFLAGS),
+ Files: getFiles(rpm.Header),
+ Changelogs: getChangelogs(rpm.Header),
+ },
+ }
+
+ if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
+ p.VersionMetadata.ProjectURL = ""
+ }
+
+ return p, nil
+}
+
+func getString(h *rpmutils.RpmHeader, tag int) string {
+ values, err := h.GetStrings(tag)
+ if err != nil || len(values) < 1 {
+ return ""
+ }
+ return values[0]
+}
+
+func getUInt64(h *rpmutils.RpmHeader, tag int) uint64 {
+ values, err := h.GetUint64s(tag)
+ if err != nil || len(values) < 1 {
+ return 0
+ }
+ return values[0]
+}
+
+func getEntries(h *rpmutils.RpmHeader, namesTag, versionsTag, flagsTag int) []*Entry {
+ names, err := h.GetStrings(namesTag)
+ if err != nil || len(names) == 0 {
+ return nil
+ }
+ flags, err := h.GetUint64s(flagsTag)
+ if err != nil || len(flags) == 0 {
+ return nil
+ }
+ versions, err := h.GetStrings(versionsTag)
+ if err != nil || len(versions) == 0 {
+ return nil
+ }
+ if len(names) != len(flags) || len(names) != len(versions) {
+ return nil
+ }
+
+ entries := make([]*Entry, 0, len(names))
+ for i := range names {
+ e := &Entry{
+ Name: names[i],
+ }
+
+ flags := flags[i]
+ if (flags&rpmutils.RPMSENSE_GREATER) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
+ e.Flags = "GE"
+ } else if (flags&rpmutils.RPMSENSE_LESS) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
+ e.Flags = "LE"
+ } else if (flags & rpmutils.RPMSENSE_GREATER) != 0 {
+ e.Flags = "GT"
+ } else if (flags & rpmutils.RPMSENSE_LESS) != 0 {
+ e.Flags = "LT"
+ } else if (flags & rpmutils.RPMSENSE_EQUAL) != 0 {
+ e.Flags = "EQ"
+ }
+
+ version := versions[i]
+ if version != "" {
+ parts := strings.Split(version, "-")
+
+ versionParts := strings.Split(parts[0], ":")
+ if len(versionParts) == 2 {
+ e.Version = versionParts[1]
+ e.Epoch = versionParts[0]
+ } else {
+ e.Version = versionParts[0]
+ e.Epoch = "0"
+ }
+
+ if len(parts) > 1 {
+ e.Release = parts[1]
+ }
+ }
+
+ entries = append(entries, e)
+ }
+ return entries
+}
+
+func getFiles(h *rpmutils.RpmHeader) []*File {
+ baseNames, _ := h.GetStrings(rpmutils.BASENAMES)
+ dirNames, _ := h.GetStrings(rpmutils.DIRNAMES)
+ dirIndexes, _ := h.GetUint32s(rpmutils.DIRINDEXES)
+ fileFlags, _ := h.GetUint32s(rpmutils.FILEFLAGS)
+ fileModes, _ := h.GetUint32s(rpmutils.FILEMODES)
+
+ files := make([]*File, 0, len(baseNames))
+ for i := range baseNames {
+ if len(dirIndexes) <= i {
+ continue
+ }
+ dirIndex := dirIndexes[i]
+ if len(dirNames) <= int(dirIndex) {
+ continue
+ }
+
+ var fileType string
+ var isExecutable bool
+ if i < len(fileFlags) && (fileFlags[i]&rpmutils.RPMFILE_GHOST) != 0 {
+ fileType = "ghost"
+ } else if i < len(fileModes) {
+ if (fileModes[i] & sIFMT) == sIFDIR {
+ fileType = "dir"
+ } else {
+ mode := fileModes[i] & ^uint32(sIFMT)
+ isExecutable = (mode&sIXUSR) != 0 || (mode&sIXGRP) != 0 || (mode&sIXOTH) != 0
+ }
+ }
+
+ files = append(files, &File{
+ Path: dirNames[dirIndex] + baseNames[i],
+ Type: fileType,
+ IsExecutable: isExecutable,
+ })
+ }
+
+ return files
+}
+
+func getChangelogs(h *rpmutils.RpmHeader) []*Changelog {
+ texts, err := h.GetStrings(rpmutils.CHANGELOGTEXT)
+ if err != nil || len(texts) == 0 {
+ return nil
+ }
+ authors, err := h.GetStrings(rpmutils.CHANGELOGNAME)
+ if err != nil || len(authors) == 0 {
+ return nil
+ }
+ times, err := h.GetUint32s(rpmutils.CHANGELOGTIME)
+ if err != nil || len(times) == 0 {
+ return nil
+ }
+ if len(texts) != len(authors) || len(texts) != len(times) {
+ return nil
+ }
+
+ changelogs := make([]*Changelog, 0, len(texts))
+ for i := range texts {
+ changelogs = append(changelogs, &Changelog{
+ Author: authors[i],
+ Date: timeutil.TimeStamp(times[i]),
+ Text: texts[i],
+ })
+ }
+ return changelogs
+}
diff --git a/modules/packages/rpm/metadata_test.go b/modules/packages/rpm/metadata_test.go
new file mode 100644
index 0000000..dc9b480
--- /dev/null
+++ b/modules/packages/rpm/metadata_test.go
@@ -0,0 +1,164 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/base64"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParsePackage(t *testing.T) {
+ base64RpmPackageContent := `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`
+ rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
+ require.NoError(t, err)
+
+ zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
+ require.NoError(t, err)
+
+ p, err := ParsePackage(zr)
+ assert.NotNil(t, p)
+ require.NoError(t, err)
+
+ assert.Equal(t, "gitea-test", p.Name)
+ assert.Equal(t, "1.0.2-1", p.Version)
+ assert.NotNil(t, p.VersionMetadata)
+ assert.NotNil(t, p.FileMetadata)
+
+ assert.Equal(t, "MIT", p.VersionMetadata.License)
+ assert.Equal(t, "https://gitea.io", p.VersionMetadata.ProjectURL)
+ assert.Equal(t, "RPM package summary", p.VersionMetadata.Summary)
+ assert.Equal(t, "RPM package description", p.VersionMetadata.Description)
+
+ assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
+ assert.Equal(t, "0", p.FileMetadata.Epoch)
+ assert.Equal(t, "1.0.2", p.FileMetadata.Version)
+ assert.Equal(t, "1", p.FileMetadata.Release)
+ assert.Empty(t, p.FileMetadata.Vendor)
+ assert.Equal(t, "KN4CK3R", p.FileMetadata.Packager)
+ assert.Equal(t, "gitea-test-1.0.2-1.src.rpm", p.FileMetadata.SourceRpm)
+ assert.Equal(t, "e44b1687d04b", p.FileMetadata.BuildHost)
+ assert.EqualValues(t, 1678225964, p.FileMetadata.BuildTime)
+ assert.EqualValues(t, 1678225964, p.FileMetadata.FileTime)
+ assert.EqualValues(t, 13, p.FileMetadata.InstalledSize)
+ assert.EqualValues(t, 272, p.FileMetadata.ArchiveSize)
+ assert.Empty(t, p.FileMetadata.Conflicts)
+ assert.Empty(t, p.FileMetadata.Obsoletes)
+
+ assert.ElementsMatch(
+ t,
+ []*Entry{
+ {
+ Name: "gitea-test",
+ Flags: "EQ",
+ Version: "1.0.2",
+ Epoch: "0",
+ Release: "1",
+ },
+ {
+ Name: "gitea-test(x86-64)",
+ Flags: "EQ",
+ Version: "1.0.2",
+ Epoch: "0",
+ Release: "1",
+ },
+ },
+ p.FileMetadata.Provides,
+ )
+ assert.ElementsMatch(
+ t,
+ []*Entry{
+ {
+ Name: "/bin/sh",
+ },
+ {
+ Name: "/bin/sh",
+ },
+ {
+ Name: "/bin/sh",
+ },
+ {
+ Name: "rpmlib(CompressedFileNames)",
+ Flags: "LE",
+ Version: "3.0.4",
+ Epoch: "0",
+ Release: "1",
+ },
+ {
+ Name: "rpmlib(FileDigests)",
+ Flags: "LE",
+ Version: "4.6.0",
+ Epoch: "0",
+ Release: "1",
+ },
+ {
+ Name: "rpmlib(PayloadFilesHavePrefix)",
+ Flags: "LE",
+ Version: "4.0",
+ Epoch: "0",
+ Release: "1",
+ },
+ {
+ Name: "rpmlib(PayloadIsXz)",
+ Flags: "LE",
+ Version: "5.2",
+ Epoch: "0",
+ Release: "1",
+ },
+ },
+ p.FileMetadata.Requires,
+ )
+ assert.ElementsMatch(
+ t,
+ []*File{
+ {
+ Path: "/usr/local/bin/hello",
+ IsExecutable: true,
+ },
+ },
+ p.FileMetadata.Files,
+ )
+ assert.ElementsMatch(
+ t,
+ []*Changelog{
+ {
+ Author: "KN4CK3R <dummy@gitea.io>",
+ Date: 1678276800,
+ Text: "- Changelog message.",
+ },
+ },
+ p.FileMetadata.Changelogs,
+ )
+}