merge multiple targets for the same dotnet package (#3869)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-05-08 11:28:08 -04:00 committed by GitHub
parent 00c4a4e72a
commit 1574fb20ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 201 additions and 27 deletions

View File

@ -149,7 +149,12 @@ func isRuntimePackageLocation(loc file.Location) (string, bool) {
func partitionPEs(depJsons []logicalDepsJSON, peFiles []logicalPE) ([]logicalDepsJSON, []logicalPE, []logicalDepsJSON) {
// sort deps.json paths from longest to shortest. This is so we are processing the most specific match first.
sort.Slice(depJsons, func(i, j int) bool {
return len(depJsons[i].Location.RealPath) > len(depJsons[j].Location.RealPath)
return depJsons[i].Location.RealPath > depJsons[j].Location.RealPath
})
// we should be processing PE files in a stable order
sort.Slice(peFiles, func(i, j int) bool {
return peFiles[i].Location.RealPath > peFiles[j].Location.RealPath
})
peFilesByPath := make(map[file.Coordinates][]logicalPE)
@ -321,8 +326,7 @@ func relationshipsFromLogicalDepsJSON(doc logicalDepsJSON, pkgMap map[string]pkg
if lp.Targets == nil {
continue
}
for depName, depVersion := range lp.Targets.Dependencies {
depNameVersion := createNameAndVersion(depName, depVersion)
for _, depNameVersion := range lp.dependencyNameVersions() {
thisPkg, ok := pkgMap[lp.NameVersion]
if !ok {
continue
@ -371,8 +375,7 @@ func findNearestDependencyPackages(skippedDep logicalDepsJSONPackage, pkgMap map
processed.Add(skippedDep.NameVersion)
for depName, depVersion := range skippedDep.Targets.Dependencies {
depNameVersion := createNameAndVersion(depName, depVersion)
for _, depNameVersion := range skippedDep.dependencyNameVersions() {
depPkg, ok := pkgMap[depNameVersion]
if !ok {
skippedDepPkg, ok := skipped[depNameVersion]

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
@ -66,7 +67,11 @@ func (t depsTarget) resourcePaths() map[string]string {
func (t depsTarget) runtimePaths() map[string]string {
result := make(map[string]string)
for path := range t.Runtime {
result[trimLibPrefix(path)] = path
trimmedPath := trimLibPrefix(path)
if _, exists := result[trimmedPath]; exists {
continue
}
result[trimmedPath] = path
}
return result
}
@ -82,14 +87,14 @@ type depsLibrary struct {
// Note: this is not a real construct of the deps.json, just a useful reorganization of the data for downstream processing.
type logicalDepsJSONPackage struct {
NameVersion string
Targets *depsTarget
Targets []depsTarget
Library *depsLibrary
// anyChildClaimsDLLs is a flag that indicates if any of the children of this package claim a DLL associated with them in the deps.json.
anyChildClaimsDLLs bool
// AnyChildClaimsDLLs is a flag that indicates if any of the children of this package claim a DLL associated with them in the deps.json.
AnyChildClaimsDLLs bool
// anyChildHasDLLs is a flag that indicates if any of the children of this package have a DLL associated with them (found on disk).
anyChildHasDLLs bool
// AnyChildHasDLLs is a flag that indicates if any of the children of this package have a DLL associated with them (found on disk).
AnyChildHasDLLs bool
// RuntimePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "runtime".
@ -103,7 +108,7 @@ type logicalDepsJSONPackage struct {
// to the target path as described in the deps.json target entry under "compile".
CompilePathsByRelativeDLLPath map[string]string
// NativePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
// NativePaths is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "native". These should not have
// any runtime references to trim from the front of the path.
NativePaths *strset.Set
@ -118,11 +123,15 @@ func (l *logicalDepsJSONPackage) dependencyNameVersions() []string {
if l.Targets == nil {
return nil
}
var results []string
for name, version := range l.Targets.Dependencies {
results = append(results, createNameAndVersion(name, version))
results := strset.New()
for _, t := range l.Targets {
for name, version := range t.Dependencies {
results.Add(createNameAndVersion(name, version))
}
}
return results
r := results.List()
sort.Strings(r)
return r
}
// ClaimsDLLs indicates if this package has any DLLs associated with it (directly or indirectly with a dependency).
@ -131,7 +140,7 @@ func (l *logicalDepsJSONPackage) ClaimsDLLs(includeChildren bool) bool {
if !includeChildren {
return selfClaim
}
return selfClaim || l.anyChildClaimsDLLs
return selfClaim || l.AnyChildClaimsDLLs
}
func (l *logicalDepsJSONPackage) FoundDLLs(includeChildren bool) bool {
@ -139,7 +148,7 @@ func (l *logicalDepsJSONPackage) FoundDLLs(includeChildren bool) bool {
if !includeChildren {
return selfClaim
}
return selfClaim || l.anyChildHasDLLs
return selfClaim || l.AnyChildHasDLLs
}
type logicalDepsJSON struct {
@ -206,6 +215,14 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
for libName, target := range targets {
_, exists := packageMap[libName]
if exists {
// merge this with existing targets (multiple targets can exist for the same library)
p := packageMap[libName]
p.Targets = append(p.Targets, target)
p.RuntimePathsByRelativeDLLPath = mergeMaps(p.RuntimePathsByRelativeDLLPath, target.runtimePaths())
p.ResourcePathsByRelativeDLLPath = mergeMaps(p.ResourcePathsByRelativeDLLPath, target.resourcePaths())
p.CompilePathsByRelativeDLLPath = mergeMaps(p.CompilePathsByRelativeDLLPath, target.compilePaths())
p.NativePaths = mergeSets(p.NativePaths, target.nativePaths())
continue
}
@ -218,7 +235,7 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
p := &logicalDepsJSONPackage{
NameVersion: libName,
Library: lib,
Targets: &target,
Targets: []depsTarget{target},
RuntimePathsByRelativeDLLPath: target.runtimePaths(),
ResourcePathsByRelativeDLLPath: target.resourcePaths(),
CompilePathsByRelativeDLLPath: target.compilePaths(),
@ -235,8 +252,8 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
if !bundlingDetected && knownBundlers.Has(name) {
bundlingDetected = true
}
p.anyChildClaimsDLLs = searchForDLLClaims(packageMap, p.dependencyNameVersions()...)
p.anyChildHasDLLs = searchForDLLEvidence(packageMap, p.dependencyNameVersions()...)
p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, p.dependencyNameVersions()...)
p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, p.dependencyNameVersions()...)
packages[p.NameVersion] = *p
}
@ -250,6 +267,22 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
}
}
func mergeMaps(m1, m2 map[string]string) map[string]string {
if m1 == nil {
m1 = make(map[string]string)
}
for k, v := range m2 {
if _, exists := m1[k]; !exists {
m1[k] = v
}
}
return m1
}
func mergeSets(s1, s2 *strset.Set) *strset.Set {
return strset.Union(s1, s2)
}
type visitorFunc func(p *logicalDepsJSONPackage) bool
// searchForDLLEvidence recursively searches for executables found for any of the given nameVersions and children recursively.
@ -277,12 +310,7 @@ func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor
return true
}
var children []string
for name, version := range p.Targets.Dependencies {
children = append(children, createNameAndVersion(name, version))
}
if traverseDependencies(packageMap, visitor, children...) {
if traverseDependencies(packageMap, visitor, p.dependencyNameVersions()...) {
return true
}
}

View File

@ -2,6 +2,12 @@ package dotnet
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
)
func TestTrimLibPrefix(t *testing.T) {
@ -71,3 +77,140 @@ func TestTrimLibPrefix(t *testing.T) {
})
}
}
func TestGetLogicalDepsJSON_MergeTargets(t *testing.T) {
deps := depsJSON{
Location: file.NewLocation("/path/to/deps.json"),
RuntimeTarget: runtimeTarget{
Name: ".NETCoreApp,Version=v6.0",
},
// note: for this test we have two targets with the same name, which will be merged when creating a logical deps
Targets: map[string]map[string]depsTarget{
".NETCoreApp,Version=v6.0": {
"Microsoft.CodeAnalysis.CSharp/4.0.0": {
Dependencies: map[string]string{
"Microsoft.CodeAnalysis.Common": "4.0.0",
},
Runtime: map[string]map[string]string{
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.0.21.51404",
},
},
Resources: map[string]map[string]string{
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "cs",
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "de",
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "es",
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "fr",
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "it",
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ja",
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ko",
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pl",
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pt-BR",
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ru",
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "tr",
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hans",
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hant",
},
},
},
},
"net6.0": {
"Microsoft.CodeAnalysis.CSharp/4.0.0": {
Dependencies: map[string]string{
"Microsoft.CodeAnalysis.Common": "4.0.0",
},
Compile: map[string]map[string]string{
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {},
},
},
},
},
Libraries: map[string]depsLibrary{
"Microsoft.CodeAnalysis.CSharp/4.0.0": {
Type: "package",
Path: "microsoft.codeanalysis.csharp/4.0.0",
Sha512: "sha512-example-hash",
HashPath: "microsoft.codeanalysis.csharp.4.0.0.nupkg.sha512",
},
},
}
result := getLogicalDepsJSON(deps, &libmanJSON{})
assert.Equal(t, "/path/to/deps.json", result.Location.RealPath)
assert.Equal(t, ".NETCoreApp,Version=v6.0", result.RuntimeTarget.Name)
libPackage, exists := result.PackagesByNameVersion["Microsoft.CodeAnalysis.CSharp/4.0.0"]
require.True(t, exists, "Expected to find the merged package")
assert.NotNil(t, libPackage.Library)
assert.Equal(t, "package", libPackage.Library.Type)
assert.Equal(t, "microsoft.codeanalysis.csharp/4.0.0", libPackage.Library.Path)
assert.Equal(t, "sha512-example-hash", libPackage.Library.Sha512)
assert.Equal(t, "microsoft.codeanalysis.csharp.4.0.0.nupkg.sha512", libPackage.Library.HashPath)
require.Equal(t, 2, len(libPackage.Targets), "Expected 2 targets to be merged")
expectedRuntimePaths := map[string]string{
"Microsoft.CodeAnalysis.CSharp.dll": "lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll",
}
if diff := cmp.Diff(expectedRuntimePaths, libPackage.RuntimePathsByRelativeDLLPath); diff != "" {
t.Errorf("RuntimePathsByRelativeDLLPath mismatch (-expected +actual):\n%s", diff)
}
expectedResourcePaths := map[string]string{
"cs/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll",
"de/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll",
"es/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll",
"fr/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll",
"it/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll",
"ja/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll",
"ko/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll",
"pl/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll",
"pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll",
"ru/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll",
"tr/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll",
"zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll",
"zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll",
}
if diff := cmp.Diff(expectedResourcePaths, libPackage.ResourcePathsByRelativeDLLPath); diff != "" {
t.Errorf("ResourcePathsByRelativeDLLPath mismatch (-expected +actual):\n%s", diff)
}
expectedCompilePaths := map[string]string{
"Microsoft.CodeAnalysis.CSharp.dll": "lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll",
}
if diff := cmp.Diff(expectedCompilePaths, libPackage.CompilePathsByRelativeDLLPath); diff != "" {
t.Errorf("CompilePathsByRelativeDLLPath mismatch (-expected +actual):\n%s", diff)
}
assert.Equal(t, 0, libPackage.NativePaths.Size(), "Expected no native paths")
assert.True(t, result.PackageNameVersions.Has("Microsoft.CodeAnalysis.CSharp/4.0.0"))
}