Remove MetadataType from core package object and normalize JSON metadataType values (#1983)

* [wip]

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* distinct the package metadata functions

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove metadata type from package core model

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate review feedback for names

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add RPM archive metadata and split parser helpers

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* clarify the python package metadata type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename the KB metadata type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* break hackage and composer types by use case

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* linting fix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix encoding and decoding for syft-json and cyclonedx

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump json schema to 11

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update cyclonedx-json snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update cyclonedx-xml snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update spdx-json snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update spdx-tv snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update syft-json snapshots

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* correct metadata type in stack yaml parser test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix bom-ref redactor for cyclonedx-xml

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for legacy package metadata names

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* regenerate json schema v11

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix legacy HackageMetadataType reflect type value check

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* packagemetadata discovery should account for type shadowing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix cli tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump json schema version to v12

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update json schema to incorporate changes from main

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add syft-json legacy config option

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests around v11-v12 json decoding

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add docs for SYFT_JSON_LEGACY

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename structs to be compliant with new naming scheme

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2023-10-30 12:12:04 -04:00
committed by GitHub
parent f442586ec9
commit 1aaa644007
261 changed files with 7010 additions and 4040 deletions
@@ -0,0 +1,69 @@
package packagemetadata
import (
"reflect"
"testing"
)
type CompletionTester struct {
saw []any
valid []any
ignore []any
}
func NewCompletionTester(t testing.TB, ignore ...any) *CompletionTester {
tester := &CompletionTester{
valid: AllTypes(),
ignore: ignore,
}
t.Cleanup(func() {
t.Helper()
tester.validate(t)
})
return tester
}
func (tr *CompletionTester) Tested(t testing.TB, m any) {
t.Helper()
if m == nil {
return
}
if len(tr.valid) == 0 {
t.Fatal("no valid metadata types to test against")
}
ty := reflect.TypeOf(m)
for _, v := range tr.valid {
if reflect.TypeOf(v) == ty {
tr.saw = append(tr.saw, m)
return
}
}
t.Fatalf("tested metadata type is not valid: %s", ty.Name())
}
func (tr *CompletionTester) validate(t testing.TB) {
t.Helper()
count := make(map[reflect.Type]int)
for _, m := range tr.saw {
count[reflect.TypeOf(m)]++
}
validations:
for _, v := range tr.valid {
ty := reflect.TypeOf(v)
for _, ignore := range tr.ignore {
if ty == reflect.TypeOf(ignore) {
// skip ignored types
continue validations
}
}
if c, exists := count[ty]; c == 0 || !exists {
t.Errorf("metadata type %s is not covered by a test", ty.Name())
}
}
}
@@ -14,8 +14,12 @@ import (
"github.com/scylladb/go-set/strset"
)
var metadataExceptions = strset.New(
"FileMetadata",
// these are names of struct types in the pkg package that are not metadata types (thus should not be in the JSON schema)
var knownNonMetadataTypeNames = strset.New(
"Package",
"Collection",
"License",
"LicenseSet",
)
func DiscoverTypeNames() ([]string, error) {
@@ -67,9 +71,9 @@ func findMetadataDefinitionNames(paths ...string) ([]string, error) {
strNames := names.List()
sort.Strings(strNames)
// note: 30 is a point-in-time gut check. This number could be updated if new metadata definitions are added, but is not required.
// note: 35 is a point-in-time gut check. This number could be updated if new metadata definitions are added, but is not required.
// it is really intended to catch any major issues with the generation process that would generate, say, 0 definitions.
if len(strNames) < 30 {
if len(strNames) < 35 {
return nil, fmt.Errorf("not enough metadata definitions found (discovered: " + fmt.Sprintf("%d", len(strNames)) + ")")
}
@@ -101,25 +105,53 @@ func findMetadataDefinitionNamesInFile(path string) ([]string, []string, error)
continue
}
structType, ok := spec.Type.(*ast.StructType)
if !ok {
name := spec.Name.String()
// only look for exported types
if !isMetadataTypeCandidate(name) {
continue
}
// check if the struct type ends with "Metadata"
name := spec.Name.String()
// only look for exported types that end with "Metadata"
if isMetadataTypeCandidate(name) {
// print the full declaration of the struct type
metadataDefinitions = append(metadataDefinitions, name)
usedTypeNames = append(usedTypeNames, typeNamesUsedInStruct(structType)...)
structType := extractStructType(spec.Type)
if structType == nil {
continue
}
metadataDefinitions = append(metadataDefinitions, name)
usedTypeNames = append(usedTypeNames, typeNamesUsedInStruct(structType)...)
}
}
return metadataDefinitions, usedTypeNames, nil
}
func extractStructType(exp ast.Expr) *ast.StructType {
var structType *ast.StructType
switch ty := exp.(type) {
case *ast.StructType:
// this is a standard definition:
// type FooMetadata struct { ... }
structType = ty
case *ast.Ident:
if ty.Obj == nil {
return nil
}
// this might be a type created from another type:
// type FooMetadata BarMetadata
// ... but we need to check that the other type definition is a struct type
typeSpec, ok := ty.Obj.Decl.(*ast.TypeSpec)
if !ok {
return nil
}
nestedStructType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return nil
}
structType = nestedStructType
}
return structType
}
func typeNamesUsedInStruct(structType *ast.StructType) []string {
// recursively find all type names used in the struct type
var names []string
@@ -144,7 +176,6 @@ func typeNamesUsedInStruct(structType *ast.StructType) []string {
func isMetadataTypeCandidate(name string) bool {
return len(name) > 0 &&
strings.HasSuffix(name, "Metadata") &&
unicode.IsUpper(rune(name[0])) && // must be exported
!metadataExceptions.Has(name)
!knownNonMetadataTypeNames.Has(name)
}
@@ -0,0 +1,41 @@
package packagemetadata
import (
"testing"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDiscoverTypeNames_byExample(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "go case",
want: "ApkDBEntry",
},
{
name: "type shadowed with another type",
want: "RpmDBEntry",
},
{
name: "type shadows another type",
want: "RpmArchive",
},
}
got, err := DiscoverTypeNames()
require.NotEmpty(t, got)
gotSet := strset.New(got...)
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.True(t, gotSet.Has(tt.want))
require.NoError(t, err)
})
}
}
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"strings"
"github.com/dave/jennifer/jen"
@@ -22,6 +23,10 @@ func main() {
panic(fmt.Errorf("unable to get all metadata type names: %w", err))
}
// for _, typeName := range typeNames {
// fmt.Printf(" - %s\n", typeName)
//}
fmt.Printf("updating package metadata type list with %+v types\n", len(typeNames))
f := jen.NewFile("packagemetadata")
@@ -45,6 +50,12 @@ func main() {
if err != nil {
panic(fmt.Errorf("unable to open file: %w", err))
}
// fix a little whitespacing
rendered = strings.ReplaceAll(rendered, ",", ",\n")
rendered = strings.ReplaceAll(rendered, "[]any{", "[]any{\n")
rendered = strings.ReplaceAll(rendered, "}}\n}", "},\n}\n}")
_, err = fh.WriteString(rendered)
if err != nil {
panic(fmt.Errorf("unable to write file: %w", err))
+39 -1
View File
@@ -6,5 +6,43 @@ import "github.com/anchore/syft/syft/pkg"
// AllTypes returns a list of all pkg metadata types that syft supports (that are represented in the pkg.Package.Metadata field).
func AllTypes() []any {
return []any{pkg.AlpmMetadata{}, pkg.ApkMetadata{}, pkg.BinaryMetadata{}, pkg.CargoPackageMetadata{}, pkg.CocoapodsMetadata{}, pkg.ConanLockMetadata{}, pkg.ConanMetadata{}, pkg.DartPubMetadata{}, pkg.DotnetDepsMetadata{}, pkg.DotnetPortableExecutableMetadata{}, pkg.DpkgMetadata{}, pkg.GemMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.HackageMetadata{}, pkg.JavaMetadata{}, pkg.KbPackageMetadata{}, pkg.LinuxKernelMetadata{}, pkg.LinuxKernelModuleMetadata{}, pkg.MixLockMetadata{}, pkg.NixStoreMetadata{}, pkg.NpmPackageJSONMetadata{}, pkg.NpmPackageLockJSONMetadata{}, pkg.PhpComposerJSONMetadata{}, pkg.PortageMetadata{}, pkg.PythonPackageMetadata{}, pkg.PythonPipfileLockMetadata{}, pkg.PythonRequirementsMetadata{}, pkg.RDescriptionFileMetadata{}, pkg.RebarLockMetadata{}, pkg.RpmMetadata{}, pkg.SwiftPackageManagerMetadata{}}
return []any{
pkg.AlpmDBEntry{},
pkg.ApkDBEntry{},
pkg.BinarySignature{},
pkg.CocoaPodfileLockEntry{},
pkg.ConanLockEntry{},
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.DartPubspecLockEntry{},
pkg.DotnetDepsEntry{},
pkg.DotnetPortableExecutableEntry{},
pkg.DpkgDBEntry{},
pkg.ElixirMixLockEntry{},
pkg.ErlangRebarLockEntry{},
pkg.GolangBinaryBuildinfoEntry{},
pkg.GolangModuleEntry{},
pkg.HackageStackYamlEntry{},
pkg.HackageStackYamlLockEntry{},
pkg.JavaArchive{},
pkg.LinuxKernel{},
pkg.LinuxKernelModule{},
pkg.MicrosoftKbPatch{},
pkg.NixStoreEntry{},
pkg.NpmPackage{},
pkg.NpmPackageLockEntry{},
pkg.PhpComposerInstalledEntry{},
pkg.PhpComposerLockEntry{},
pkg.PortageEntry{},
pkg.PythonPackage{},
pkg.PythonPipfileLockEntry{},
pkg.PythonRequirementsEntry{},
pkg.RDescription{},
pkg.RpmArchive{},
pkg.RpmDBEntry{},
pkg.RubyGemspec{},
pkg.RustBinaryAuditEntry{},
pkg.RustCargoLockEntry{},
pkg.SwiftPackageManagerResolvedEntry{},
}
}
+130 -1
View File
@@ -2,12 +2,141 @@ package packagemetadata
import (
"reflect"
"strings"
"github.com/anchore/syft/syft/pkg"
)
func AllNames() []string {
type jsonType struct {
ty any
name string
legacyNames []string
noLookupLegacyName string // legacy name that conflict with other types, thus should not affect the lookup
}
func jsonNames(ty any, name string, legacyNames ...string) jsonType {
return jsonType{
ty: ty,
name: name,
legacyNames: expandLegacyNameVariants(legacyNames...),
}
}
func jsonNamesWithoutLookup(ty any, name string, noLookupLegacyName string) jsonType {
return jsonType{
ty: ty,
name: name,
noLookupLegacyName: noLookupLegacyName,
}
}
type jsonTypeMapping struct {
typeToName map[reflect.Type]string
typeToLegacyName map[reflect.Type]string
nameToType map[string]reflect.Type
}
func makeJSONTypes(types ...jsonType) jsonTypeMapping {
out := jsonTypeMapping{
typeToName: make(map[reflect.Type]string),
typeToLegacyName: make(map[reflect.Type]string),
nameToType: make(map[string]reflect.Type),
}
for _, t := range types {
typ := reflect.TypeOf(t.ty)
out.typeToName[typ] = t.name
if len(t.noLookupLegacyName) > 0 {
out.typeToLegacyName[typ] = t.noLookupLegacyName
} else if len(t.legacyNames) > 0 {
out.typeToLegacyName[typ] = t.legacyNames[0]
}
out.nameToType[strings.ToLower(t.name)] = typ
for _, name := range t.legacyNames {
out.nameToType[strings.ToLower(name)] = typ
}
}
return out
}
// jsonNameFromType is lookup of all known package metadata types to their current JSON name and all previously known aliases.
// It is important that if a name needs to change that the old name is kept in this map (as an alias) for backwards
// compatibility to support decoding older JSON documents.
var jsonTypes = makeJSONTypes(
jsonNames(pkg.AlpmDBEntry{}, "alpm-db-entry", "AlpmMetadata"),
jsonNames(pkg.ApkDBEntry{}, "apk-db-entry", "ApkMetadata"),
jsonNames(pkg.BinarySignature{}, "binary-signature", "BinaryMetadata"),
jsonNames(pkg.CocoaPodfileLockEntry{}, "cocoa-podfile-lock-entry", "CocoapodsMetadataType"),
jsonNames(pkg.ConanLockEntry{}, "c-conan-lock-entry", "ConanLockMetadataType"),
jsonNames(pkg.ConanfileEntry{}, "c-conan-file-entry", "ConanMetadataType"),
jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"),
jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"),
jsonNames(pkg.DotnetDepsEntry{}, "dotnet-deps-entry", "DotnetDepsMetadata"),
jsonNames(pkg.DotnetPortableExecutableEntry{}, "dotnet-portable-executable-entry"),
jsonNames(pkg.DpkgDBEntry{}, "dpkg-db-entry", "DpkgMetadata"),
jsonNames(pkg.RubyGemspec{}, "ruby-gemspec", "GemMetadata"),
jsonNames(pkg.GolangBinaryBuildinfoEntry{}, "go-module-buildinfo-entry", "GolangBinMetadata", "GolangMetadata"),
jsonNames(pkg.GolangModuleEntry{}, "go-module-entry", "GolangModMetadata"),
jsonNames(pkg.HackageStackYamlLockEntry{}, "haskell-hackage-stack-lock-entry", "HackageMetadataType"),
jsonNamesWithoutLookup(pkg.HackageStackYamlEntry{}, "haskell-hackage-stack-entry", "HackageMetadataType"), // the legacy value is split into two types, where the other is preferred
jsonNames(pkg.JavaArchive{}, "java-archive", "JavaMetadata"),
jsonNames(pkg.MicrosoftKbPatch{}, "microsoft-kb-patch", "KbPatchMetadata"),
jsonNames(pkg.LinuxKernel{}, "linux-kernel-archive", "LinuxKernel"),
jsonNames(pkg.LinuxKernelModule{}, "linux-kernel-module", "LinuxKernelModule"),
jsonNames(pkg.ElixirMixLockEntry{}, "elixir-mix-lock-entry", "MixLockMetadataType"),
jsonNames(pkg.NixStoreEntry{}, "nix-store-entry", "NixStoreMetadata"),
jsonNames(pkg.NpmPackage{}, "javascript-npm-package", "NpmPackageJsonMetadata"),
jsonNames(pkg.NpmPackageLockEntry{}, "javascript-npm-package-lock-entry", "NpmPackageLockJsonMetadata"),
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),
jsonNames(pkg.PythonRequirementsEntry{}, "python-pip-requirements-entry", "PythonRequirementsMetadata"),
jsonNames(pkg.ErlangRebarLockEntry{}, "erlang-rebar-lock-entry", "RebarLockMetadataType"),
jsonNames(pkg.RDescription{}, "r-description", "RDescriptionFileMetadataType"),
jsonNames(pkg.RpmDBEntry{}, "rpm-db-entry", "RpmMetadata", "RpmdbMetadata"),
jsonNamesWithoutLookup(pkg.RpmArchive{}, "rpm-archive", "RpmMetadata"), // the legacy value is split into two types, where the other is preferred
jsonNames(pkg.SwiftPackageManagerResolvedEntry{}, "swift-package-manager-lock-entry", "SwiftPackageManagerMetadata"),
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
)
func expandLegacyNameVariants(names ...string) []string {
var candidates []string
for _, name := range names {
candidates = append(candidates, name)
if strings.HasSuffix(name, "MetadataType") {
candidates = append(candidates, strings.TrimSuffix(name, "Type"))
} else if strings.HasSuffix(name, "Metadata") {
candidates = append(candidates, name+"Type")
}
}
return candidates
}
func AllTypeNames() []string {
names := make([]string, 0)
for _, t := range AllTypes() {
names = append(names, reflect.TypeOf(t).Name())
}
return names
}
func JSONName(metadata any) string {
if name, exists := jsonTypes.typeToName[reflect.TypeOf(metadata)]; exists {
return name
}
return ""
}
func JSONLegacyName(metadata any) string {
if name, exists := jsonTypes.typeToLegacyName[reflect.TypeOf(metadata)]; exists {
return name
}
return JSONName(metadata)
}
func ReflectTypeFromJSONName(name string) reflect.Type {
name = strings.ToLower(name)
return jsonTypes.nameToType[name]
}
+469 -1
View File
@@ -1,11 +1,14 @@
package packagemetadata
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
)
func TestAllNames(t *testing.T) {
@@ -14,7 +17,7 @@ func TestAllNames(t *testing.T) {
expected, err := DiscoverTypeNames()
require.NoError(t, err)
actual := AllNames()
actual := AllTypeNames()
// ensure that the codebase (from ast analysis) reflects the latest code generated state
if !assert.ElementsMatch(t, expected, actual) {
@@ -22,4 +25,469 @@ func TestAllNames(t *testing.T) {
t.Log("did you add a new pkg.*Metadata type without updating the JSON schema?")
t.Log("if so, you need to update the schema version and regenerate the JSON schema (make generate-json-schema)")
}
for _, ty := range AllTypes() {
assert.NotEmpty(t, JSONName(ty), "metadata type %q does not have a JSON name", reflect.TypeOf(ty).Name())
}
}
func TestReflectTypeFromJSONName(t *testing.T) {
tests := []struct {
name string
lookup string
wantRecord reflect.Type
}{
{
name: "exact match on ID",
lookup: "rust-cargo-lock-entry",
wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}),
},
{
name: "exact match on former name",
lookup: "RustCargoPackageMetadata",
wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}),
},
{
name: "case insensitive on ID",
lookup: "RUST-CARGO-lock-entrY",
wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}),
},
{
name: "case insensitive on alias",
lookup: "rusTcArgopacKagEmEtadATa",
wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}),
},
{
name: "consistent override",
// there are two correct answers for this -- we should always get the same answer.
lookup: "HackageMetadataType",
wantRecord: reflect.TypeOf(pkg.HackageStackYamlLockEntry{}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ReflectTypeFromJSONName(tt.lookup)
require.NotNil(t, got)
assert.Equal(t, tt.wantRecord.Name(), got.Name())
})
}
}
func TestReflectTypeFromJSONName_LegacyValues(t *testing.T) {
testCases := []struct {
name string
input string
expected reflect.Type
}{
// these cases are always 1:1
{
name: "map pkg.AlpmDBEntry struct type",
input: "AlpmMetadata",
expected: reflect.TypeOf(pkg.AlpmDBEntry{}),
},
{
name: "map pkg.ApkDBEntry struct type",
input: "ApkMetadata",
expected: reflect.TypeOf(pkg.ApkDBEntry{}),
},
{
name: "map pkg.BinarySignature struct type",
input: "BinaryMetadata",
expected: reflect.TypeOf(pkg.BinarySignature{}),
},
{
name: "map pkg.CocoaPodfileLockEntry struct type",
input: "CocoapodsMetadataType",
expected: reflect.TypeOf(pkg.CocoaPodfileLockEntry{}),
},
{
name: "map pkg.ConanLockEntry struct type",
input: "ConanLockMetadataType",
expected: reflect.TypeOf(pkg.ConanLockEntry{}),
},
{
name: "map pkg.ConanfileEntry struct type",
input: "ConanMetadataType",
expected: reflect.TypeOf(pkg.ConanfileEntry{}),
},
{
name: "map pkg.DartPubspecLockEntry struct type",
input: "DartPubMetadata",
expected: reflect.TypeOf(pkg.DartPubspecLockEntry{}),
},
{
name: "map pkg.DotnetDepsEntry struct type",
input: "DotnetDepsMetadata",
expected: reflect.TypeOf(pkg.DotnetDepsEntry{}),
},
{
name: "map pkg.DpkgDBEntry struct type",
input: "DpkgMetadata",
expected: reflect.TypeOf(pkg.DpkgDBEntry{}),
},
{
name: "map pkg.RubyGemspec struct type",
input: "GemMetadata",
expected: reflect.TypeOf(pkg.RubyGemspec{}),
},
{
name: "map pkg.GolangBinaryBuildinfoEntry struct type",
input: "GolangBinMetadata",
expected: reflect.TypeOf(pkg.GolangBinaryBuildinfoEntry{}),
},
{
name: "map pkg.GolangModuleEntry struct type",
input: "GolangModMetadata",
expected: reflect.TypeOf(pkg.GolangModuleEntry{}),
},
{
name: "map pkg.JavaArchive struct type",
input: "JavaMetadata",
expected: reflect.TypeOf(pkg.JavaArchive{}),
},
{
name: "map pkg.MicrosoftKbPatch struct type",
input: "KbPatchMetadata",
expected: reflect.TypeOf(pkg.MicrosoftKbPatch{}),
},
{
name: "map pkg.LinuxKernel struct type",
input: "LinuxKernel",
expected: reflect.TypeOf(pkg.LinuxKernel{}),
},
{
name: "map pkg.LinuxKernelModule struct type",
input: "LinuxKernelModule",
expected: reflect.TypeOf(pkg.LinuxKernelModule{}),
},
{
name: "map pkg.ElixirMixLockEntry struct type",
input: "MixLockMetadataType",
expected: reflect.TypeOf(pkg.ElixirMixLockEntry{}),
},
{
name: "map pkg.NixStoreEntry struct type",
input: "NixStoreMetadata",
expected: reflect.TypeOf(pkg.NixStoreEntry{}),
},
{
name: "map pkg.NpmPackage struct type",
input: "NpmPackageJsonMetadata",
expected: reflect.TypeOf(pkg.NpmPackage{}),
},
{
name: "map pkg.NpmPackageLockEntry struct type",
input: "NpmPackageLockJsonMetadata",
expected: reflect.TypeOf(pkg.NpmPackageLockEntry{}),
},
{
name: "map pkg.PortageEntry struct type",
input: "PortageMetadata",
expected: reflect.TypeOf(pkg.PortageEntry{}),
},
{
name: "map pkg.PythonPackage struct type",
input: "PythonPackageMetadata",
expected: reflect.TypeOf(pkg.PythonPackage{}),
},
{
name: "map pkg.PythonPipfileLockEntry struct type",
input: "PythonPipfileLockMetadata",
expected: reflect.TypeOf(pkg.PythonPipfileLockEntry{}),
},
{
name: "map pkg.PythonRequirementsEntry struct type",
input: "PythonRequirementsMetadata",
expected: reflect.TypeOf(pkg.PythonRequirementsEntry{}),
},
{
name: "map pkg.ErlangRebarLockEntry struct type",
input: "RebarLockMetadataType",
expected: reflect.TypeOf(pkg.ErlangRebarLockEntry{}),
},
{
name: "map pkg.RDescription struct type",
input: "RDescriptionFileMetadataType",
expected: reflect.TypeOf(pkg.RDescription{}),
},
{
name: "map pkg.RpmDBEntry struct type",
input: "RpmdbMetadata",
expected: reflect.TypeOf(pkg.RpmDBEntry{}),
},
// these cases are 1:many
{
name: "map pkg.RpmDBEntry struct type - overlap with RpmArchiveMetadata",
input: "RpmMetadata",
// this used to be shared as a use case for both RpmArchive and RpmDBEntry
// from a data-shape perspective either would be equally correct
// however, the RPMDBMetadata has been around longer and may have been more widely used
// so we'll map to that type for backwards compatibility.
expected: reflect.TypeOf(pkg.RpmDBEntry{}),
},
{
name: "map pkg.HackageStackYamlLockEntry struct type - overlap with HackageStack*Metadata",
input: "HackageMetadataType",
// this used to be shared as a use case for both HackageStackYamlLockEntry and HackageStackYamlEntry
// but the HackageStackYamlLockEntry maps most closely to the original data shape.
expected: reflect.TypeOf(pkg.HackageStackYamlLockEntry{}),
},
{
name: "map pkg.PhpComposerLockEntry struct type",
input: "PhpComposerJsonMetadata",
// this used to be shared as a use case for both PhpComposerLockEntry and PhpComposerInstalledEntry
// neither of these is more correct over the other. These parsers were also introduced at the same time.
expected: reflect.TypeOf(pkg.PhpComposerLockEntry{}),
},
{
name: "map pkg.RustCargoLockEntry struct type",
input: "RustCargoPackageMetadata",
// this used to be shared as a use case for both RustCargoLockEntry and RustBinaryAuditEntry
// neither of these is more correct over the other.
expected: reflect.TypeOf(pkg.RustCargoLockEntry{}),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := ReflectTypeFromJSONName(testCase.input)
assert.Equal(t, testCase.expected.Name(), result.Name())
})
}
}
func Test_JSONName_JSONLegacyName(t *testing.T) {
// note: these are all the types and names covered by the v11.x and v12.x JSON schemas
tests := []struct {
name string
metadata any
expectedJSONName string
expectedLegacyName string
}{
{
name: "AlpmMetadata",
metadata: pkg.AlpmDBEntry{},
expectedJSONName: "alpm-db-entry",
expectedLegacyName: "AlpmMetadata",
},
{
name: "ApkMetadata",
metadata: pkg.ApkDBEntry{},
expectedJSONName: "apk-db-entry",
expectedLegacyName: "ApkMetadata",
},
{
name: "BinaryMetadata",
metadata: pkg.BinarySignature{},
expectedJSONName: "binary-signature",
expectedLegacyName: "BinaryMetadata",
},
{
name: "CocoapodsMetadata",
metadata: pkg.CocoaPodfileLockEntry{},
expectedJSONName: "cocoa-podfile-lock-entry",
expectedLegacyName: "CocoapodsMetadataType",
},
{
name: "ConanLockMetadata",
metadata: pkg.ConanLockEntry{},
expectedJSONName: "c-conan-lock-entry",
expectedLegacyName: "ConanLockMetadataType",
},
{
name: "ConanMetadata",
metadata: pkg.ConanfileEntry{},
expectedJSONName: "c-conan-file-entry",
expectedLegacyName: "ConanMetadataType",
},
{
name: "DartPubMetadata",
metadata: pkg.DartPubspecLockEntry{},
expectedJSONName: "dart-pubspec-lock-entry",
expectedLegacyName: "DartPubMetadata",
},
{
name: "DotnetDepsMetadata",
metadata: pkg.DotnetDepsEntry{},
expectedJSONName: "dotnet-deps-entry",
expectedLegacyName: "DotnetDepsMetadata",
},
{
name: "DotnetPortableExecutableMetadata",
metadata: pkg.DotnetPortableExecutableEntry{},
expectedJSONName: "dotnet-portable-executable-entry",
expectedLegacyName: "dotnet-portable-executable-entry", // note: the legacy name should never be blank if it didn't exist pre v11.x
},
{
name: "DpkgMetadata",
metadata: pkg.DpkgDBEntry{},
expectedJSONName: "dpkg-db-entry",
expectedLegacyName: "DpkgMetadata",
},
{
name: "GemMetadata",
metadata: pkg.RubyGemspec{},
expectedJSONName: "ruby-gemspec",
expectedLegacyName: "GemMetadata",
},
{
name: "GolangBinMetadata",
metadata: pkg.GolangBinaryBuildinfoEntry{},
expectedJSONName: "go-module-buildinfo-entry",
expectedLegacyName: "GolangBinMetadata",
},
{
name: "GolangModMetadata",
metadata: pkg.GolangModuleEntry{},
expectedJSONName: "go-module-entry",
expectedLegacyName: "GolangModMetadata",
},
{
name: "HackageStackYamlLockMetadata",
metadata: pkg.HackageStackYamlLockEntry{},
expectedJSONName: "haskell-hackage-stack-lock-entry",
expectedLegacyName: "HackageMetadataType", // this is closest to the original data shape in <=v11.x schema
},
{
name: "HackageStackYamlMetadata",
metadata: pkg.HackageStackYamlEntry{},
expectedJSONName: "haskell-hackage-stack-entry",
expectedLegacyName: "HackageMetadataType", // note: this conflicts with <=v11.x schema for "haskell-hackage-stack-lock" metadata type
},
{
name: "JavaMetadata",
metadata: pkg.JavaArchive{},
expectedJSONName: "java-archive",
expectedLegacyName: "JavaMetadata",
},
{
name: "KbPatchMetadata",
metadata: pkg.MicrosoftKbPatch{},
expectedJSONName: "microsoft-kb-patch",
expectedLegacyName: "KbPatchMetadata",
},
{
name: "LinuxKernel",
metadata: pkg.LinuxKernel{},
expectedJSONName: "linux-kernel-archive",
expectedLegacyName: "LinuxKernel",
},
{
name: "LinuxKernelModule",
metadata: pkg.LinuxKernelModule{},
expectedJSONName: "linux-kernel-module",
expectedLegacyName: "LinuxKernelModule",
},
{
name: "MixLockMetadata",
metadata: pkg.ElixirMixLockEntry{},
expectedJSONName: "elixir-mix-lock-entry",
expectedLegacyName: "MixLockMetadataType",
},
{
name: "NixStoreMetadata",
metadata: pkg.NixStoreEntry{},
expectedJSONName: "nix-store-entry",
expectedLegacyName: "NixStoreMetadata",
},
{
name: "NpmPackageJSONMetadata",
metadata: pkg.NpmPackage{},
expectedJSONName: "javascript-npm-package",
expectedLegacyName: "NpmPackageJsonMetadata",
},
{
name: "NpmPackageLockJSONMetadata",
metadata: pkg.NpmPackageLockEntry{},
expectedJSONName: "javascript-npm-package-lock-entry",
expectedLegacyName: "NpmPackageLockJsonMetadata",
},
{
name: "PhpComposerLockMetadata",
metadata: pkg.PhpComposerLockEntry{},
expectedJSONName: "php-composer-lock-entry",
expectedLegacyName: "PhpComposerJsonMetadata", // note: maps to multiple entries (v11-12 breaking change)
},
{
name: "PhpComposerInstalledMetadata",
metadata: pkg.PhpComposerInstalledEntry{},
expectedJSONName: "php-composer-installed-entry",
expectedLegacyName: "PhpComposerJsonMetadata", // note: maps to multiple entries (v11-12 breaking change)
},
{
name: "PortageMetadata",
metadata: pkg.PortageEntry{},
expectedJSONName: "portage-db-entry",
expectedLegacyName: "PortageMetadata",
},
{
name: "PythonPackageMetadata",
metadata: pkg.PythonPackage{},
expectedJSONName: "python-package",
expectedLegacyName: "PythonPackageMetadata",
},
{
name: "PythonPipfileLockMetadata",
metadata: pkg.PythonPipfileLockEntry{},
expectedJSONName: "python-pipfile-lock-entry",
expectedLegacyName: "PythonPipfileLockMetadata",
},
{
name: "PythonRequirementsMetadata",
metadata: pkg.PythonRequirementsEntry{},
expectedJSONName: "python-pip-requirements-entry",
expectedLegacyName: "PythonRequirementsMetadata",
},
{
name: "RebarLockMetadata",
metadata: pkg.ErlangRebarLockEntry{},
expectedJSONName: "erlang-rebar-lock-entry",
expectedLegacyName: "RebarLockMetadataType",
},
{
name: "RDescriptionFileMetadata",
metadata: pkg.RDescription{},
expectedJSONName: "r-description",
expectedLegacyName: "RDescriptionFileMetadataType",
},
{
name: "RpmDBMetadata",
metadata: pkg.RpmDBEntry{},
expectedJSONName: "rpm-db-entry",
expectedLegacyName: "RpmMetadata", // not accurate, but how it was pre v12 of the schema
},
{
name: "RpmArchiveMetadata",
metadata: pkg.RpmArchive{},
expectedJSONName: "rpm-archive",
expectedLegacyName: "RpmMetadata", // note: conflicts with <=v11.x schema for "rpm-db-entry" metadata type
},
{
name: "SwiftPackageManagerMetadata",
metadata: pkg.SwiftPackageManagerResolvedEntry{},
expectedJSONName: "swift-package-manager-lock-entry",
expectedLegacyName: "SwiftPackageManagerMetadata",
},
{
name: "CargoPackageMetadata",
metadata: pkg.RustCargoLockEntry{},
expectedJSONName: "rust-cargo-lock-entry",
expectedLegacyName: "RustCargoPackageMetadata", // note: maps to multiple entries (v11-12 breaking change)
},
{
name: "CargoPackageMetadata (audit binary)",
metadata: pkg.RustBinaryAuditEntry{},
expectedJSONName: "rust-cargo-audit-entry",
expectedLegacyName: "RustCargoPackageMetadata", // note: maps to multiple entries (v11-12 breaking change)
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualJSONName := JSONName(test.metadata)
actualLegacyName := JSONLegacyName(test.metadata)
assert.Equal(t, test.expectedJSONName, actualJSONName, "unexpected name")
assert.Equal(t, test.expectedLegacyName, actualLegacyName, "unexpected legacy name")
})
}
}