code.gitea.io/gitea@v1.22.3/modules/packages/rpm/metadata.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package rpm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  
    11  	"code.gitea.io/gitea/modules/timeutil"
    12  	"code.gitea.io/gitea/modules/validation"
    13  
    14  	"github.com/sassoftware/go-rpmutils"
    15  )
    16  
    17  const (
    18  	PropertyMetadata     = "rpm.metadata"
    19  	PropertyGroup        = "rpm.group"
    20  	PropertyArchitecture = "rpm.architecture"
    21  
    22  	SettingKeyPrivate = "rpm.key.private"
    23  	SettingKeyPublic  = "rpm.key.public"
    24  
    25  	RepositoryPackage = "_rpm"
    26  	RepositoryVersion = "_repository"
    27  )
    28  
    29  const (
    30  	// Can't use the syscall constants because they are not available for windows build.
    31  	sIFMT  = 0xf000
    32  	sIFDIR = 0x4000
    33  	sIXUSR = 0x40
    34  	sIXGRP = 0x8
    35  	sIXOTH = 0x1
    36  )
    37  
    38  // https://rpm-software-management.github.io/rpm/manual/spec.html
    39  // https://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/pkgformat.html
    40  
    41  type Package struct {
    42  	Name            string
    43  	Version         string
    44  	VersionMetadata *VersionMetadata
    45  	FileMetadata    *FileMetadata
    46  }
    47  
    48  type VersionMetadata struct {
    49  	License     string `json:"license,omitempty"`
    50  	ProjectURL  string `json:"project_url,omitempty"`
    51  	Summary     string `json:"summary,omitempty"`
    52  	Description string `json:"description,omitempty"`
    53  }
    54  
    55  type FileMetadata struct {
    56  	Architecture  string `json:"architecture,omitempty"`
    57  	Epoch         string `json:"epoch,omitempty"`
    58  	Version       string `json:"version,omitempty"`
    59  	Release       string `json:"release,omitempty"`
    60  	Vendor        string `json:"vendor,omitempty"`
    61  	Group         string `json:"group,omitempty"`
    62  	Packager      string `json:"packager,omitempty"`
    63  	SourceRpm     string `json:"source_rpm,omitempty"`
    64  	BuildHost     string `json:"build_host,omitempty"`
    65  	BuildTime     uint64 `json:"build_time,omitempty"`
    66  	FileTime      uint64 `json:"file_time,omitempty"`
    67  	InstalledSize uint64 `json:"installed_size,omitempty"`
    68  	ArchiveSize   uint64 `json:"archive_size,omitempty"`
    69  
    70  	Provides  []*Entry `json:"provide,omitempty"`
    71  	Requires  []*Entry `json:"require,omitempty"`
    72  	Conflicts []*Entry `json:"conflict,omitempty"`
    73  	Obsoletes []*Entry `json:"obsolete,omitempty"`
    74  
    75  	Files []*File `json:"files,omitempty"`
    76  
    77  	Changelogs []*Changelog `json:"changelogs,omitempty"`
    78  }
    79  
    80  type Entry struct {
    81  	Name    string `json:"name" xml:"name,attr"`
    82  	Flags   string `json:"flags,omitempty" xml:"flags,attr,omitempty"`
    83  	Version string `json:"version,omitempty" xml:"ver,attr,omitempty"`
    84  	Epoch   string `json:"epoch,omitempty" xml:"epoch,attr,omitempty"`
    85  	Release string `json:"release,omitempty" xml:"rel,attr,omitempty"`
    86  }
    87  
    88  type File struct {
    89  	Path         string `json:"path" xml:",chardata"`
    90  	Type         string `json:"type,omitempty" xml:"type,attr,omitempty"`
    91  	IsExecutable bool   `json:"is_executable" xml:"-"`
    92  }
    93  
    94  type Changelog struct {
    95  	Author string             `json:"author,omitempty" xml:"author,attr"`
    96  	Date   timeutil.TimeStamp `json:"date,omitempty" xml:"date,attr"`
    97  	Text   string             `json:"text,omitempty" xml:",chardata"`
    98  }
    99  
   100  // ParsePackage parses the RPM package file
   101  func ParsePackage(r io.Reader) (*Package, error) {
   102  	rpm, err := rpmutils.ReadRpm(r)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	nevra, err := rpm.Header.GetNEVRA()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	version := fmt.Sprintf("%s-%s", nevra.Version, nevra.Release)
   113  	if nevra.Epoch != "" && nevra.Epoch != "0" {
   114  		version = fmt.Sprintf("%s-%s", nevra.Epoch, version)
   115  	}
   116  
   117  	p := &Package{
   118  		Name:    nevra.Name,
   119  		Version: version,
   120  		VersionMetadata: &VersionMetadata{
   121  			Summary:     getString(rpm.Header, rpmutils.SUMMARY),
   122  			Description: getString(rpm.Header, rpmutils.DESCRIPTION),
   123  			License:     getString(rpm.Header, rpmutils.LICENSE),
   124  			ProjectURL:  getString(rpm.Header, rpmutils.URL),
   125  		},
   126  		FileMetadata: &FileMetadata{
   127  			Architecture:  nevra.Arch,
   128  			Epoch:         nevra.Epoch,
   129  			Version:       nevra.Version,
   130  			Release:       nevra.Release,
   131  			Vendor:        getString(rpm.Header, rpmutils.VENDOR),
   132  			Group:         getString(rpm.Header, rpmutils.GROUP),
   133  			Packager:      getString(rpm.Header, rpmutils.PACKAGER),
   134  			SourceRpm:     getString(rpm.Header, rpmutils.SOURCERPM),
   135  			BuildHost:     getString(rpm.Header, rpmutils.BUILDHOST),
   136  			BuildTime:     getUInt64(rpm.Header, rpmutils.BUILDTIME),
   137  			FileTime:      getUInt64(rpm.Header, rpmutils.FILEMTIMES),
   138  			InstalledSize: getUInt64(rpm.Header, rpmutils.SIZE),
   139  			ArchiveSize:   getUInt64(rpm.Header, rpmutils.SIG_PAYLOADSIZE),
   140  
   141  			Provides:   getEntries(rpm.Header, rpmutils.PROVIDENAME, rpmutils.PROVIDEVERSION, rpmutils.PROVIDEFLAGS),
   142  			Requires:   getEntries(rpm.Header, rpmutils.REQUIRENAME, rpmutils.REQUIREVERSION, rpmutils.REQUIREFLAGS),
   143  			Conflicts:  getEntries(rpm.Header, rpmutils.CONFLICTNAME, rpmutils.CONFLICTVERSION, rpmutils.CONFLICTFLAGS),
   144  			Obsoletes:  getEntries(rpm.Header, rpmutils.OBSOLETENAME, rpmutils.OBSOLETEVERSION, rpmutils.OBSOLETEFLAGS),
   145  			Files:      getFiles(rpm.Header),
   146  			Changelogs: getChangelogs(rpm.Header),
   147  		},
   148  	}
   149  
   150  	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
   151  		p.VersionMetadata.ProjectURL = ""
   152  	}
   153  
   154  	return p, nil
   155  }
   156  
   157  func getString(h *rpmutils.RpmHeader, tag int) string {
   158  	values, err := h.GetStrings(tag)
   159  	if err != nil || len(values) < 1 {
   160  		return ""
   161  	}
   162  	return values[0]
   163  }
   164  
   165  func getUInt64(h *rpmutils.RpmHeader, tag int) uint64 {
   166  	values, err := h.GetUint64s(tag)
   167  	if err != nil || len(values) < 1 {
   168  		return 0
   169  	}
   170  	return values[0]
   171  }
   172  
   173  func getEntries(h *rpmutils.RpmHeader, namesTag, versionsTag, flagsTag int) []*Entry {
   174  	names, err := h.GetStrings(namesTag)
   175  	if err != nil || len(names) == 0 {
   176  		return nil
   177  	}
   178  	flags, err := h.GetUint64s(flagsTag)
   179  	if err != nil || len(flags) == 0 {
   180  		return nil
   181  	}
   182  	versions, err := h.GetStrings(versionsTag)
   183  	if err != nil || len(versions) == 0 {
   184  		return nil
   185  	}
   186  	if len(names) != len(flags) || len(names) != len(versions) {
   187  		return nil
   188  	}
   189  
   190  	entries := make([]*Entry, 0, len(names))
   191  	for i := range names {
   192  		e := &Entry{
   193  			Name: names[i],
   194  		}
   195  
   196  		flags := flags[i]
   197  		if (flags&rpmutils.RPMSENSE_GREATER) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
   198  			e.Flags = "GE"
   199  		} else if (flags&rpmutils.RPMSENSE_LESS) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
   200  			e.Flags = "LE"
   201  		} else if (flags & rpmutils.RPMSENSE_GREATER) != 0 {
   202  			e.Flags = "GT"
   203  		} else if (flags & rpmutils.RPMSENSE_LESS) != 0 {
   204  			e.Flags = "LT"
   205  		} else if (flags & rpmutils.RPMSENSE_EQUAL) != 0 {
   206  			e.Flags = "EQ"
   207  		}
   208  
   209  		version := versions[i]
   210  		if version != "" {
   211  			parts := strings.Split(version, "-")
   212  
   213  			versionParts := strings.Split(parts[0], ":")
   214  			if len(versionParts) == 2 {
   215  				e.Version = versionParts[1]
   216  				e.Epoch = versionParts[0]
   217  			} else {
   218  				e.Version = versionParts[0]
   219  				e.Epoch = "0"
   220  			}
   221  
   222  			if len(parts) > 1 {
   223  				e.Release = parts[1]
   224  			}
   225  		}
   226  
   227  		entries = append(entries, e)
   228  	}
   229  	return entries
   230  }
   231  
   232  func getFiles(h *rpmutils.RpmHeader) []*File {
   233  	baseNames, _ := h.GetStrings(rpmutils.BASENAMES)
   234  	dirNames, _ := h.GetStrings(rpmutils.DIRNAMES)
   235  	dirIndexes, _ := h.GetUint32s(rpmutils.DIRINDEXES)
   236  	fileFlags, _ := h.GetUint32s(rpmutils.FILEFLAGS)
   237  	fileModes, _ := h.GetUint32s(rpmutils.FILEMODES)
   238  
   239  	files := make([]*File, 0, len(baseNames))
   240  	for i := range baseNames {
   241  		if len(dirIndexes) <= i {
   242  			continue
   243  		}
   244  		dirIndex := dirIndexes[i]
   245  		if len(dirNames) <= int(dirIndex) {
   246  			continue
   247  		}
   248  
   249  		var fileType string
   250  		var isExecutable bool
   251  		if i < len(fileFlags) && (fileFlags[i]&rpmutils.RPMFILE_GHOST) != 0 {
   252  			fileType = "ghost"
   253  		} else if i < len(fileModes) {
   254  			if (fileModes[i] & sIFMT) == sIFDIR {
   255  				fileType = "dir"
   256  			} else {
   257  				mode := fileModes[i] & ^uint32(sIFMT)
   258  				isExecutable = (mode&sIXUSR) != 0 || (mode&sIXGRP) != 0 || (mode&sIXOTH) != 0
   259  			}
   260  		}
   261  
   262  		files = append(files, &File{
   263  			Path:         dirNames[dirIndex] + baseNames[i],
   264  			Type:         fileType,
   265  			IsExecutable: isExecutable,
   266  		})
   267  	}
   268  
   269  	return files
   270  }
   271  
   272  func getChangelogs(h *rpmutils.RpmHeader) []*Changelog {
   273  	texts, err := h.GetStrings(rpmutils.CHANGELOGTEXT)
   274  	if err != nil || len(texts) == 0 {
   275  		return nil
   276  	}
   277  	authors, err := h.GetStrings(rpmutils.CHANGELOGNAME)
   278  	if err != nil || len(authors) == 0 {
   279  		return nil
   280  	}
   281  	times, err := h.GetUint32s(rpmutils.CHANGELOGTIME)
   282  	if err != nil || len(times) == 0 {
   283  		return nil
   284  	}
   285  	if len(texts) != len(authors) || len(texts) != len(times) {
   286  		return nil
   287  	}
   288  
   289  	changelogs := make([]*Changelog, 0, len(texts))
   290  	for i := range texts {
   291  		changelogs = append(changelogs, &Changelog{
   292  			Author: authors[i],
   293  			Date:   timeutil.TimeStamp(times[i]),
   294  			Text:   texts[i],
   295  		})
   296  	}
   297  	return changelogs
   298  }