Skip to content

Commit

Permalink
ext/featurefmt/rpm: Extract source package from rpm database
Browse files Browse the repository at this point in the history
Source package is now extracted from the RPM database by using
${SourceRPM} option in the rpm --qf argument.
  • Loading branch information
KeyboardNerd committed Oct 11, 2018
1 parent 4ac0466 commit a057e4a
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 47 deletions.
141 changes: 117 additions & 24 deletions ext/featurefmt/rpm/rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,56 @@ package rpm

import (
"bufio"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"

"github.com/deckarep/golang-set"
log "github.com/sirupsen/logrus"

"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/ext/versionfmt/rpm"
"github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/strutil"
"github.com/coreos/clair/pkg/tarutil"
)

var ignoredPackages = []string{
"gpg-pubkey", // Ignore gpg-pubkey packages which are fake packages used to store GPG keys - they are not versionned properly.
}

type lister struct{}

func init() {
featurefmt.RegisterLister("rpm", "1.0", &lister{})
}

func isIgnored(packageName string) bool {
for _, pkg := range ignoredPackages {
if pkg == packageName {
return true
}
}

return false
}

func valid(pkg *featurefmt.PackageInfo) bool {
return pkg.PackageName != "" && pkg.PackageVersion != "" &&
((pkg.SourceName == "" && pkg.SourceVersion != "") ||
(pkg.SourceName != "" && pkg.SourceVersion != ""))
}

func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.Feature, error) {
f, hasFile := files["var/lib/rpm/Packages"]
if !hasFile {
return []database.Feature{}, nil
}

// Create a map to store packages and ensure their uniqueness
packagesMap := make(map[string]database.Feature)

// Write the required "Packages" file to disk
tmpDir, err := ioutil.TempDir(os.TempDir(), "rpm")
defer os.RemoveAll(tmpDir)
Expand All @@ -62,54 +82,127 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.Feature, error)
}

// Extract binary package names because RHSA refers to binary package names.
out, err := exec.Command("rpm", "--dbpath", tmpDir, "-qa", "--qf", "%{NAME} %{EPOCH}:%{VERSION}-%{RELEASE}\n").CombinedOutput()
out, err := exec.Command("rpm", "--dbpath", tmpDir, "-qa", "--qf", "%{NAME} %{EPOCH}:%{VERSION}-%{RELEASE} %{SOURCERPM}\n").CombinedOutput()
if err != nil {
log.WithError(err).WithField("output", string(out)).Error("could not query RPM")
// Do not bubble up because we probably won't be able to fix it,
// the database must be corrupted
return []database.Feature{}, nil
}

packages := mapset.NewSet()
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
line := strings.Split(scanner.Text(), " ")
if len(line) != 2 {
if len(line) != 3 {
// We may see warnings on some RPM versions:
// "warning: Generating 12 missing index(es), please wait..."
continue
}

// Ignore gpg-pubkey packages which are fake packages used to store GPG keys - they are not versionned properly.
if line[0] == "gpg-pubkey" {
if isIgnored(line[0]) {
continue
}

// Parse version
version := strings.Replace(line[1], "(none):", "", -1)
err := versionfmt.Valid(rpm.ParserName, version)
if err != nil {
log.WithError(err).WithField("version", line[1]).Warning("could not parse package version. skipping")
pkg := featurefmt.PackageInfo{PackageName: line[0]}
pkg.PackageVersion = strings.Replace(line[1], "(none):", "", -1)
if err := versionfmt.Valid(rpm.ParserName, pkg.PackageVersion); err != nil {
log.WithError(err).WithField("version", line[1]).Warning("skipped unparseable package")
continue
}

// Add package
pkg := database.Feature{
Name: line[0],
Version: version,
if err := parseSourceRPM(line[2], &pkg); err != nil {
log.WithError(err).WithField("sourcerpm", line[2]).Warning("skipped unparseable package")
continue
}
packagesMap[pkg.Name+"#"+pkg.Version] = pkg
}

// Convert the map to a slice
packages := make([]database.Feature, 0, len(packagesMap))
for _, pkg := range packagesMap {
pkg.VersionFormat = rpm.ParserName
packages = append(packages, pkg)
if valid(&pkg) {
packages.Add(pkg)
}
}

return packages, nil
return featurefmt.PackageSetToFeatures(rpm.ParserName, packages), nil
}

func (l lister) RequiredFilenames() []string {
return []string{"var/lib/rpm/Packages"}
}

type rpmParserState string

const (
terminate rpmParserState = "terminate"
parseRPM rpmParserState = "RPM Token"
parseArchitecture rpmParserState = "Architecture Token"
parseRelease rpmParserState = "Release Token"
parseVersion rpmParserState = "Version Token"
)

// parseSourceRPM parses the source rpm package representation string
// http://ftp.rpm.org/max-rpm/ch-rpm-file-format.html
func parseSourceRPM(sourceRPM string, pkg *featurefmt.PackageInfo) error {
state := parseRPM
previousCheckPoint := len(sourceRPM)
release := ""
version := ""
for i := len(sourceRPM) - 1; i >= 0; i-- {
switch state {
case parseRPM:
if string(sourceRPM[i]) == "." {
state = parseArchitecture
packageType := strutil.Substring(sourceRPM, i+1, len(sourceRPM))
previousCheckPoint = i
if packageType != "rpm" {
return fmt.Errorf("unexpected package type, expect: 'rpm', got: '%s'", packageType)
}
}
case parseArchitecture:
if string(sourceRPM[i]) == "." {
state = parseRelease
architecture := strutil.Substring(sourceRPM, i+1, previousCheckPoint)
previousCheckPoint = i
if architecture != "src" && architecture != "nosrc" {
return fmt.Errorf("unexpected package architecture, expect: 'src' or 'nosrc', got: '%s'", architecture)
}
}
case parseRelease:
if string(sourceRPM[i]) == "-" {
state = parseVersion
release = strutil.Substring(sourceRPM, i+1, previousCheckPoint)
previousCheckPoint = i
if release == "" {
return fmt.Errorf("unexpected package release, expect: not empty")
}
}
case parseVersion:
if string(sourceRPM[i]) == "-" {
// terminate state
state = terminate
version = strutil.Substring(sourceRPM, i+1, previousCheckPoint)
previousCheckPoint = i
if version == "" {
return fmt.Errorf("unexpected package version, expect: not empty")
}
break
}
}
}

if state != terminate {
return fmt.Errorf("unexpected termination while parsing '%s'", state)
}

concatVersion := version + "-" + release
if err := versionfmt.Valid(rpm.ParserName, concatVersion); err != nil {
return err
}

name := strutil.Substring(sourceRPM, 0, previousCheckPoint)
if name == "" {
return fmt.Errorf("unexpected package name, expect: not empty")
}

pkg.SourceName = name
pkg.SourceVersion = concatVersion
return nil
}

0 comments on commit a057e4a

Please sign in to comment.