github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/java/archive_parser_test.go (about)

     1  package java
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strings"
    14  	"syscall"
    15  	"testing"
    16  
    17  	"github.com/google/go-cmp/cmp"
    18  	"github.com/google/go-cmp/cmp/cmpopts"
    19  	"github.com/gookit/color"
    20  	"github.com/scylladb/go-set/strset"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  
    24  	"github.com/anchore/syft/syft/artifact"
    25  	"github.com/anchore/syft/syft/file"
    26  	"github.com/anchore/syft/syft/license"
    27  	"github.com/anchore/syft/syft/pkg"
    28  	"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
    29  )
    30  
    31  func generateJavaBuildFixture(t *testing.T, fixturePath string) {
    32  	if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
    33  		// fixture already exists...
    34  		return
    35  	}
    36  
    37  	makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/")
    38  	t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
    39  
    40  	cwd, err := os.Getwd()
    41  	if err != nil {
    42  		t.Errorf("unable to get cwd: %+v", err)
    43  	}
    44  
    45  	cmd := exec.Command("make", makeTask)
    46  	cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/")
    47  
    48  	run(t, cmd)
    49  }
    50  
    51  func generateMockMavenHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
    52  	return func(w http.ResponseWriter, r *http.Request) {
    53  		w.WriteHeader(http.StatusOK)
    54  		// Set the Content-Type header to indicate that the response is XML
    55  		w.Header().Set("Content-Type", "application/xml")
    56  		// Copy the file's content to the response writer
    57  		file, err := os.Open(responseFixture)
    58  		if err != nil {
    59  			http.Error(w, err.Error(), http.StatusInternalServerError)
    60  			return
    61  		}
    62  		defer file.Close()
    63  		_, err = io.Copy(w, file)
    64  		if err != nil {
    65  			http.Error(w, err.Error(), http.StatusInternalServerError)
    66  			return
    67  		}
    68  	}
    69  }
    70  
    71  type handlerPath struct {
    72  	path    string
    73  	handler func(w http.ResponseWriter, r *http.Request)
    74  }
    75  
    76  func TestSearchMavenForLicenses(t *testing.T) {
    77  	mux, url, teardown := setup()
    78  	defer teardown()
    79  	tests := []struct {
    80  		name             string
    81  		fixture          string
    82  		detectNested     bool
    83  		config           ArchiveCatalogerConfig
    84  		requestPath      string
    85  		requestHandlers  []handlerPath
    86  		expectedLicenses []pkg.License
    87  	}{
    88  		{
    89  			name:         "searchMavenForLicenses returns the expected licenses when search is set to true",
    90  			fixture:      "opensaml-core-3.4.6",
    91  			detectNested: false,
    92  			config: ArchiveCatalogerConfig{
    93  				UseNetwork:              true,
    94  				MavenBaseURL:            url,
    95  				MaxParentRecursiveDepth: 2,
    96  			},
    97  			requestHandlers: []handlerPath{
    98  				{
    99  					path:    "/org/opensaml/opensaml-parent/3.4.6/opensaml-parent-3.4.6.pom",
   100  					handler: generateMockMavenHandler("test-fixtures/maven-xml-responses/opensaml-parent-3.4.6.pom"),
   101  				},
   102  				{
   103  					path:    "/net/shibboleth/parent/7.11.2/parent-7.11.2.pom",
   104  					handler: generateMockMavenHandler("test-fixtures/maven-xml-responses/parent-7.11.2.pom"),
   105  				},
   106  			},
   107  			expectedLicenses: []pkg.License{
   108  				{
   109  					Type:           license.Declared,
   110  					Value:          `The Apache Software License, Version 2.0`,
   111  					SPDXExpression: ``,
   112  				},
   113  			},
   114  		},
   115  	}
   116  
   117  	for _, tc := range tests {
   118  		t.Run(tc.name, func(t *testing.T) {
   119  			// configure maven central requests
   120  			for _, hdlr := range tc.requestHandlers {
   121  				mux.HandleFunc(hdlr.path, hdlr.handler)
   122  			}
   123  
   124  			// setup metadata fixture; note:
   125  			// this fixture has a pomProjectObject and has a parent object
   126  			// it has no licenses on either which is the condition for testing
   127  			// the searchMavenForLicenses functionality
   128  			jarName := generateJavaMetadataJarFixture(t, tc.fixture)
   129  			fixture, err := os.Open(jarName)
   130  			require.NoError(t, err)
   131  
   132  			// setup parser
   133  			ap, cleanupFn, err := newJavaArchiveParser(
   134  				file.LocationReadCloser{
   135  					Location:   file.NewLocation(fixture.Name()),
   136  					ReadCloser: fixture,
   137  				}, tc.detectNested, tc.config)
   138  			defer cleanupFn()
   139  
   140  			// assert licenses are discovered from upstream
   141  			_, _, licenses := ap.guessMainPackageNameAndVersionFromPomInfo(context.Background())
   142  			assert.Equal(t, tc.expectedLicenses, licenses)
   143  		})
   144  	}
   145  }
   146  
   147  func TestFormatMavenURL(t *testing.T) {
   148  	tests := []struct {
   149  		name       string
   150  		groupID    string
   151  		artifactID string
   152  		version    string
   153  		expected   string
   154  	}{
   155  		{
   156  			name:       "formatMavenURL correctly assembles the pom URL",
   157  			groupID:    "org.springframework.boot",
   158  			artifactID: "spring-boot-starter-test",
   159  			version:    "3.1.5",
   160  			expected:   "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-test/3.1.5/spring-boot-starter-test-3.1.5.pom",
   161  		},
   162  	}
   163  
   164  	for _, tc := range tests {
   165  		t.Run(tc.name, func(t *testing.T) {
   166  			requestURL, err := formatMavenPomURL(tc.groupID, tc.artifactID, tc.version, mavenBaseURL)
   167  			assert.NoError(t, err, "expected no err; got %w", err)
   168  			assert.Equal(t, tc.expected, requestURL)
   169  		})
   170  	}
   171  }
   172  
   173  func TestParseJar(t *testing.T) {
   174  	tests := []struct {
   175  		name         string
   176  		fixture      string
   177  		expected     map[string]pkg.Package
   178  		ignoreExtras []string
   179  	}{
   180  		{
   181  			name:    "example-jenkins-plugin",
   182  			fixture: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
   183  			ignoreExtras: []string{
   184  				"Plugin-Version", // has dynamic date
   185  				"Built-By",       // podman returns the real UID
   186  				"Build-Jdk",      // can't guarantee the JDK used at build time
   187  			},
   188  			expected: map[string]pkg.Package{
   189  				"example-jenkins-plugin": {
   190  					Name:    "example-jenkins-plugin",
   191  					Version: "1.0-SNAPSHOT",
   192  					PURL:    "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT",
   193  					Licenses: pkg.NewLicenseSet(
   194  						pkg.NewLicenseFromLocations("MIT License", file.NewLocation("test-fixtures/java-builds/packages/example-jenkins-plugin.hpi")),
   195  					),
   196  					Language: pkg.Java,
   197  					Type:     pkg.JenkinsPluginPkg,
   198  					Metadata: pkg.JavaArchive{
   199  						VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
   200  						Manifest: &pkg.JavaManifest{
   201  							Main: pkg.KeyValues{
   202  								{Key: "Manifest-Version", Value: "1.0"},
   203  								{Key: "Created-By", Value: "Maven Archiver 3.6.0"},
   204  								{Key: "Build-Jdk-Spec", Value: "18"},
   205  								{Key: "Specification-Title", Value: "Example Jenkins Plugin"},
   206  								{Key: "Specification-Version", Value: "1.0"},
   207  								{Key: "Implementation-Title", Value: "Example Jenkins Plugin"},
   208  								{Key: "Implementation-Version", Value: "1.0-SNAPSHOT"},
   209  								{Key: "Group-Id", Value: "io.jenkins.plugins"},
   210  								{Key: "Short-Name", Value: "example-jenkins-plugin"},
   211  								{Key: "Long-Name", Value: "Example Jenkins Plugin"},
   212  								{Key: "Hudson-Version", Value: "2.204"},
   213  								{Key: "Jenkins-Version", Value: "2.204"},
   214  								{Key: "Plugin-Dependencies", Value: "structs:1.20"},
   215  								{Key: "Plugin-Developers", Value: ""},
   216  								{Key: "Plugin-License-Name", Value: "MIT License"},
   217  								{Key: "Plugin-License-Url", Value: "https://opensource.org/licenses/MIT"},
   218  								{Key: "Plugin-ScmUrl", Value: "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin"},
   219  								// extra fields...
   220  								//{Key: "Minimum-Java-Version", Value: "1.8"},
   221  								//{Key: "Archiver-Version", Value: "Plexus Archiver"},
   222  								//{Key: "Built-By", Value: "?"},
   223  								//{Key: "Build-Jdk", Value: "14.0.1"},
   224  								//{Key: "Extension-Name", Value: "example-jenkins-plugin"},
   225  								//{Key: "Plugin-Version", Value: "1.0-SNAPSHOT (private-07/09/2020 13:30-?)"},
   226  							},
   227  						},
   228  						PomProperties: &pkg.JavaPomProperties{
   229  							Path:       "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.properties",
   230  							GroupID:    "io.jenkins.plugins",
   231  							ArtifactID: "example-jenkins-plugin",
   232  							Version:    "1.0-SNAPSHOT",
   233  						},
   234  					},
   235  				},
   236  			},
   237  		},
   238  		{
   239  			name:    "example-java-app-gradle",
   240  			fixture: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
   241  			expected: map[string]pkg.Package{
   242  				"example-java-app-gradle": {
   243  					Name:     "example-java-app-gradle",
   244  					Version:  "0.1.0",
   245  					PURL:     "pkg:maven/example-java-app-gradle/example-java-app-gradle@0.1.0",
   246  					Language: pkg.Java,
   247  					Type:     pkg.JavaPkg,
   248  					Licenses: pkg.NewLicenseSet(
   249  						pkg.License{
   250  							Value:          "Apache-2.0",
   251  							SPDXExpression: "Apache-2.0",
   252  							Type:           license.Concluded,
   253  							Locations:      file.NewLocationSet(file.NewLocation("test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar")),
   254  						},
   255  					),
   256  					Metadata: pkg.JavaArchive{
   257  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
   258  						Manifest: &pkg.JavaManifest{
   259  							Main: []pkg.KeyValue{
   260  								{
   261  									Key:   "Manifest-Version",
   262  									Value: "1.0",
   263  								},
   264  								{
   265  									Key:   "Main-Class",
   266  									Value: "hello.HelloWorld",
   267  								},
   268  							},
   269  						},
   270  					},
   271  				},
   272  				"joda-time": {
   273  					Name:     "joda-time",
   274  					Version:  "2.2",
   275  					PURL:     "pkg:maven/joda-time/joda-time@2.2",
   276  					Language: pkg.Java,
   277  					Type:     pkg.JavaPkg,
   278  					Licenses: pkg.NewLicenseSet(
   279  						pkg.NewLicenseFromFields(
   280  							"Apache 2",
   281  							"http://www.apache.org/licenses/LICENSE-2.0.txt",
   282  							func() *file.Location {
   283  								l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar")
   284  								return &l
   285  							}(),
   286  						),
   287  					),
   288  					Metadata: pkg.JavaArchive{
   289  						// ensure that nested packages with different names than that of the parent are appended as
   290  						// a suffix on the virtual path with a colon separator between group name and artifact name
   291  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar:joda-time:joda-time",
   292  						PomProperties: &pkg.JavaPomProperties{
   293  							Path:       "META-INF/maven/joda-time/joda-time/pom.properties",
   294  							GroupID:    "joda-time",
   295  							ArtifactID: "joda-time",
   296  							Version:    "2.2",
   297  						},
   298  						PomProject: &pkg.JavaPomProject{
   299  							Path:        "META-INF/maven/joda-time/joda-time/pom.xml",
   300  							GroupID:     "joda-time",
   301  							ArtifactID:  "joda-time",
   302  							Version:     "2.2",
   303  							Name:        "Joda time",
   304  							Description: "Date and time library to replace JDK date handling",
   305  							URL:         "http://joda-time.sourceforge.net",
   306  						},
   307  					},
   308  				},
   309  			},
   310  		},
   311  		{
   312  			name:    "example-java-app-maven",
   313  			fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
   314  			ignoreExtras: []string{
   315  				"Build-Jdk", // can't guarantee the JDK used at build time
   316  				"Built-By",  // podman returns the real UID
   317  			},
   318  			expected: map[string]pkg.Package{
   319  				"example-java-app-maven": {
   320  					Name:     "example-java-app-maven",
   321  					Version:  "0.1.0",
   322  					PURL:     "pkg:maven/org.anchore/example-java-app-maven@0.1.0",
   323  					Language: pkg.Java,
   324  					Type:     pkg.JavaPkg,
   325  					Licenses: pkg.NewLicenseSet(
   326  						pkg.License{
   327  							Value:          "Apache-2.0",
   328  							SPDXExpression: "Apache-2.0",
   329  							Type:           license.Concluded,
   330  							Locations:      file.NewLocationSet(file.NewLocation("test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar")),
   331  						},
   332  					),
   333  					Metadata: pkg.JavaArchive{
   334  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
   335  						Manifest: &pkg.JavaManifest{
   336  							Main: []pkg.KeyValue{
   337  								{
   338  									Key:   "Manifest-Version",
   339  									Value: "1.0",
   340  								},
   341  								// extra fields...
   342  								{
   343  									Key:   "Archiver-Version",
   344  									Value: "Plexus Archiver",
   345  								},
   346  								{
   347  									Key:   "Created-By",
   348  									Value: "Apache Maven 3.8.6",
   349  								},
   350  								//{
   351  								//  Key:   "Built-By",
   352  								//  Value: "?",
   353  								//},
   354  								//{
   355  								//	Key:   "Build-Jdk",
   356  								//	Value: "14.0.1",
   357  								//},
   358  								{
   359  									Key:   "Main-Class",
   360  									Value: "hello.HelloWorld",
   361  								},
   362  							},
   363  						},
   364  						PomProperties: &pkg.JavaPomProperties{
   365  							Path:       "META-INF/maven/org.anchore/example-java-app-maven/pom.properties",
   366  							GroupID:    "org.anchore",
   367  							ArtifactID: "example-java-app-maven",
   368  							Version:    "0.1.0",
   369  						},
   370  					},
   371  				},
   372  				"joda-time": {
   373  					Name:    "joda-time",
   374  					Version: "2.9.2",
   375  					PURL:    "pkg:maven/joda-time/joda-time@2.9.2",
   376  					Licenses: pkg.NewLicenseSet(
   377  						pkg.NewLicenseFromFields(
   378  							"Apache 2",
   379  							"http://www.apache.org/licenses/LICENSE-2.0.txt",
   380  							func() *file.Location {
   381  								l := file.NewLocation("test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar")
   382  								return &l
   383  							}(),
   384  						),
   385  					),
   386  					Language: pkg.Java,
   387  					Type:     pkg.JavaPkg,
   388  					Metadata: pkg.JavaArchive{
   389  						// ensure that nested packages with different names than that of the parent are appended as
   390  						// a suffix on the virtual path
   391  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time:joda-time",
   392  						PomProperties: &pkg.JavaPomProperties{
   393  							Path:       "META-INF/maven/joda-time/joda-time/pom.properties",
   394  							GroupID:    "joda-time",
   395  							ArtifactID: "joda-time",
   396  							Version:    "2.9.2",
   397  						},
   398  						PomProject: &pkg.JavaPomProject{
   399  							Path:        "META-INF/maven/joda-time/joda-time/pom.xml",
   400  							GroupID:     "joda-time",
   401  							ArtifactID:  "joda-time",
   402  							Version:     "2.9.2",
   403  							Name:        "Joda-Time",
   404  							Description: "Date and time library to replace JDK date handling",
   405  							URL:         "http://www.joda.org/joda-time/",
   406  						},
   407  					},
   408  				},
   409  			},
   410  		},
   411  	}
   412  
   413  	for _, test := range tests {
   414  		t.Run(test.name, func(t *testing.T) {
   415  
   416  			generateJavaBuildFixture(t, test.fixture)
   417  
   418  			fixture, err := os.Open(test.fixture)
   419  			require.NoError(t, err)
   420  
   421  			for k := range test.expected {
   422  				p := test.expected[k]
   423  				p.Locations.Add(file.NewLocation(test.fixture))
   424  				test.expected[k] = p
   425  			}
   426  
   427  			parser, cleanupFn, err := newJavaArchiveParser(file.LocationReadCloser{
   428  				Location:   file.NewLocation(fixture.Name()),
   429  				ReadCloser: fixture,
   430  			}, false, ArchiveCatalogerConfig{UseNetwork: false})
   431  			defer cleanupFn()
   432  			require.NoError(t, err)
   433  
   434  			actual, _, err := parser.parse(context.Background())
   435  			require.NoError(t, err)
   436  
   437  			if len(actual) != len(test.expected) {
   438  				for _, a := range actual {
   439  					t.Log("   ", a)
   440  				}
   441  				t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
   442  			}
   443  
   444  			var parent *pkg.Package
   445  			for _, a := range actual {
   446  				a := a
   447  				if strings.Contains(a.Name, "example-") {
   448  					parent = &a
   449  				}
   450  			}
   451  
   452  			if parent == nil {
   453  				t.Fatal("could not find the parent pkg")
   454  			}
   455  
   456  			for _, a := range actual {
   457  				if a.ID() == "" {
   458  					t.Fatalf("empty package ID: %+v", a)
   459  				}
   460  
   461  				e, ok := test.expected[a.Name]
   462  				if !ok {
   463  					t.Errorf("entry not found: %s", a.Name)
   464  					continue
   465  				}
   466  
   467  				if a.Name != parent.Name && a.Metadata.(pkg.JavaArchive).Parent != nil && a.Metadata.(pkg.JavaArchive).Parent.Name != parent.Name {
   468  					t.Errorf("mismatched parent: %+v", a.Metadata.(pkg.JavaArchive).Parent)
   469  				}
   470  
   471  				// we need to compare the other fields without parent attached
   472  				metadata := a.Metadata.(pkg.JavaArchive)
   473  				metadata.Parent = nil
   474  
   475  				// redact Digest which is computed differently between CI and local
   476  				if len(metadata.ArchiveDigests) > 0 {
   477  					metadata.ArchiveDigests = nil
   478  				}
   479  
   480  				// ignore select fields (only works for the main section)
   481  				for _, field := range test.ignoreExtras {
   482  					if metadata.Manifest != nil && metadata.Manifest.Main != nil {
   483  						newMain := make(pkg.KeyValues, 0)
   484  						for i, kv := range metadata.Manifest.Main {
   485  							if kv.Key == field {
   486  								continue
   487  							}
   488  							newMain = append(newMain, metadata.Manifest.Main[i])
   489  						}
   490  						metadata.Manifest.Main = newMain
   491  					}
   492  				}
   493  
   494  				// write censored data back
   495  				a.Metadata = metadata
   496  
   497  				pkgtest.AssertPackagesEqual(t, e, a)
   498  			}
   499  		})
   500  	}
   501  }
   502  
   503  func TestParseNestedJar(t *testing.T) {
   504  	tests := []struct {
   505  		fixture      string
   506  		expected     []pkg.Package
   507  		ignoreExtras []string
   508  	}{
   509  		{
   510  			fixture: "test-fixtures/java-builds/packages/spring-boot-0.0.1-SNAPSHOT.jar",
   511  			expected: []pkg.Package{
   512  				{
   513  					Name:    "spring-boot",
   514  					Version: "0.0.1-SNAPSHOT",
   515  				},
   516  				{
   517  					Name:    "spring-boot-starter",
   518  					Version: "2.2.2.RELEASE",
   519  				},
   520  				{
   521  					Name:    "jul-to-slf4j",
   522  					Version: "1.7.29",
   523  				},
   524  				{
   525  					Name:    "tomcat-embed-websocket",
   526  					Version: "9.0.29",
   527  				},
   528  				{
   529  					Name:    "spring-boot-starter-validation",
   530  					Version: "2.2.2.RELEASE",
   531  				},
   532  				{
   533  					Name:    "hibernate-validator",
   534  					Version: "6.0.18.Final",
   535  				},
   536  				{
   537  					Name:    "jboss-logging",
   538  					Version: "3.4.1.Final",
   539  				},
   540  				{
   541  					Name:    "spring-expression",
   542  					Version: "5.2.2.RELEASE",
   543  				},
   544  				{
   545  					Name:    "jakarta.validation-api",
   546  					Version: "2.0.1",
   547  				},
   548  				{
   549  					Name:    "spring-web",
   550  					Version: "5.2.2.RELEASE",
   551  				},
   552  				{
   553  					Name:    "spring-boot-starter-actuator",
   554  					Version: "2.2.2.RELEASE",
   555  				},
   556  				{
   557  					Name:    "log4j-api",
   558  					Version: "2.12.1",
   559  				},
   560  				{
   561  					Name:    "snakeyaml",
   562  					Version: "1.25",
   563  				},
   564  				{
   565  					Name:    "jackson-core",
   566  					Version: "2.10.1",
   567  				},
   568  				{
   569  					Name:    "jackson-datatype-jsr310",
   570  					Version: "2.10.1",
   571  				},
   572  				{
   573  					Name:    "spring-aop",
   574  					Version: "5.2.2.RELEASE",
   575  				},
   576  				{
   577  					Name:    "spring-boot-actuator-autoconfigure",
   578  					Version: "2.2.2.RELEASE",
   579  				},
   580  				{
   581  					Name:    "spring-jcl",
   582  					Version: "5.2.2.RELEASE",
   583  				},
   584  				{
   585  					Name:    "spring-boot",
   586  					Version: "2.2.2.RELEASE",
   587  				},
   588  				{
   589  					Name:    "spring-boot-starter-logging",
   590  					Version: "2.2.2.RELEASE",
   591  				},
   592  				{
   593  					Name:    "jakarta.annotation-api",
   594  					Version: "1.3.5",
   595  				},
   596  				{
   597  					Name:    "spring-webmvc",
   598  					Version: "5.2.2.RELEASE",
   599  				},
   600  				{
   601  					Name:    "HdrHistogram",
   602  					Version: "2.1.11",
   603  				},
   604  				{
   605  					Name:    "spring-boot-starter-web",
   606  					Version: "2.2.2.RELEASE",
   607  				},
   608  				{
   609  					Name:    "logback-classic",
   610  					Version: "1.2.3",
   611  				},
   612  				{
   613  					Name:    "log4j-to-slf4j",
   614  					Version: "2.12.1",
   615  				},
   616  				{
   617  					Name:    "spring-boot-starter-json",
   618  					Version: "2.2.2.RELEASE",
   619  				},
   620  				{
   621  					Name:    "jackson-databind",
   622  					Version: "2.10.1",
   623  				},
   624  				{
   625  					Name:    "jackson-module-parameter-names",
   626  					Version: "2.10.1",
   627  				},
   628  				{
   629  					Name:    "LatencyUtils",
   630  					Version: "2.0.3",
   631  				},
   632  				{
   633  					Name:    "spring-boot-autoconfigure",
   634  					Version: "2.2.2.RELEASE",
   635  				},
   636  				{
   637  					Name:    "jackson-datatype-jdk8",
   638  					Version: "2.10.1",
   639  				},
   640  				{
   641  					Name:    "tomcat-embed-core",
   642  					Version: "9.0.29",
   643  				},
   644  				{
   645  					Name:    "tomcat-embed-el",
   646  					Version: "9.0.29",
   647  				},
   648  				{
   649  					Name:    "spring-beans",
   650  					Version: "5.2.2.RELEASE",
   651  				},
   652  				{
   653  					Name:    "spring-boot-actuator",
   654  					Version: "2.2.2.RELEASE",
   655  				},
   656  				{
   657  					Name:    "slf4j-api",
   658  					Version: "1.7.29",
   659  				},
   660  				{
   661  					Name:    "spring-core",
   662  					Version: "5.2.2.RELEASE",
   663  				},
   664  				{
   665  					Name:    "logback-core",
   666  					Version: "1.2.3",
   667  				},
   668  				{
   669  					Name:    "micrometer-core",
   670  					Version: "1.3.1",
   671  				},
   672  				{
   673  					Name:    "pcollections",
   674  					Version: "3.1.0",
   675  				},
   676  				{
   677  					Name:    "jackson-annotations",
   678  					Version: "2.10.1",
   679  				},
   680  				{
   681  					Name:    "spring-boot-starter-tomcat",
   682  					Version: "2.2.2.RELEASE",
   683  				},
   684  				{
   685  					Name:    "classmate",
   686  					Version: "1.5.1",
   687  				},
   688  				{
   689  					Name:    "spring-context",
   690  					Version: "5.2.2.RELEASE",
   691  				},
   692  			},
   693  		},
   694  	}
   695  
   696  	for _, test := range tests {
   697  		t.Run(test.fixture, func(t *testing.T) {
   698  
   699  			generateJavaBuildFixture(t, test.fixture)
   700  
   701  			fixture, err := os.Open(test.fixture)
   702  			require.NoError(t, err)
   703  			gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
   704  
   705  			actual, _, err := gap.parseJavaArchive(context.Background(), nil, nil, file.LocationReadCloser{
   706  				Location:   file.NewLocation(fixture.Name()),
   707  				ReadCloser: fixture,
   708  			})
   709  			require.NoError(t, err)
   710  
   711  			expectedNameVersionPairSet := strset.New()
   712  
   713  			makeKey := func(p *pkg.Package) string {
   714  				if p == nil {
   715  					t.Fatal("cannot make key for nil pkg")
   716  				}
   717  				return fmt.Sprintf("%s|%s", p.Name, p.Version)
   718  			}
   719  
   720  			for _, e := range test.expected {
   721  				expectedNameVersionPairSet.Add(makeKey(&e))
   722  			}
   723  
   724  			actualNameVersionPairSet := strset.New()
   725  			for _, a := range actual {
   726  				a := a
   727  				key := makeKey(&a)
   728  				actualNameVersionPairSet.Add(key)
   729  				if !expectedNameVersionPairSet.Has(key) {
   730  					t.Errorf("extra package: %s", a)
   731  				}
   732  			}
   733  
   734  			for _, key := range expectedNameVersionPairSet.List() {
   735  				if !actualNameVersionPairSet.Has(key) {
   736  					t.Errorf("missing package: %s", key)
   737  				}
   738  			}
   739  
   740  			if len(actual) != expectedNameVersionPairSet.Size() {
   741  				t.Fatalf("unexpected package count: %d!=%d", len(actual), expectedNameVersionPairSet.Size())
   742  			}
   743  
   744  			for _, a := range actual {
   745  				a := a
   746  				actualKey := makeKey(&a)
   747  
   748  				metadata := a.Metadata.(pkg.JavaArchive)
   749  				if actualKey == "spring-boot|0.0.1-SNAPSHOT" {
   750  					if metadata.Parent != nil {
   751  						t.Errorf("expected no parent for root pkg, got %q", makeKey(metadata.Parent))
   752  					}
   753  				} else {
   754  					if metadata.Parent == nil {
   755  						t.Errorf("unassigned error for pkg=%q", actualKey)
   756  					} else if makeKey(metadata.Parent) != "spring-boot|0.0.1-SNAPSHOT" {
   757  						// NB: this is a hard-coded condition to simplify the test harness to account for https://github.com/micrometer-metrics/micrometer/issues/1785
   758  						if a.Name == "pcollections" {
   759  							if metadata.Parent.Name != "micrometer-core" {
   760  								t.Errorf("nested 'pcollections' pkg has wrong parent: %q", metadata.Parent.Name)
   761  							}
   762  						} else {
   763  							t.Errorf("bad parent for pkg=%q parent=%q", actualKey, makeKey(metadata.Parent))
   764  						}
   765  					}
   766  				}
   767  			}
   768  		})
   769  	}
   770  }
   771  
   772  func Test_newPackageFromMavenData(t *testing.T) {
   773  	virtualPath := "given/virtual/path"
   774  	tests := []struct {
   775  		name            string
   776  		props           pkg.JavaPomProperties
   777  		project         *parsedPomProject
   778  		parent          *pkg.Package
   779  		expectedParent  pkg.Package
   780  		expectedPackage *pkg.Package
   781  	}{
   782  		{
   783  			name: "go case: get a single package from pom properties",
   784  			props: pkg.JavaPomProperties{
   785  				Name:       "some-name",
   786  				GroupID:    "some-group-id",
   787  				ArtifactID: "some-artifact-id",
   788  				Version:    "1.0",
   789  			},
   790  			parent: &pkg.Package{
   791  				Name:    "some-parent-name",
   792  				Version: "2.0",
   793  				Metadata: pkg.JavaArchive{
   794  					VirtualPath:   "some-parent-virtual-path",
   795  					Manifest:      nil,
   796  					PomProperties: nil,
   797  					Parent:        nil,
   798  				},
   799  			},
   800  			// note: the SAME as the original parent values
   801  			expectedParent: pkg.Package{
   802  				Name:    "some-parent-name",
   803  				Version: "2.0",
   804  				Metadata: pkg.JavaArchive{
   805  					VirtualPath:   "some-parent-virtual-path",
   806  					Manifest:      nil,
   807  					PomProperties: nil,
   808  					Parent:        nil,
   809  				},
   810  			},
   811  			expectedPackage: &pkg.Package{
   812  				Name:     "some-artifact-id",
   813  				Version:  "1.0",
   814  				Language: pkg.Java,
   815  				Type:     pkg.JavaPkg,
   816  				Metadata: pkg.JavaArchive{
   817  					VirtualPath: virtualPath + ":" + "some-group-id" + ":" + "some-artifact-id",
   818  					PomProperties: &pkg.JavaPomProperties{
   819  						Name:       "some-name",
   820  						GroupID:    "some-group-id",
   821  						ArtifactID: "some-artifact-id",
   822  						Version:    "1.0",
   823  					},
   824  					Parent: &pkg.Package{
   825  						Name:    "some-parent-name",
   826  						Version: "2.0",
   827  						Metadata: pkg.JavaArchive{
   828  							VirtualPath:   "some-parent-virtual-path",
   829  							Manifest:      nil,
   830  							PomProperties: nil,
   831  							Parent:        nil,
   832  						},
   833  					},
   834  				},
   835  			},
   836  		},
   837  		{
   838  			name: "get a single package from pom properties + project",
   839  			props: pkg.JavaPomProperties{
   840  				Name:       "some-name",
   841  				GroupID:    "some-group-id",
   842  				ArtifactID: "some-artifact-id",
   843  				Version:    "1.0",
   844  			},
   845  			project: &parsedPomProject{
   846  				JavaPomProject: &pkg.JavaPomProject{
   847  					Parent: &pkg.JavaPomParent{
   848  						GroupID:    "some-parent-group-id",
   849  						ArtifactID: "some-parent-artifact-id",
   850  						Version:    "1.0-parent",
   851  					},
   852  					Name:        "some-name",
   853  					GroupID:     "some-group-id",
   854  					ArtifactID:  "some-artifact-id",
   855  					Version:     "1.0",
   856  					Description: "desc",
   857  					URL:         "aweso.me",
   858  				},
   859  				Licenses: []pkg.License{
   860  					{
   861  						Value:          "MIT",
   862  						SPDXExpression: "MIT",
   863  						Type:           license.Declared,
   864  						URLs:           []string{"https://opensource.org/licenses/MIT"},
   865  						Locations:      file.NewLocationSet(file.NewLocation("some-license-path")),
   866  					},
   867  				},
   868  			},
   869  			parent: &pkg.Package{
   870  				Name:    "some-parent-name",
   871  				Version: "2.0",
   872  				Metadata: pkg.JavaArchive{
   873  					VirtualPath:   "some-parent-virtual-path",
   874  					Manifest:      nil,
   875  					PomProperties: nil,
   876  					Parent:        nil,
   877  				},
   878  			},
   879  			// note: the SAME as the original parent values
   880  			expectedParent: pkg.Package{
   881  				Name:    "some-parent-name",
   882  				Version: "2.0",
   883  				Metadata: pkg.JavaArchive{
   884  					VirtualPath:   "some-parent-virtual-path",
   885  					Manifest:      nil,
   886  					PomProperties: nil,
   887  					Parent:        nil,
   888  				},
   889  			},
   890  			expectedPackage: &pkg.Package{
   891  				Name:     "some-artifact-id",
   892  				Version:  "1.0",
   893  				Language: pkg.Java,
   894  				Type:     pkg.JavaPkg,
   895  				Licenses: pkg.NewLicenseSet(
   896  					pkg.License{
   897  						Value:          "MIT",
   898  						SPDXExpression: "MIT",
   899  						Type:           license.Declared,
   900  						URLs:           []string{"https://opensource.org/licenses/MIT"},
   901  						Locations:      file.NewLocationSet(file.NewLocation("some-license-path")),
   902  					},
   903  				),
   904  				Metadata: pkg.JavaArchive{
   905  					VirtualPath: virtualPath + ":" + "some-group-id" + ":" + "some-artifact-id",
   906  					PomProperties: &pkg.JavaPomProperties{
   907  						Name:       "some-name",
   908  						GroupID:    "some-group-id",
   909  						ArtifactID: "some-artifact-id",
   910  						Version:    "1.0",
   911  					},
   912  					PomProject: &pkg.JavaPomProject{
   913  						Parent: &pkg.JavaPomParent{
   914  							GroupID:    "some-parent-group-id",
   915  							ArtifactID: "some-parent-artifact-id",
   916  							Version:    "1.0-parent",
   917  						},
   918  						Name:        "some-name",
   919  						GroupID:     "some-group-id",
   920  						ArtifactID:  "some-artifact-id",
   921  						Version:     "1.0",
   922  						Description: "desc",
   923  						URL:         "aweso.me",
   924  					},
   925  					Parent: &pkg.Package{
   926  						Name:    "some-parent-name",
   927  						Version: "2.0",
   928  						Metadata: pkg.JavaArchive{
   929  							VirtualPath:   "some-parent-virtual-path",
   930  							Manifest:      nil,
   931  							PomProperties: nil,
   932  							Parent:        nil,
   933  						},
   934  					},
   935  				},
   936  			},
   937  		},
   938  		{
   939  			name: "single package from pom properties that's a Jenkins plugin",
   940  			props: pkg.JavaPomProperties{
   941  				Name:       "some-name",
   942  				GroupID:    "com.cloudbees.jenkins.plugins",
   943  				ArtifactID: "some-artifact-id",
   944  				Version:    "1.0",
   945  			},
   946  			parent: &pkg.Package{
   947  				Name:    "some-parent-name",
   948  				Version: "2.0",
   949  				Metadata: pkg.JavaArchive{
   950  					VirtualPath:   "some-parent-virtual-path",
   951  					Manifest:      nil,
   952  					PomProperties: nil,
   953  					Parent:        nil,
   954  				},
   955  			},
   956  			// note: the SAME as the original parent values
   957  			expectedParent: pkg.Package{
   958  				Name:    "some-parent-name",
   959  				Version: "2.0",
   960  				Metadata: pkg.JavaArchive{
   961  					VirtualPath:   "some-parent-virtual-path",
   962  					Manifest:      nil,
   963  					PomProperties: nil,
   964  					Parent:        nil,
   965  				},
   966  			},
   967  			expectedPackage: &pkg.Package{
   968  				Name:     "some-artifact-id",
   969  				Version:  "1.0",
   970  				Language: pkg.Java,
   971  				Type:     pkg.JenkinsPluginPkg,
   972  				Metadata: pkg.JavaArchive{
   973  					VirtualPath: virtualPath + ":" + "com.cloudbees.jenkins.plugins" + ":" + "some-artifact-id",
   974  					PomProperties: &pkg.JavaPomProperties{
   975  						Name:       "some-name",
   976  						GroupID:    "com.cloudbees.jenkins.plugins",
   977  						ArtifactID: "some-artifact-id",
   978  						Version:    "1.0",
   979  					},
   980  					Parent: &pkg.Package{
   981  						Name:    "some-parent-name",
   982  						Version: "2.0",
   983  						Metadata: pkg.JavaArchive{
   984  							VirtualPath:   "some-parent-virtual-path",
   985  							Manifest:      nil,
   986  							PomProperties: nil,
   987  							Parent:        nil,
   988  						},
   989  					},
   990  				},
   991  			},
   992  		},
   993  		{
   994  			name: "child matches parent by key",
   995  			props: pkg.JavaPomProperties{
   996  				Name:       "some-name",
   997  				GroupID:    "some-group-id",
   998  				ArtifactID: "some-parent-name", // note: matches parent package
   999  				Version:    "2.0",              // note: matches parent package
  1000  			},
  1001  			parent: &pkg.Package{
  1002  				Name:    "some-parent-name",
  1003  				Version: "2.0",
  1004  				Type:    pkg.JavaPkg,
  1005  				Metadata: pkg.JavaArchive{
  1006  					VirtualPath:   "some-parent-virtual-path",
  1007  					Manifest:      nil,
  1008  					PomProperties: nil,
  1009  					Parent:        nil,
  1010  				},
  1011  			},
  1012  			// note: the SAME as the original parent values
  1013  			expectedParent: pkg.Package{
  1014  				Name:    "some-parent-name",
  1015  				Version: "2.0",
  1016  				Type:    pkg.JavaPkg,
  1017  				Metadata: pkg.JavaArchive{
  1018  					VirtualPath: "some-parent-virtual-path",
  1019  					Manifest:    nil,
  1020  					// note: we attach the discovered pom properties data
  1021  					PomProperties: &pkg.JavaPomProperties{
  1022  						Name:       "some-name",
  1023  						GroupID:    "some-group-id",
  1024  						ArtifactID: "some-parent-name", // note: matches parent package
  1025  						Version:    "2.0",              // note: matches parent package
  1026  					},
  1027  					Parent: nil,
  1028  				},
  1029  			},
  1030  			expectedPackage: nil,
  1031  		},
  1032  		{
  1033  			name: "child matches parent by key and is Jenkins plugin",
  1034  			props: pkg.JavaPomProperties{
  1035  				Name:       "some-name",
  1036  				GroupID:    "com.cloudbees.jenkins.plugins",
  1037  				ArtifactID: "some-parent-name", // note: matches parent package
  1038  				Version:    "2.0",              // note: matches parent package
  1039  			},
  1040  			parent: &pkg.Package{
  1041  				Name:    "some-parent-name",
  1042  				Version: "2.0",
  1043  				Type:    pkg.JavaPkg,
  1044  				Metadata: pkg.JavaArchive{
  1045  					VirtualPath:   "some-parent-virtual-path",
  1046  					Manifest:      nil,
  1047  					PomProperties: nil,
  1048  					Parent:        nil,
  1049  				},
  1050  			},
  1051  			expectedParent: pkg.Package{
  1052  				Name:    "some-parent-name",
  1053  				Version: "2.0",
  1054  				Type:    pkg.JenkinsPluginPkg,
  1055  				Metadata: pkg.JavaArchive{
  1056  					VirtualPath: "some-parent-virtual-path",
  1057  					Manifest:    nil,
  1058  					// note: we attach the discovered pom properties data
  1059  					PomProperties: &pkg.JavaPomProperties{
  1060  						Name:       "some-name",
  1061  						GroupID:    "com.cloudbees.jenkins.plugins",
  1062  						ArtifactID: "some-parent-name", // note: matches parent package
  1063  						Version:    "2.0",              // note: matches parent package
  1064  					},
  1065  					Parent: nil,
  1066  				},
  1067  			},
  1068  			expectedPackage: nil,
  1069  		},
  1070  		{
  1071  			name: "child matches parent by artifact id",
  1072  			props: pkg.JavaPomProperties{
  1073  				Name:       "some-name",
  1074  				GroupID:    "some-group-id",
  1075  				ArtifactID: "some-parent-name",       // note: matches parent package
  1076  				Version:    "NOT_THE_PARENT_VERSION", // note: DOES NOT match parent package
  1077  			},
  1078  			parent: &pkg.Package{
  1079  				Name:    "some-parent-name",
  1080  				Version: "2.0",
  1081  				Type:    pkg.JavaPkg,
  1082  				Metadata: pkg.JavaArchive{
  1083  					VirtualPath:   virtualPath + ":NEW_VIRTUAL_PATH", // note: DOES NOT match the existing virtual path
  1084  					Manifest:      nil,
  1085  					PomProperties: nil,
  1086  					Parent:        nil,
  1087  				},
  1088  			},
  1089  			// note: the SAME as the original parent values
  1090  			expectedParent: pkg.Package{
  1091  				Name:    "some-parent-name",
  1092  				Version: "NOT_THE_PARENT_VERSION", // note: the version is updated from pom properties
  1093  				Type:    pkg.JavaPkg,
  1094  				Metadata: pkg.JavaArchive{
  1095  					VirtualPath: virtualPath + ":NEW_VIRTUAL_PATH",
  1096  					Manifest:    nil,
  1097  					// note: we attach the discovered pom properties data
  1098  					PomProperties: &pkg.JavaPomProperties{
  1099  						Name:       "some-name",
  1100  						GroupID:    "some-group-id",
  1101  						ArtifactID: "some-parent-name",
  1102  						Version:    "NOT_THE_PARENT_VERSION",
  1103  					},
  1104  					Parent: nil,
  1105  				},
  1106  			},
  1107  			expectedPackage: nil,
  1108  		},
  1109  	}
  1110  
  1111  	for _, test := range tests {
  1112  		t.Run(test.name, func(t *testing.T) {
  1113  			locations := file.NewLocationSet(file.NewLocation(virtualPath))
  1114  			if test.expectedPackage != nil {
  1115  				test.expectedPackage.Locations = locations
  1116  				if test.expectedPackage.Metadata.(pkg.JavaArchive).Parent != nil {
  1117  					test.expectedPackage.Metadata.(pkg.JavaArchive).Parent.Locations = locations
  1118  				}
  1119  			}
  1120  			if test.parent != nil {
  1121  				test.parent.Locations = locations
  1122  			}
  1123  			test.expectedParent.Locations = locations
  1124  
  1125  			actualPackage := newPackageFromMavenData(context.Background(), test.props, test.project, test.parent, file.NewLocation(virtualPath), DefaultArchiveCatalogerConfig())
  1126  			if test.expectedPackage == nil {
  1127  				require.Nil(t, actualPackage)
  1128  			} else {
  1129  				pkgtest.AssertPackagesEqual(t, *test.expectedPackage, *actualPackage)
  1130  			}
  1131  
  1132  			pkgtest.AssertPackagesEqual(t, test.expectedParent, *test.parent)
  1133  		})
  1134  	}
  1135  }
  1136  
  1137  func Test_artifactIDMatchesFilename(t *testing.T) {
  1138  	tests := []struct {
  1139  		name       string
  1140  		artifactID string
  1141  		fileName   string // without version or extension
  1142  		want       bool
  1143  	}{
  1144  		{
  1145  			name:       "artifact id within file name",
  1146  			artifactID: "atlassian-extras-api",
  1147  			fileName:   "com.atlassian.extras_atlassian-extras-api",
  1148  			want:       true,
  1149  		},
  1150  		{
  1151  			name:       "file name within artifact id",
  1152  			artifactID: "atlassian-extras-api-something",
  1153  			fileName:   "atlassian-extras-api",
  1154  			want:       true,
  1155  		},
  1156  	}
  1157  	for _, tt := range tests {
  1158  		t.Run(tt.name, func(t *testing.T) {
  1159  			assert.Equal(t, tt.want, artifactIDMatchesFilename(tt.artifactID, tt.fileName))
  1160  		})
  1161  	}
  1162  }
  1163  
  1164  func Test_parseJavaArchive_regressions(t *testing.T) {
  1165  	tests := []struct {
  1166  		name                  string
  1167  		fixtureName           string
  1168  		expectedPkgs          []pkg.Package
  1169  		expectedRelationships []artifact.Relationship
  1170  		assignParent          bool
  1171  	}{
  1172  		{
  1173  			name:        "duplicate jar regression - go case (issue #2130)",
  1174  			fixtureName: "jackson-core-2.15.2",
  1175  			expectedPkgs: []pkg.Package{
  1176  				{
  1177  					Name:      "jackson-core",
  1178  					Version:   "2.15.2",
  1179  					Type:      pkg.JavaPkg,
  1180  					Language:  pkg.Java,
  1181  					PURL:      "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.15.2",
  1182  					Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/jackson-core-2.15.2.jar")),
  1183  					Licenses: pkg.NewLicenseSet(
  1184  						pkg.NewLicensesFromLocation(
  1185  							file.NewLocation("test-fixtures/jar-metadata/cache/jackson-core-2.15.2.jar"),
  1186  							"https://www.apache.org/licenses/LICENSE-2.0.txt",
  1187  						)...,
  1188  					),
  1189  					Metadata: pkg.JavaArchive{
  1190  						VirtualPath: "test-fixtures/jar-metadata/cache/jackson-core-2.15.2.jar",
  1191  						Manifest: &pkg.JavaManifest{
  1192  							Main: pkg.KeyValues{
  1193  								{Key: "Manifest-Version", Value: "1.0"},
  1194  								{Key: "Bundle-License", Value: "https://www.apache.org/licenses/LICENSE-2.0.txt"},
  1195  								{Key: "Bundle-SymbolicName", Value: "com.fasterxml.jackson.core.jackson-core"},
  1196  								{Key: "Implementation-Vendor-Id", Value: "com.fasterxml.jackson.core"},
  1197  								{Key: "Specification-Title", Value: "Jackson-core"},
  1198  								{Key: "Bundle-DocURL", Value: "https://github.com/FasterXML/jackson-core"},
  1199  								{Key: "Import-Package", Value: "com.fasterxml.jackson.core;version=...snip"},
  1200  								{Key: "Require-Capability", Value: `osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"`},
  1201  								{Key: "Export-Package", Value: "com.fasterxml.jackson.core;version...snip"},
  1202  								{Key: "Bundle-Name", Value: "Jackson-core"},
  1203  								{Key: "Multi-Release", Value: "true"},
  1204  								{Key: "Build-Jdk-Spec", Value: "1.8"},
  1205  								{Key: "Bundle-Description", Value: "Core Jackson processing abstractions"},
  1206  								{Key: "Implementation-Title", Value: "Jackson-core"},
  1207  								{Key: "Implementation-Version", Value: "2.15.2"},
  1208  								{Key: "Bundle-ManifestVersion", Value: "2"},
  1209  								{Key: "Specification-Vendor", Value: "FasterXML"},
  1210  								{Key: "Bundle-Vendor", Value: "FasterXML"},
  1211  								{Key: "Tool", Value: "Bnd-6.3.1.202206071316"},
  1212  								{Key: "Implementation-Vendor", Value: "FasterXML"},
  1213  								{Key: "Bundle-Version", Value: "2.15.2"},
  1214  								{Key: "X-Compile-Target-JDK", Value: "1.8"},
  1215  								{Key: "X-Compile-Source-JDK", Value: "1.8"},
  1216  								{Key: "Created-By", Value: "Apache Maven Bundle Plugin 5.1.8"},
  1217  								{Key: "Specification-Version", Value: "2.15.2"},
  1218  							},
  1219  						},
  1220  						// not under test
  1221  						//ArchiveDigests: []file.Digest{{Algorithm: "sha1", Value: "d8bc1d9c428c96fe447e2c429fc4304d141024df"}},
  1222  					},
  1223  				},
  1224  			},
  1225  		},
  1226  		{
  1227  			name:        "duplicate jar regression - bad case (issue #2130)",
  1228  			fixtureName: "com.fasterxml.jackson.core.jackson-core-2.15.2",
  1229  			expectedPkgs: []pkg.Package{
  1230  				{
  1231  					Name:      "jackson-core",
  1232  					Version:   "2.15.2",
  1233  					Type:      pkg.JavaPkg,
  1234  					Language:  pkg.Java,
  1235  					PURL:      "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.15.2",
  1236  					Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/com.fasterxml.jackson.core.jackson-core-2.15.2.jar")),
  1237  					Licenses: pkg.NewLicenseSet(
  1238  						pkg.NewLicensesFromLocation(
  1239  							file.NewLocation("test-fixtures/jar-metadata/cache/com.fasterxml.jackson.core.jackson-core-2.15.2.jar"),
  1240  							"https://www.apache.org/licenses/LICENSE-2.0.txt",
  1241  						)...,
  1242  					),
  1243  					Metadata: pkg.JavaArchive{
  1244  						VirtualPath: "test-fixtures/jar-metadata/cache/com.fasterxml.jackson.core.jackson-core-2.15.2.jar",
  1245  						Manifest: &pkg.JavaManifest{
  1246  							Main: pkg.KeyValues{
  1247  								{Key: "Manifest-Version", Value: "1.0"},
  1248  								{Key: "Bundle-License", Value: "https://www.apache.org/licenses/LICENSE-2.0.txt"},
  1249  								{Key: "Bundle-SymbolicName", Value: "com.fasterxml.jackson.core.jackson-core"},
  1250  								{Key: "Implementation-Vendor-Id", Value: "com.fasterxml.jackson.core"},
  1251  								{Key: "Specification-Title", Value: "Jackson-core"},
  1252  								{Key: "Bundle-DocURL", Value: "https://github.com/FasterXML/jackson-core"},
  1253  								{Key: "Import-Package", Value: "com.fasterxml.jackson.core;version=...snip"},
  1254  								{Key: "Require-Capability", Value: `osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"`},
  1255  								{Key: "Export-Package", Value: "com.fasterxml.jackson.core;version...snip"},
  1256  								{Key: "Bundle-Name", Value: "Jackson-core"},
  1257  								{Key: "Multi-Release", Value: "true"},
  1258  								{Key: "Build-Jdk-Spec", Value: "1.8"},
  1259  								{Key: "Bundle-Description", Value: "Core Jackson processing abstractions"},
  1260  								{Key: "Implementation-Title", Value: "Jackson-core"},
  1261  								{Key: "Implementation-Version", Value: "2.15.2"},
  1262  								{Key: "Bundle-ManifestVersion", Value: "2"},
  1263  								{Key: "Specification-Vendor", Value: "FasterXML"},
  1264  								{Key: "Bundle-Vendor", Value: "FasterXML"},
  1265  								{Key: "Tool", Value: "Bnd-6.3.1.202206071316"},
  1266  								{Key: "Implementation-Vendor", Value: "FasterXML"},
  1267  								{Key: "Bundle-Version", Value: "2.15.2"},
  1268  								{Key: "X-Compile-Target-JDK", Value: "1.8"},
  1269  								{Key: "X-Compile-Source-JDK", Value: "1.8"},
  1270  								{Key: "Created-By", Value: "Apache Maven Bundle Plugin 5.1.8"},
  1271  								{Key: "Specification-Version", Value: "2.15.2"},
  1272  							},
  1273  						},
  1274  						// not under test
  1275  						//ArchiveDigests: []file.Digest{{Algorithm: "sha1", Value: "abd3e329270fc54a2acaceb45420fd5710ecefd5"}},
  1276  					},
  1277  				},
  1278  			},
  1279  		},
  1280  		{
  1281  			name:         "multiple pom for parent selection regression (pr 2231)",
  1282  			fixtureName:  "api-all-2.0.0-sources",
  1283  			assignParent: true,
  1284  			expectedPkgs: []pkg.Package{
  1285  				{
  1286  					Name:      "api-all",
  1287  					Version:   "2.0.0",
  1288  					Type:      pkg.JavaPkg,
  1289  					Language:  pkg.Java,
  1290  					PURL:      "pkg:maven/org.apache.directory.api/api-all@2.0.0",
  1291  					Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar")),
  1292  					Metadata: pkg.JavaArchive{
  1293  						VirtualPath: "test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar",
  1294  						Manifest: &pkg.JavaManifest{
  1295  							Main: []pkg.KeyValue{
  1296  								{
  1297  									Key:   "Manifest-Version",
  1298  									Value: "1.0",
  1299  								},
  1300  								{
  1301  									Key:   "Built-By",
  1302  									Value: "elecharny",
  1303  								},
  1304  								{
  1305  									Key:   "Created-By",
  1306  									Value: "Apache Maven 3.6.0",
  1307  								},
  1308  								{
  1309  									Key:   "Build-Jdk",
  1310  									Value: "1.8.0_191",
  1311  								},
  1312  							},
  1313  						},
  1314  						PomProperties: &pkg.JavaPomProperties{
  1315  							Path:       "META-INF/maven/org.apache.directory.api/api-all/pom.properties",
  1316  							GroupID:    "org.apache.directory.api",
  1317  							ArtifactID: "api-all",
  1318  							Version:    "2.0.0",
  1319  						},
  1320  					},
  1321  				},
  1322  				{
  1323  					Name:      "api-asn1-api",
  1324  					Version:   "2.0.0",
  1325  					PURL:      "pkg:maven/org.apache.directory.api/api-asn1-api@2.0.0",
  1326  					Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar")),
  1327  					Type:      pkg.JavaPkg,
  1328  					Language:  pkg.Java,
  1329  					Metadata: pkg.JavaArchive{
  1330  						VirtualPath: "test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar:org.apache.directory.api:api-asn1-api",
  1331  						PomProperties: &pkg.JavaPomProperties{
  1332  							Path:       "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.properties",
  1333  							GroupID:    "org.apache.directory.api",
  1334  							ArtifactID: "api-asn1-api",
  1335  							Version:    "2.0.0",
  1336  						},
  1337  						PomProject: &pkg.JavaPomProject{
  1338  							Path:        "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml",
  1339  							ArtifactID:  "api-asn1-api",
  1340  							Name:        "Apache Directory API ASN.1 API",
  1341  							Description: "ASN.1 API",
  1342  							Parent: &pkg.JavaPomParent{
  1343  								GroupID:    "org.apache.directory.api",
  1344  								ArtifactID: "api-asn1-parent",
  1345  								Version:    "2.0.0",
  1346  							},
  1347  						},
  1348  						Parent: nil,
  1349  					},
  1350  				},
  1351  			},
  1352  		},
  1353  		{
  1354  			name:         "exclude instrumentation jars with Weave-Classes in manifest",
  1355  			fixtureName:  "spring-instrumentation-4.3.0-1.0",
  1356  			expectedPkgs: nil, // we expect no packages to be discovered when Weave-Classes present in the manifest
  1357  		},
  1358  	}
  1359  	for _, tt := range tests {
  1360  		t.Run(tt.name, func(t *testing.T) {
  1361  			gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
  1362  			if tt.assignParent {
  1363  				assignParent(&tt.expectedPkgs[0], tt.expectedPkgs[1:]...)
  1364  			}
  1365  			for i := range tt.expectedPkgs {
  1366  				tt.expectedPkgs[i].SetID()
  1367  			}
  1368  			pkgtest.NewCatalogTester().
  1369  				FromFile(t, generateJavaMetadataJarFixture(t, tt.fixtureName)).
  1370  				Expects(tt.expectedPkgs, tt.expectedRelationships).
  1371  				WithCompareOptions(
  1372  					cmpopts.IgnoreFields(pkg.JavaArchive{}, "ArchiveDigests"),
  1373  					cmp.Comparer(func(x, y pkg.KeyValue) bool {
  1374  						if x.Key != y.Key {
  1375  							return false
  1376  						}
  1377  						if x.Value != y.Value {
  1378  							return false
  1379  						}
  1380  
  1381  						return true
  1382  					}),
  1383  				).
  1384  				TestParser(t, gap.parseJavaArchive)
  1385  		})
  1386  	}
  1387  }
  1388  
  1389  func assignParent(parent *pkg.Package, childPackages ...pkg.Package) {
  1390  	for i, jp := range childPackages {
  1391  		if v, ok := jp.Metadata.(pkg.JavaArchive); ok {
  1392  			parent := *parent
  1393  			// PURL are not calculated after the fact for parent
  1394  			parent.PURL = ""
  1395  			v.Parent = &parent
  1396  			childPackages[i].Metadata = v
  1397  		}
  1398  	}
  1399  }
  1400  
  1401  func generateJavaMetadataJarFixture(t *testing.T, fixtureName string) string {
  1402  	fixturePath := filepath.Join("test-fixtures/jar-metadata/cache/", fixtureName+".jar")
  1403  	if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
  1404  		// fixture already exists...
  1405  		return fixturePath
  1406  	}
  1407  
  1408  	makeTask := filepath.Join("cache", fixtureName+".jar")
  1409  	t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
  1410  
  1411  	cwd, err := os.Getwd()
  1412  	if err != nil {
  1413  		t.Errorf("unable to get cwd: %+v", err)
  1414  	}
  1415  
  1416  	cmd := exec.Command("make", makeTask)
  1417  	cmd.Dir = filepath.Join(cwd, "test-fixtures/jar-metadata")
  1418  
  1419  	run(t, cmd)
  1420  
  1421  	return fixturePath
  1422  }
  1423  
  1424  func run(t testing.TB, cmd *exec.Cmd) {
  1425  
  1426  	stderr, err := cmd.StderrPipe()
  1427  	if err != nil {
  1428  		t.Fatalf("could not get stderr: %+v", err)
  1429  	}
  1430  	stdout, err := cmd.StdoutPipe()
  1431  	if err != nil {
  1432  		t.Fatalf("could not get stdout: %+v", err)
  1433  	}
  1434  
  1435  	err = cmd.Start()
  1436  	if err != nil {
  1437  		t.Fatalf("failed to start cmd: %+v", err)
  1438  	}
  1439  
  1440  	show := func(label string, reader io.ReadCloser) {
  1441  		scanner := bufio.NewScanner(reader)
  1442  		scanner.Split(bufio.ScanLines)
  1443  		for scanner.Scan() {
  1444  			t.Logf("%s: %s", label, scanner.Text())
  1445  		}
  1446  	}
  1447  	go show("out", stdout)
  1448  	go show("err", stderr)
  1449  
  1450  	if err := cmd.Wait(); err != nil {
  1451  		if exiterr, ok := err.(*exec.ExitError); ok {
  1452  			// The program has exited with an exit code != 0
  1453  
  1454  			// This works on both Unix and Windows. Although package
  1455  			// syscall is generally platform dependent, WaitStatus is
  1456  			// defined for both Unix and Windows and in both cases has
  1457  			// an ExitStatus() method with the same signature.
  1458  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
  1459  				if status.ExitStatus() != 0 {
  1460  					t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
  1461  				}
  1462  			}
  1463  		} else {
  1464  			t.Fatalf("unable to get generate fixture result: %+v", err)
  1465  		}
  1466  	}
  1467  }
  1468  
  1469  // setup sets up a test HTTP server for mocking requests to maven central.
  1470  // The returned url is injected into the Config so the client uses the test server.
  1471  // Tests should register handlers on mux to simulate the expected request/response structure
  1472  func setup() (mux *http.ServeMux, serverURL string, teardown func()) {
  1473  	// mux is the HTTP request multiplexer used with the test server.
  1474  	mux = http.NewServeMux()
  1475  
  1476  	// We want to ensure that tests catch mistakes where the endpoint URL is
  1477  	// specified as absolute rather than relative. It only makes a difference
  1478  	// when there's a non-empty base URL path. So, use that. See issue #752.
  1479  	apiHandler := http.NewServeMux()
  1480  	apiHandler.Handle("/", mux)
  1481  	// server is a test HTTP server used to provide mock API responses.
  1482  	server := httptest.NewServer(apiHandler)
  1483  
  1484  	return mux, server.URL, server.Close
  1485  }