diff --git a/ext/featurefmt/rpm/rpm.go b/ext/featurefmt/rpm/rpm.go index be582384..634dfd0e 100644 --- a/ext/featurefmt/rpm/rpm.go +++ b/ext/featurefmt/rpm/rpm.go @@ -17,11 +17,13 @@ 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" @@ -29,24 +31,42 @@ import ( "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) @@ -62,7 +82,7 @@ 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, @@ -70,46 +90,119 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.Feature, error) 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 +} diff --git a/ext/featurefmt/rpm/rpm_test.go b/ext/featurefmt/rpm/rpm_test.go index 7206f153..a4e05fae 100644 --- a/ext/featurefmt/rpm/rpm_test.go +++ b/ext/featurefmt/rpm/rpm_test.go @@ -17,36 +17,245 @@ package rpm import ( "testing" - "github.com/coreos/clair/database" - "github.com/coreos/clair/ext/featurefmt/featurefmttest" + "github.com/stretchr/testify/require" + + "github.com/coreos/clair/ext/featurefmt" "github.com/coreos/clair/ext/versionfmt/rpm" - "github.com/coreos/clair/pkg/tarutil" ) +var expectedBigCaseInfo = []featurefmt.PackageInfo{ + {"publicsuffix-list-dafsa", "20180514-1.fc28", "publicsuffix-list", "20180514-1.fc28"}, + {"libreport-filesystem", "2.9.5-1.fc28", "libreport", "2.9.5-1.fc28"}, + {"fedora-gpg-keys", "28-5", "fedora-repos", "28-5"}, + {"fedora-release", "28-2", "", ""}, + {"filesystem", "3.8-2.fc28", "", ""}, + {"tzdata", "2018e-1.fc28", "", ""}, + {"pcre2", "10.31-10.fc28", "", ""}, + {"glibc-minimal-langpack", "2.27-32.fc28", "glibc", "2.27-32.fc28"}, + {"glibc-common", "2.27-32.fc28", "glibc", "2.27-32.fc28"}, + {"bash", "4.4.23-1.fc28", "", ""}, + {"zlib", "1.2.11-8.fc28", "", ""}, + {"bzip2-libs", "1.0.6-26.fc28", "bzip2", "1.0.6-26.fc28"}, + {"libcap", "2.25-9.fc28", "", ""}, + {"libgpg-error", "1.31-1.fc28", "", ""}, + {"libzstd", "1.3.5-1.fc28", "zstd", "1.3.5-1.fc28"}, + {"expat", "2.2.5-3.fc28", "", ""}, + {"nss-util", "3.38.0-1.0.fc28", "", ""}, + {"libcom_err", "1.44.2-0.fc28", "e2fsprogs", "1.44.2-0.fc28"}, + {"libffi", "3.1-16.fc28", "", ""}, + {"libgcrypt", "1.8.3-1.fc28", "", ""}, + {"libxml2", "2.9.8-4.fc28", "", ""}, + {"libacl", "2.2.53-1.fc28", "acl", "2.2.53-1.fc28"}, + {"sed", "4.5-1.fc28", "", ""}, + {"libmount", "2.32.1-1.fc28", "util-linux", "2.32.1-1.fc28"}, + {"p11-kit", "0.23.12-1.fc28", "", ""}, + {"libidn2", "2.0.5-1.fc28", "", ""}, + {"libcap-ng", "0.7.9-4.fc28", "", ""}, + {"lz4-libs", "1.8.1.2-4.fc28", "lz4", "1.8.1.2-4.fc28"}, + {"libassuan", "2.5.1-3.fc28", "", ""}, + {"keyutils-libs", "1.5.10-6.fc28", "keyutils", "1.5.10-6.fc28"}, + {"glib2", "2.56.1-4.fc28", "", ""}, + {"systemd-libs", "238-9.git0e0aa59.fc28", "systemd", "238-9.git0e0aa59.fc28"}, + {"dbus-libs", "1:1.12.10-1.fc28", "dbus", "1.12.10-1.fc28"}, + {"libtasn1", "4.13-2.fc28", "", ""}, + {"ca-certificates", "2018.2.24-1.0.fc28", "", ""}, + {"libarchive", "3.3.1-4.fc28", "", ""}, + {"openssl", "1:1.1.0h-3.fc28", "openssl", "1.1.0h-3.fc28"}, + {"libusbx", "1.0.22-1.fc28", "", ""}, + {"libsemanage", "2.8-2.fc28", "", ""}, + {"libutempter", "1.1.6-14.fc28", "", ""}, + {"mpfr", "3.1.6-1.fc28", "", ""}, + {"gnutls", "3.6.3-4.fc28", "", ""}, + {"gzip", "1.9-3.fc28", "", ""}, + {"acl", "2.2.53-1.fc28", "", ""}, + {"nss-softokn-freebl", "3.38.0-1.0.fc28", "nss-softokn", "3.38.0-1.0.fc28"}, + {"nss", "3.38.0-1.0.fc28", "", ""}, + {"libmetalink", "0.1.3-6.fc28", "", ""}, + {"libdb-utils", "5.3.28-30.fc28", "libdb", "5.3.28-30.fc28"}, + {"file-libs", "5.33-7.fc28", "file", "5.33-7.fc28"}, + {"libsss_idmap", "1.16.3-2.fc28", "sssd", "1.16.3-2.fc28"}, + {"libsigsegv", "2.11-5.fc28", "", ""}, + {"krb5-libs", "1.16.1-13.fc28", "krb5", "1.16.1-13.fc28"}, + {"libnsl2", "1.2.0-2.20180605git4a062cf.fc28", "", ""}, + {"python3-pip", "9.0.3-2.fc28", "python-pip", "9.0.3-2.fc28"}, + {"python3", "3.6.6-1.fc28", "", ""}, + {"pam", "1.3.1-1.fc28", "", ""}, + {"python3-gobject-base", "3.28.3-1.fc28", "pygobject3", "3.28.3-1.fc28"}, + {"python3-smartcols", "0.3.0-2.fc28", "python-smartcols", "0.3.0-2.fc28"}, + {"python3-iniparse", "0.4-30.fc28", "python-iniparse", "0.4-30.fc28"}, + {"openldap", "2.4.46-3.fc28", "", ""}, + {"libseccomp", "2.3.3-2.fc28", "", ""}, + {"npth", "1.5-4.fc28", "", ""}, + {"gpgme", "1.10.0-4.fc28", "", ""}, + {"json-c", "0.13.1-2.fc28", "", ""}, + {"libyaml", "0.1.7-5.fc28", "", ""}, + {"libpkgconf", "1.4.2-1.fc28", "pkgconf", "1.4.2-1.fc28"}, + {"pkgconf-pkg-config", "1.4.2-1.fc28", "pkgconf", "1.4.2-1.fc28"}, + {"iptables-libs", "1.6.2-3.fc28", "iptables", "1.6.2-3.fc28"}, + {"device-mapper-libs", "1.02.146-5.fc28", "lvm2", "2.02.177-5.fc28"}, + {"systemd-pam", "238-9.git0e0aa59.fc28", "systemd", "238-9.git0e0aa59.fc28"}, + {"systemd", "238-9.git0e0aa59.fc28", "", ""}, + {"elfutils-default-yama-scope", "0.173-1.fc28", "elfutils", "0.173-1.fc28"}, + {"libcurl", "7.59.0-6.fc28", "curl", "7.59.0-6.fc28"}, + {"python3-librepo", "1.8.1-7.fc28", "librepo", "1.8.1-7.fc28"}, + {"rpm-plugin-selinux", "4.14.1-9.fc28", "rpm", "4.14.1-9.fc28"}, + {"rpm", "4.14.1-9.fc28", "", ""}, + {"libdnf", "0.11.1-3.fc28", "", ""}, + {"rpm-build-libs", "4.14.1-9.fc28", "rpm", "4.14.1-9.fc28"}, + {"python3-rpm", "4.14.1-9.fc28", "rpm", "4.14.1-9.fc28"}, + {"dnf", "2.7.5-12.fc28", "", ""}, + {"deltarpm", "3.6-25.fc28", "", ""}, + {"sssd-client", "1.16.3-2.fc28", "sssd", "1.16.3-2.fc28"}, + {"cracklib-dicts", "2.9.6-13.fc28", "cracklib", "2.9.6-13.fc28"}, + {"tar", "2:1.30-3.fc28", "tar", "1.30-3.fc28"}, + {"diffutils", "3.6-4.fc28", "", ""}, + {"langpacks-en", "1.0-12.fc28", "langpacks", "1.0-12.fc28"}, + {"libgcc", "8.1.1-5.fc28", "gcc", "8.1.1-5.fc28"}, + {"pkgconf-m4", "1.4.2-1.fc28", "pkgconf", "1.4.2-1.fc28"}, + {"dnf-conf", "2.7.5-12.fc28", "dnf", "2.7.5-12.fc28"}, + {"fedora-repos", "28-5", "", ""}, + {"setup", "2.11.4-1.fc28", "", ""}, + {"basesystem", "11-5.fc28", "", ""}, + {"ncurses-base", "6.1-5.20180224.fc28", "ncurses", "6.1-5.20180224.fc28"}, + {"libselinux", "2.8-1.fc28", "", ""}, + {"ncurses-libs", "6.1-5.20180224.fc28", "ncurses", "6.1-5.20180224.fc28"}, + {"glibc", "2.27-32.fc28", "", ""}, + {"libsepol", "2.8-1.fc28", "", ""}, + {"xz-libs", "5.2.4-2.fc28", "xz", "5.2.4-2.fc28"}, + {"info", "6.5-4.fc28", "texinfo", "6.5-4.fc28"}, + {"libdb", "5.3.28-30.fc28", "", ""}, + {"elfutils-libelf", "0.173-1.fc28", "elfutils", "0.173-1.fc28"}, + {"popt", "1.16-14.fc28", "", ""}, + {"nspr", "4.19.0-1.fc28", "", ""}, + {"libxcrypt", "4.1.2-1.fc28", "", ""}, + {"lua-libs", "5.3.4-10.fc28", "lua", "5.3.4-10.fc28"}, + {"libuuid", "2.32.1-1.fc28", "util-linux", "2.32.1-1.fc28"}, + {"readline", "7.0-11.fc28", "", ""}, + {"libattr", "2.4.48-3.fc28", "attr", "2.4.48-3.fc28"}, + {"coreutils-single", "8.29-7.fc28", "coreutils", "8.29-7.fc28"}, + {"libblkid", "2.32.1-1.fc28", "util-linux", "2.32.1-1.fc28"}, + {"gmp", "1:6.1.2-7.fc28", "gmp", "6.1.2-7.fc28"}, + {"libunistring", "0.9.10-1.fc28", "", ""}, + {"sqlite-libs", "3.22.0-4.fc28", "sqlite", "3.22.0-4.fc28"}, + {"audit-libs", "2.8.4-2.fc28", "audit", "2.8.4-2.fc28"}, + {"chkconfig", "1.10-4.fc28", "", ""}, + {"libsmartcols", "2.32.1-1.fc28", "util-linux", "2.32.1-1.fc28"}, + {"pcre", "8.42-3.fc28", "", ""}, + {"grep", "3.1-5.fc28", "", ""}, + {"crypto-policies", "20180425-5.git6ad4018.fc28", "", ""}, + {"gdbm-libs", "1:1.14.1-4.fc28", "gdbm", "1.14.1-4.fc28"}, + {"p11-kit-trust", "0.23.12-1.fc28", "p11-kit", "0.23.12-1.fc28"}, + {"openssl-libs", "1:1.1.0h-3.fc28", "openssl", "1.1.0h-3.fc28"}, + {"ima-evm-utils", "1.1-2.fc28", "", ""}, + {"gdbm", "1:1.14.1-4.fc28", "gdbm", "1.14.1-4.fc28"}, + {"gobject-introspection", "1.56.1-1.fc28", "", ""}, + {"shadow-utils", "2:4.6-1.fc28", "shadow-utils", "4.6-1.fc28"}, + {"libpsl", "0.20.2-2.fc28", "", ""}, + {"nettle", "3.4-2.fc28", "", ""}, + {"libfdisk", "2.32.1-1.fc28", "util-linux", "2.32.1-1.fc28"}, + {"cracklib", "2.9.6-13.fc28", "", ""}, + {"libcomps", "0.1.8-11.fc28", "", ""}, + {"nss-softokn", "3.38.0-1.0.fc28", "", ""}, + {"nss-sysinit", "3.38.0-1.0.fc28", "nss", "3.38.0-1.0.fc28"}, + {"libksba", "1.3.5-7.fc28", "", ""}, + {"kmod-libs", "25-2.fc28", "kmod", "25-2.fc28"}, + {"libsss_nss_idmap", "1.16.3-2.fc28", "sssd", "1.16.3-2.fc28"}, + {"libverto", "0.3.0-5.fc28", "", ""}, + {"gawk", "4.2.1-1.fc28", "", ""}, + {"libtirpc", "1.0.3-3.rc2.fc28", "", ""}, + {"python3-libs", "3.6.6-1.fc28", "python3", "3.6.6-1.fc28"}, + {"python3-setuptools", "39.2.0-6.fc28", "python-setuptools", "39.2.0-6.fc28"}, + {"libpwquality", "1.4.0-7.fc28", "", ""}, + {"util-linux", "2.32.1-1.fc28", "", ""}, + {"python3-libcomps", "0.1.8-11.fc28", "libcomps", "0.1.8-11.fc28"}, + {"python3-six", "1.11.0-3.fc28", "python-six", "1.11.0-3.fc28"}, + {"cyrus-sasl-lib", "2.1.27-0.2rc7.fc28", "cyrus-sasl", "2.1.27-0.2rc7.fc28"}, + {"libssh", "0.8.2-1.fc28", "", ""}, + {"qrencode-libs", "3.4.4-5.fc28", "qrencode", "3.4.4-5.fc28"}, + {"gnupg2", "2.2.8-1.fc28", "", ""}, + {"python3-gpg", "1.10.0-4.fc28", "gpgme", "1.10.0-4.fc28"}, + {"libargon2", "20161029-5.fc28", "argon2", "20161029-5.fc28"}, + {"libmodulemd", "1.6.2-2.fc28", "", ""}, + {"pkgconf", "1.4.2-1.fc28", "", ""}, + {"libpcap", "14:1.9.0-1.fc28", "libpcap", "1.9.0-1.fc28"}, + {"device-mapper", "1.02.146-5.fc28", "lvm2", "2.02.177-5.fc28"}, + {"cryptsetup-libs", "2.0.4-1.fc28", "cryptsetup", "2.0.4-1.fc28"}, + {"elfutils-libs", "0.173-1.fc28", "elfutils", "0.173-1.fc28"}, + {"dbus", "1:1.12.10-1.fc28", "dbus", "1.12.10-1.fc28"}, + {"libnghttp2", "1.32.1-1.fc28", "nghttp2", "1.32.1-1.fc28"}, + {"librepo", "1.8.1-7.fc28", "", ""}, + {"curl", "7.59.0-6.fc28", "", ""}, + {"rpm-libs", "4.14.1-9.fc28", "rpm", "4.14.1-9.fc28"}, + {"libsolv", "0.6.35-1.fc28", "", ""}, + {"python3-hawkey", "0.11.1-3.fc28", "libdnf", "0.11.1-3.fc28"}, + {"rpm-sign-libs", "4.14.1-9.fc28", "rpm", "4.14.1-9.fc28"}, + {"python3-dnf", "2.7.5-12.fc28", "dnf", "2.7.5-12.fc28"}, + {"dnf-yum", "2.7.5-12.fc28", "dnf", "2.7.5-12.fc28"}, + {"rpm-plugin-systemd-inhibit", "4.14.1-9.fc28", "rpm", "4.14.1-9.fc28"}, + {"nss-tools", "3.38.0-1.0.fc28", "nss", "3.38.0-1.0.fc28"}, + {"openssl-pkcs11", "0.4.8-1.fc28", "", ""}, + {"vim-minimal", "2:8.1.328-1.fc28", "vim", "8.1.328-1.fc28"}, + {"glibc-langpack-en", "2.27-32.fc28", "glibc", "2.27-32.fc28"}, + {"rootfiles", "8.1-22.fc28", "", ""}, +} + func TestRpmFeatureDetection(t *testing.T) { - testData := []featurefmttest.TestData{ - // Test a CentOS 7 RPM database - // Memo: Use the following command on a RPM-based system to shrink a database: rpm -qa --qf "%{NAME}\n" |tail -n +3| xargs rpm -e --justdb + for _, test := range []featurefmt.TestCase{ { - Features: []database.Feature{ - // Two packages from this source are installed, it should only appear once - { - Name: "centos-release", - Version: "7-1.1503.el7.centos.2.8", - VersionFormat: rpm.ParserName, - }, - // Two packages from this source are installed, it should only appear once - { - Name: "filesystem", - Version: "3.2-18.el7", - VersionFormat: rpm.ParserName, - }, - }, - Files: tarutil.FilesMap{ - "var/lib/rpm/Packages": featurefmttest.LoadFileForTest("rpm/testdata/Packages"), + "valid small case", + map[string]string{"var/lib/rpm/Packages": "rpm/testdata/valid"}, + []featurefmt.PackageInfo{ + {"centos-release", "7-1.1503.el7.centos.2.8", "", ""}, + {"filesystem", "3.2-18.el7", "", ""}, }, }, + { + "valid big case", + map[string]string{"var/lib/rpm/Packages": "rpm/testdata/valid_big"}, + expectedBigCaseInfo, + }, + } { + featurefmt.RunTest(t, test, lister{}, rpm.ParserName) } +} + +func TestParseSourceRPM(t *testing.T) { + for _, test := range [...]struct { + sourceRPM string + + expectedName string + expectedVersion string + expectedErr string + }{ + // valid cases + {"publicsuffix-list-20180514-1.fc28.src.rpm", "publicsuffix-list", "20180514-1.fc28", ""}, + {"libreport-2.9.5-1.fc28.src.rpm", "libreport", "2.9.5-1.fc28", ""}, + {"lua-5.3.4-10.fc28.src.rpm", "lua", "5.3.4-10.fc28", ""}, + {"crypto-policies-20180425-5.git6ad4018.fc28.src.rpm", "crypto-policies", "20180425-5.git6ad4018.fc28", ""}, - featurefmttest.TestLister(t, &lister{}, testData) + // invalid cases + {"crypto-policies-20180425-5.git6ad4018.fc28.src.dpkg", "", "", "unexpected package type, expect: 'rpm', got: 'dpkg'"}, + {"crypto-policies-20180425-5.git6ad4018.fc28.debian-8.rpm", "", "", "unexpected package architecture, expect: 'src' or 'nosrc', got: 'debian-8'"}, + {"fc28.src.rpm", "", "", "unexpected termination while parsing 'Release Token'"}, + {"...", "", "", "unexpected package type, expect: 'rpm', got: ''"}, + + // impossible case + // This illustrates the limitation of this parser, it will not find the + // error cased by extra '-' in the intended version/expect token. Based + // on the documentation, this case should never happen and indicates a + // corrupted rpm database. + // actual expected: name="lua", version="5.3.4", release="10.fc-28" + {"lua-5.3.4-10.fc-28.src.rpm", "lua-5.3.4", "10.fc-28", ""}, + } { + pkg := featurefmt.PackageInfo{} + err := parseSourceRPM(test.sourceRPM, &pkg) + if test.expectedErr != "" { + require.EqualError(t, err, test.expectedErr) + continue + } + + require.Nil(t, err) + require.Equal(t, test.expectedName, pkg.SourceName) + require.Equal(t, test.expectedVersion, pkg.SourceVersion) + } } diff --git a/ext/featurefmt/rpm/testdata/Packages b/ext/featurefmt/rpm/testdata/valid similarity index 100% rename from ext/featurefmt/rpm/testdata/Packages rename to ext/featurefmt/rpm/testdata/valid diff --git a/ext/featurefmt/rpm/testdata/valid_big b/ext/featurefmt/rpm/testdata/valid_big new file mode 100644 index 00000000..925367c6 Binary files /dev/null and b/ext/featurefmt/rpm/testdata/valid_big differ diff --git a/pkg/strutil/strutil.go b/pkg/strutil/strutil.go index bfd8dc01..f521d6ae 100644 --- a/pkg/strutil/strutil.go +++ b/pkg/strutil/strutil.go @@ -57,3 +57,13 @@ func Intersect(X, Y []string) []string { func CleanURL(str string) string { return urlParametersRegexp.ReplaceAllString(str, "") } + +// Substring returns a substring by [start, end). If start or end are out +// of bound, it returns "". +func Substring(s string, start, end int) string { + if start > len(s) || start < 0 || end > len(s) || end < 0 || start >= end { + return "" + } + + return s[start:end] +} diff --git a/pkg/strutil/strutil_test.go b/pkg/strutil/strutil_test.go index 2e81856c..83528912 100644 --- a/pkg/strutil/strutil_test.go +++ b/pkg/strutil/strutil_test.go @@ -18,8 +18,24 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestSubstring(t *testing.T) { + for _, test := range [...]struct { + in string + start int + end int + + out string + }{ + {"", 0, 1, ""}, {"", 0, 0, ""}, {"", -1, -1, ""}, {"1", 1, 0, ""}, + {"1", 1, 1, ""}, {"1", 0, 1, "1"}, {"1", 0, 2, ""}, + } { + require.Equal(t, test.out, Substring(test.in, test.start, test.end)) + } +} + func TestStringComparison(t *testing.T) { cmp := Difference([]string{"a", "b", "b", "a"}, []string{"a", "c"}) assert.Len(t, cmp, 1)