mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
fix(dotnet): resolve packages.lock dependencies deterministically (#5210)
* fix(dotnet): resolve packages.lock dependencies within their target framework Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix(dotnet): make packages.lock package metadata deterministic across target frameworks Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: parse packages lock tests Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: update lockfile parse to read into stable structure Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: remove stale comments Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * test: new test for project entry case Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix(dotnet): key packages.lock packages case-insensitively NuGet IDs are case-insensitive, and edge lookup already treated them that way, but package identity used the exact-case name. Two spellings of one ID and version across target frameworks became two packages, and type precedence never ran between them. - packages merge on the lowercased name + version, display name comes from the winning entry - a package listing itself (under any casing) no longer produces a self edge Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
co-authored by
Alex Goodman
parent
cc326e45a6
commit
c33fa32cf1
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
|
||||
var _ generic.Parser = parseDotnetPackagesLock
|
||||
|
||||
var packagesLockTypePrecedence = []string{"Project", "Direct", "CentralTransitive", "Transitive"}
|
||||
|
||||
type dotnetPackagesLock struct {
|
||||
Version int `json:"version"`
|
||||
Dependencies map[string]map[string]dotnetPackagesLockDep `json:"dependencies"`
|
||||
@@ -31,11 +34,38 @@ type dotnetPackagesLockDep struct {
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen
|
||||
var pkgs []pkg.Package
|
||||
var pkgMap = make(map[string]pkg.Package)
|
||||
var relationships []artifact.Relationship
|
||||
type packagesLockEntry struct {
|
||||
name string
|
||||
dep dotnetPackagesLockDep
|
||||
}
|
||||
|
||||
func (e packagesLockEntry) nameVersion() string {
|
||||
return createNameAndVersion(e.name, e.dep.Resolved)
|
||||
}
|
||||
|
||||
// key identifies the package an entry describes. NuGet package IDs are case-insensitive, so two spellings of the
|
||||
// same ID and version are one package.
|
||||
func (e packagesLockEntry) key() string {
|
||||
return createNameAndVersion(strings.ToLower(e.name), e.dep.Resolved)
|
||||
}
|
||||
|
||||
type packagesLockFramework struct {
|
||||
name string
|
||||
entries []packagesLockEntry
|
||||
// byName is keyed by the lowercased package name, since NuGet package IDs are case-insensitive.
|
||||
byName map[string]packagesLockEntry
|
||||
}
|
||||
|
||||
func (f packagesLockFramework) find(name string) (packagesLockEntry, bool) {
|
||||
e, ok := f.byName[strings.ToLower(name)]
|
||||
return e, ok
|
||||
}
|
||||
|
||||
type packagesLockEdge struct {
|
||||
child, parent artifact.ID
|
||||
}
|
||||
|
||||
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
// unmarshal file
|
||||
@@ -44,70 +74,21 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
|
||||
return nil, nil, fmt.Errorf("failed to parse packages.lock.json file: %w", err)
|
||||
}
|
||||
|
||||
// collect all deps here
|
||||
allDependencies := make(map[string]dotnetPackagesLockDep)
|
||||
|
||||
var names []string
|
||||
for _, dependencies := range lockFile.Dependencies {
|
||||
for name, dep := range dependencies {
|
||||
depNameVersion := createNameAndVersion(name, dep.Resolved)
|
||||
|
||||
if slices.Contains(names, depNameVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
names = append(names, depNameVersion)
|
||||
allDependencies[depNameVersion] = dep
|
||||
}
|
||||
}
|
||||
|
||||
// sort the names so that the order of the packages is deterministic
|
||||
sort.Strings(names)
|
||||
frameworks := newPackagesLockFrameworks(lockFile)
|
||||
|
||||
// create artifact for each pkg
|
||||
for _, nameVersion := range names {
|
||||
name, _ := extractNameAndVersion(nameVersion)
|
||||
var pkgs []pkg.Package
|
||||
pkgMap := make(map[string]pkg.Package)
|
||||
|
||||
dep := allDependencies[nameVersion]
|
||||
dotnetPkg := newDotnetPackagesLockPackage(name, dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
for _, entry := range mergePackagesLockEntries(frameworks) {
|
||||
dotnetPkg := newDotnetPackagesLockPackage(entry.name, entry.dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
if dotnetPkg != nil {
|
||||
pkgs = append(pkgs, *dotnetPkg)
|
||||
pkgMap[nameVersion] = *dotnetPkg
|
||||
pkgMap[entry.key()] = *dotnetPkg
|
||||
}
|
||||
}
|
||||
|
||||
// fill up relationships
|
||||
for depNameVersion, dep := range allDependencies {
|
||||
parentPkg, ok := pkgMap[depNameVersion]
|
||||
if !ok {
|
||||
log.Debugf("package \"%s\" not found in map of all packages", depNameVersion)
|
||||
continue
|
||||
}
|
||||
|
||||
for childDepName, childDepVersion := range dep.Dependencies {
|
||||
childDepNameVersion := createNameAndVersion(childDepName, childDepVersion)
|
||||
|
||||
// try and find pkg for dependency with exact name and version
|
||||
childPkg, ok := pkgMap[childDepNameVersion]
|
||||
if !ok {
|
||||
// no exact match found, lets match on name only, lockfile will contain other version of pkg
|
||||
cpkg, ok := findPkgByName(childDepName, pkgMap)
|
||||
if !ok {
|
||||
log.Debugf("dependency \"%s\" of package \"%s\" not found in map of all packages", childDepNameVersion, depNameVersion)
|
||||
continue
|
||||
}
|
||||
|
||||
childPkg = *cpkg
|
||||
}
|
||||
|
||||
rel := artifact.Relationship{
|
||||
From: childPkg,
|
||||
To: parentPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
}
|
||||
relationships = append(relationships, rel)
|
||||
}
|
||||
}
|
||||
relationships := packagesLockRelationships(frameworks, pkgMap)
|
||||
|
||||
// sort the relationships for deterministic output
|
||||
relationship.Sort(relationships)
|
||||
@@ -115,6 +96,147 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
|
||||
return pkgs, relationships, nil
|
||||
}
|
||||
|
||||
func newPackagesLockFrameworks(lockFile dotnetPackagesLock) []packagesLockFramework {
|
||||
var frameworks []packagesLockFramework
|
||||
|
||||
for _, frameworkName := range slices.Sorted(maps.Keys(lockFile.Dependencies)) {
|
||||
deps := lockFile.Dependencies[frameworkName]
|
||||
framework := packagesLockFramework{
|
||||
name: frameworkName,
|
||||
byName: make(map[string]packagesLockEntry, len(deps)),
|
||||
}
|
||||
|
||||
for _, name := range slices.Sorted(maps.Keys(deps)) {
|
||||
entry := packagesLockEntry{name: name, dep: deps[name]}
|
||||
framework.entries = append(framework.entries, entry)
|
||||
framework.byName[strings.ToLower(name)] = entry
|
||||
}
|
||||
|
||||
frameworks = append(frameworks, framework)
|
||||
}
|
||||
|
||||
return frameworks
|
||||
}
|
||||
|
||||
func mergePackagesLockEntries(frameworks []packagesLockFramework) []packagesLockEntry {
|
||||
merged := make(map[string]packagesLockEntry)
|
||||
|
||||
for _, framework := range frameworks {
|
||||
for _, entry := range framework.entries {
|
||||
key := entry.key()
|
||||
if existing, ok := merged[key]; ok && !isMoreDirect(entry.dep.Type, existing.dep.Type) {
|
||||
continue
|
||||
}
|
||||
merged[key] = entry
|
||||
}
|
||||
}
|
||||
|
||||
var entries []packagesLockEntry
|
||||
for _, key := range slices.Sorted(maps.Keys(merged)) {
|
||||
entries = append(entries, merged[key])
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func isMoreDirect(a, b string) bool {
|
||||
rankA, rankB := packagesLockTypeRank(a), packagesLockTypeRank(b)
|
||||
if rankA != rankB {
|
||||
return rankA < rankB
|
||||
}
|
||||
return a < b
|
||||
}
|
||||
|
||||
func packagesLockTypeRank(t string) int {
|
||||
if i := slices.Index(packagesLockTypePrecedence, t); i >= 0 {
|
||||
return i
|
||||
}
|
||||
return len(packagesLockTypePrecedence)
|
||||
}
|
||||
|
||||
func packagesLockRelationships(frameworks []packagesLockFramework, pkgMap map[string]pkg.Package) []artifact.Relationship {
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
frameworksByName := make(map[string]packagesLockFramework, len(frameworks))
|
||||
for _, framework := range frameworks {
|
||||
frameworksByName[framework.name] = framework
|
||||
}
|
||||
|
||||
seen := make(map[packagesLockEdge]struct{})
|
||||
for _, framework := range frameworks {
|
||||
base, hasBase := frameworksByName[baseFrameworkName(framework.name)]
|
||||
if !hasBase || base.name == framework.name {
|
||||
base = packagesLockFramework{}
|
||||
}
|
||||
|
||||
for _, entry := range framework.entries {
|
||||
parentPkg, ok := pkgMap[entry.key()]
|
||||
if !ok {
|
||||
log.Debugf("package %q not found in map of all packages", entry.nameVersion())
|
||||
continue
|
||||
}
|
||||
|
||||
relationships = append(relationships, packagesLockEntryRelationships(entry, parentPkg, framework, base, pkgMap, seen)...)
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
func packagesLockEntryRelationships(entry packagesLockEntry, parentPkg pkg.Package, framework, base packagesLockFramework, pkgMap map[string]pkg.Package, seen map[packagesLockEdge]struct{}) []artifact.Relationship {
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
for _, childName := range slices.Sorted(maps.Keys(entry.dep.Dependencies)) {
|
||||
child, ok := findPackagesLockDependency(childName, framework, base)
|
||||
if !ok {
|
||||
log.Debugf("dependency %q of package %q not found under target framework %q", childName, entry.nameVersion(), framework.name)
|
||||
continue
|
||||
}
|
||||
|
||||
childPkg, ok := pkgMap[child.key()]
|
||||
if !ok {
|
||||
log.Debugf("package %q not found in map of all packages", child.nameVersion())
|
||||
continue
|
||||
}
|
||||
|
||||
// a package listing itself (possibly under another casing) is not a dependency
|
||||
if childPkg.ID() == parentPkg.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
key := packagesLockEdge{child: childPkg.ID(), parent: parentPkg.ID()}
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: childPkg,
|
||||
To: parentPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
})
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
// findPackagesLockDependency finds the entry a dependency edge points at. The version on an edge is only the lower
|
||||
// bound of a version range, so the edge resolves to whatever version its target framework resolved. Runtime-specific
|
||||
// sections such as "net8.0/win-x64" may reference packages listed only in their base framework ("net8.0"). An edge
|
||||
// that resolves nowhere is dropped rather than guessed from another target framework.
|
||||
func findPackagesLockDependency(name string, framework, base packagesLockFramework) (packagesLockEntry, bool) {
|
||||
if entry, ok := framework.find(name); ok {
|
||||
return entry, true
|
||||
}
|
||||
return base.find(name)
|
||||
}
|
||||
|
||||
// baseFrameworkName returns the target framework of a runtime-specific section, such as "net8.0" for "net8.0/win-x64".
|
||||
func baseFrameworkName(frameworkName string) string {
|
||||
name, _, _ := strings.Cut(frameworkName, "/")
|
||||
return name
|
||||
}
|
||||
|
||||
func newDotnetPackagesLockPackage(name string, dep dotnetPackagesLockDep, locations ...file.Location) *pkg.Package {
|
||||
metadata := pkg.DotnetPackagesLockEntry{
|
||||
Name: name,
|
||||
@@ -150,14 +272,3 @@ func packagesLockPackageURL(name, version string) string {
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func findPkgByName(pkgName string, pkgMap map[string]pkg.Package) (*pkg.Package, bool) {
|
||||
for pkgNameVersion, pkg := range pkgMap {
|
||||
name, _ := extractNameAndVersion(pkgNameVersion)
|
||||
if name == pkgName {
|
||||
return &pkg, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package dotnet
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -197,3 +200,304 @@ func TestParseDotnetPackagesLock(t *testing.T) {
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParseDotnetPackagesLock_multipleTargetFrameworks(t *testing.T) {
|
||||
fixture := "testdata/packages.lock-multi-framework.json"
|
||||
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
myLibPkg := pkg.Package{
|
||||
Name: "MyLib",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:nuget/MyLib@1.0.0",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "MyLib",
|
||||
Version: "1.0.0",
|
||||
ContentHash: "mylibhash==",
|
||||
Type: "Direct",
|
||||
},
|
||||
}
|
||||
|
||||
log4net2Pkg := pkg.Package{
|
||||
Name: "log4net",
|
||||
Version: "2.0.5",
|
||||
PURL: "pkg:nuget/log4net@2.0.5",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "log4net",
|
||||
Version: "2.0.5",
|
||||
ContentHash: "log4net205hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
log4net1Pkg := pkg.Package{
|
||||
Name: "log4net",
|
||||
Version: "1.2.15",
|
||||
PURL: "pkg:nuget/log4net@1.2.15",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "log4net",
|
||||
Version: "1.2.15",
|
||||
ContentHash: "log4net1215hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
// resolved to the same version under both frameworks (spelled "newtonsoft.json" under one), but declared "Direct" by
|
||||
// only one of them
|
||||
newtonsoftPkg := pkg.Package{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
ContentHash: "newtonsoft1303hash==",
|
||||
Type: "Direct",
|
||||
},
|
||||
}
|
||||
|
||||
// only listed under the runtime-specific "net8.0/win-x64" section, and depends on a package from "net8.0"
|
||||
myLibNativePkg := pkg.Package{
|
||||
Name: "runtime.win-x64.MyLib.Native",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:nuget/runtime.win-x64.MyLib.Native@1.0.0",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "runtime.win-x64.MyLib.Native",
|
||||
Version: "1.0.0",
|
||||
ContentHash: "mylibnativehash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
// only listed under "net8.0", so the "netstandard2.0" edge to it must be dropped rather than guessed
|
||||
serilogPkg := pkg.Package{
|
||||
Name: "Serilog",
|
||||
Version: "3.1.1",
|
||||
PURL: "pkg:nuget/Serilog@3.1.1",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "Serilog",
|
||||
Version: "3.1.1",
|
||||
ContentHash: "serilog311hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
myLibPkg,
|
||||
newtonsoftPkg,
|
||||
log4net1Pkg,
|
||||
log4net2Pkg,
|
||||
myLibNativePkg,
|
||||
serilogPkg,
|
||||
}
|
||||
|
||||
// the same package is resolved to a different version per target framework, so both edges must be captured, while
|
||||
// the Newtonsoft.Json edge declared by both frameworks must appear only once
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
{
|
||||
From: log4net1Pkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: log4net2Pkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: log4net2Pkg,
|
||||
To: myLibNativePkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: newtonsoftPkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParseDotnetPackagesLock_projectReference(t *testing.T) {
|
||||
fixture := "testdata/packages.lock-project-reference.json"
|
||||
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
newtonsoftPkg := pkg.Package{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
ContentHash: "newtonsoft1303hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
// a project reference has no resolved version or content hash
|
||||
myLibPkg := pkg.Package{
|
||||
Name: "mylib",
|
||||
PURL: "pkg:nuget/mylib",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "mylib",
|
||||
Type: "Project",
|
||||
},
|
||||
}
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
newtonsoftPkg,
|
||||
myLibPkg,
|
||||
}
|
||||
|
||||
// project references declare their dependencies as version ranges, which resolve to the version the target
|
||||
// framework resolved
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
{
|
||||
From: newtonsoftPkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func Test_findPackagesLockDependency(t *testing.T) {
|
||||
frameworks := newPackagesLockFrameworks(dotnetPackagesLock{
|
||||
Dependencies: map[string]map[string]dotnetPackagesLockDep{
|
||||
"net8.0": {
|
||||
"log4net": {Resolved: "2.0.5"},
|
||||
},
|
||||
"net8.0/win-x64": {
|
||||
"runtime.win-x64.Native": {Resolved: "1.0.0"},
|
||||
},
|
||||
"netstandard2.0": {
|
||||
"log4net": {Resolved: "1.2.15"},
|
||||
"Newtonsoft.Json": {Resolved: "13.0.3"},
|
||||
},
|
||||
},
|
||||
})
|
||||
net8, rid, netstandard := frameworks[0], frameworks[1], frameworks[2]
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
depName string
|
||||
framework packagesLockFramework
|
||||
base packagesLockFramework
|
||||
wantVersion string
|
||||
wantFound bool
|
||||
}{
|
||||
{
|
||||
name: "resolves to the version pinned by its own target framework",
|
||||
depName: "log4net",
|
||||
framework: netstandard,
|
||||
wantVersion: "1.2.15",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "package names are matched case-insensitively",
|
||||
depName: "LOG4NET",
|
||||
framework: net8,
|
||||
wantVersion: "2.0.5",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "runtime-specific sections fall back to their base target framework",
|
||||
depName: "log4net",
|
||||
framework: rid,
|
||||
base: net8,
|
||||
wantVersion: "2.0.5",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "a package absent from the target framework is not guessed from another one",
|
||||
depName: "Newtonsoft.Json",
|
||||
framework: net8,
|
||||
wantFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, found := findPackagesLockDependency(tt.depName, tt.framework, tt.base)
|
||||
require.Equal(t, tt.wantFound, found)
|
||||
if !tt.wantFound {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.wantVersion, got.dep.Resolved)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergePackagesLockEntries_typePrecedence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
types map[string]string // target framework -> type
|
||||
wantType string
|
||||
}{
|
||||
{
|
||||
name: "direct wins over transitive",
|
||||
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "Direct"},
|
||||
wantType: "Direct",
|
||||
},
|
||||
{
|
||||
name: "central transitive wins over transitive regardless of framework order",
|
||||
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "CentralTransitive"},
|
||||
wantType: "CentralTransitive",
|
||||
},
|
||||
{
|
||||
name: "central transitive wins over transitive in the reverse framework order",
|
||||
types: map[string]string{"net8.0": "CentralTransitive", "netstandard2.0": "Transitive"},
|
||||
wantType: "CentralTransitive",
|
||||
},
|
||||
{
|
||||
name: "direct wins over central transitive",
|
||||
types: map[string]string{"net472": "CentralTransitive", "net8.0": "Direct", "netstandard2.0": "Transitive"},
|
||||
wantType: "Direct",
|
||||
},
|
||||
{
|
||||
name: "known types win over unknown ones",
|
||||
types: map[string]string{"net8.0": "Unknown", "netstandard2.0": "Transitive"},
|
||||
wantType: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lockFile := dotnetPackagesLock{Dependencies: map[string]map[string]dotnetPackagesLockDep{}}
|
||||
for framework, depType := range tt.types {
|
||||
lockFile.Dependencies[framework] = map[string]dotnetPackagesLockDep{
|
||||
"Newtonsoft.Json": {Type: depType, Resolved: "13.0.3"},
|
||||
}
|
||||
}
|
||||
|
||||
entries := mergePackagesLockEntries(newPackagesLockFrameworks(lockFile))
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, tt.wantType, entries[0].dep.Type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"MyLib": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "mylibhash==",
|
||||
"dependencies": {
|
||||
"log4net": "2.0.5",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"log4net": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.5",
|
||||
"contentHash": "log4net205hash=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[13.0.3, )",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "newtonsoft1303hash=="
|
||||
},
|
||||
"Serilog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.1",
|
||||
"contentHash": "serilog311hash=="
|
||||
}
|
||||
},
|
||||
"net8.0/win-x64": {
|
||||
"runtime.win-x64.MyLib.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "mylibnativehash==",
|
||||
"dependencies": {
|
||||
"log4net": "2.0.0",
|
||||
"RUNTIME.WIN-X64.MYLIB.NATIVE": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"netstandard2.0": {
|
||||
"MyLib": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "mylibhash==",
|
||||
"dependencies": {
|
||||
"log4net": "1.2.15",
|
||||
"newtonsoft.json": "13.0.3",
|
||||
"Serilog": "3.1.1"
|
||||
}
|
||||
},
|
||||
"log4net": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.2.15",
|
||||
"contentHash": "log4net1215hash=="
|
||||
},
|
||||
"newtonsoft.json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "newtonsoft1303hash=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "newtonsoft1303hash=="
|
||||
},
|
||||
"mylib": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "[13.0.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user