enable more flexible java manifest structure (closer to the spec)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2020-10-29 16:26:04 -04:00
parent a4f22e65fc
commit a5cba13ddf
No known key found for this signature in database
GPG Key ID: 5CB45AE22BAB7EA7
7 changed files with 110 additions and 94 deletions

View File

@ -126,7 +126,7 @@
}, },
"manifest": { "manifest": {
"properties": { "properties": {
"extraFields": { "main": {
"properties": { "properties": {
"Archiver-Version": { "Archiver-Version": {
"type": "string" "type": "string"
@ -149,6 +149,12 @@
"Hudson-Version": { "Hudson-Version": {
"type": "string" "type": "string"
}, },
"Implementation-Title": {
"type": "string"
},
"Implementation-Version": {
"type": "string"
},
"Jenkins-Version": { "Jenkins-Version": {
"type": "string" "type": "string"
}, },
@ -158,6 +164,9 @@
"Main-Class": { "Main-Class": {
"type": "string" "type": "string"
}, },
"Manifest-Version": {
"type": "string"
},
"Minimum-Java-Version": { "Minimum-Java-Version": {
"type": "string" "type": "string"
}, },
@ -181,32 +190,23 @@
}, },
"Short-Name": { "Short-Name": {
"type": "string" "type": "string"
},
"Specification-Title": {
"type": "string"
} }
}, },
"required": [ "required": [
"Archiver-Version", "Archiver-Version",
"Build-Jdk", "Build-Jdk",
"Built-By", "Built-By",
"Created-By" "Created-By",
"Manifest-Version"
], ],
"type": "object" "type": "object"
},
"implementationTitle": {
"type": "string"
},
"implementationVersion": {
"type": "string"
},
"manifestVersion": {
"type": "string"
},
"specificationTitle": {
"type": "string"
} }
}, },
"required": [ "required": [
"extraFields", "main"
"manifestVersion"
], ],
"type": "object" "type": "object"
}, },

View File

@ -136,7 +136,7 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// parse the manifest file into a rich object // parse the manifest file into a rich object
manifestContents := contents[manifestMatches[0]] manifestContents := contents[manifestMatches[0]]
manifest, err := parseJavaManifest(strings.NewReader(manifestContents)) manifest, err := parseJavaManifest(j.archivePath, strings.NewReader(manifestContents))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err) return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err)
} }

View File

@ -89,18 +89,19 @@ func TestSelectName(t *testing.T) {
desc: "name from Implementation-Title", desc: "name from Implementation-Title",
archive: archiveFilename{}, archive: archiveFilename{},
manifest: pkg.JavaManifest{ manifest: pkg.JavaManifest{
Name: "", Main: map[string]string{
SpecTitle: "", "Implementation-Title": "maven-wrapper",
ImplTitle: "maven-wrapper", },
}, },
expected: "maven-wrapper", expected: "maven-wrapper",
}, },
{ {
desc: "Implementation-Title does not override", desc: "Implementation-Title does not override",
manifest: pkg.JavaManifest{ manifest: pkg.JavaManifest{
Name: "Foo", Main: map[string]string{
SpecTitle: "", "Name": "foo",
ImplTitle: "maven-wrapper", "Implementation-Title": "maven-wrapper",
},
}, },
archive: archiveFilename{ archive: archiveFilename{
fields: []map[string]string{ fields: []map[string]string{
@ -145,11 +146,12 @@ func TestParseJar(t *testing.T) {
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi", VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
Manifest: &pkg.JavaManifest{ Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
SpecTitle: "The Jenkins Plugins Parent POM Project", "Manifest-Version": "1.0",
ImplTitle: "example-jenkins-plugin", "Specification-Title": "The Jenkins Plugins Parent POM Project",
ImplVersion: "1.0-SNAPSHOT", "Implementation-Title": "example-jenkins-plugin",
Extra: map[string]string{ "Implementation-Version": "1.0-SNAPSHOT",
// extra fields...
"Archiver-Version": "Plexus Archiver", "Archiver-Version": "Plexus Archiver",
"Plugin-License-Url": "https://opensource.org/licenses/MIT", "Plugin-License-Url": "https://opensource.org/licenses/MIT",
"Plugin-License-Name": "MIT License", "Plugin-License-Name": "MIT License",
@ -191,7 +193,9 @@ func TestParseJar(t *testing.T) {
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar", VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
Manifest: &pkg.JavaManifest{ Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
"Manifest-Version": "1.0",
},
}, },
}, },
}, },
@ -212,8 +216,9 @@ func TestParseJar(t *testing.T) {
Metadata: pkg.JavaMetadata{ Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar", VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
Manifest: &pkg.JavaManifest{ Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Extra: map[string]string{ "Manifest-Version": "1.0",
// extra fields...
"Archiver-Version": "Plexus Archiver", "Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3", "Created-By": "Apache Maven 3.6.3",
"Built-By": "?", "Built-By": "?",
@ -305,11 +310,11 @@ func TestParseJar(t *testing.T) {
metadata := a.Metadata.(pkg.JavaMetadata) metadata := a.Metadata.(pkg.JavaMetadata)
metadata.Parent = nil metadata.Parent = nil
// ignore select fields // ignore select fields (only works for the main section)
for _, field := range test.ignoreExtras { for _, field := range test.ignoreExtras {
if metadata.Manifest != nil && metadata.Manifest.Extra != nil { if metadata.Manifest != nil && metadata.Manifest.Main != nil {
if _, ok := metadata.Manifest.Extra[field]; ok { if _, ok := metadata.Manifest.Main[field]; ok {
delete(metadata.Manifest.Extra, field) delete(metadata.Manifest.Main, field)
} }
} }
} }

View File

@ -4,16 +4,20 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure"
) )
const manifestGlob = "/META-INF/MANIFEST.MF" const manifestGlob = "/META-INF/MANIFEST.MF"
// nolint:funlen // nolint:funlen
func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) { // parseJavaManifest takes MANIFEST.MF file content and returns sections of parsed key/value pairs.
// For more information: https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest
func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error) {
var manifest pkg.JavaManifest var manifest pkg.JavaManifest
sections := []map[string]string{ sections := []map[string]string{
make(map[string]string), make(map[string]string),
@ -63,13 +67,24 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
return nil, fmt.Errorf("unable to read java manifest: %w", err) return nil, fmt.Errorf("unable to read java manifest: %w", err)
} }
if err := mapstructure.Decode(sections[0], &manifest); err != nil { if len(sections) > 0 {
return nil, fmt.Errorf("unable to parse java manifest: %w", err) manifest.Main = sections[0]
}
// append on extra sections
if len(sections) > 1 { if len(sections) > 1 {
manifest.Sections = sections[1:] manifest.Sections = make(map[string]map[string]string)
for i, s := range sections[1:] {
name, ok := s["Name"]
if !ok {
// per the manifest spec (https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest)
// this should never happen. If it does we want to know about it, but not necessarily stop
// cataloging entirely... for this reason we only log.
log.Errorf("java manifest section found without a name: %s", path)
name = strconv.Itoa(i)
} else {
delete(s, "Name")
}
manifest.Sections[name] = s
}
}
} }
return &manifest, nil return &manifest, nil
@ -80,24 +95,21 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
switch { switch {
case filenameObj.name() != "": case filenameObj.name() != "":
name = filenameObj.name() name = filenameObj.name()
case manifest.Name != "": case manifest.Main["Name"] != "":
// Manifest original spec... // Manifest original spec...
name = manifest.Name name = manifest.Main["Name"]
case manifest.Extra["Bundle-Name"] != "": case manifest.Main["Bundle-Name"] != "":
// BND tooling... // BND tooling...
name = manifest.Extra["Bundle-Name"] name = manifest.Main["Bundle-Name"]
case manifest.Extra["Short-Name"] != "": case manifest.Main["Short-Name"] != "":
// Jenkins... // Jenkins...
name = manifest.Extra["Short-Name"] name = manifest.Main["Short-Name"]
case manifest.Extra["Extension-Name"] != "": case manifest.Main["Extension-Name"] != "":
// Jenkins... // Jenkins...
name = manifest.Extra["Extension-Name"] name = manifest.Main["Extension-Name"]
} case manifest.Main["Implementation-Title"] != "":
// last ditch effort...
// in situations where we hit this point and no name was name = manifest.Main["Implementation-Title"]
// determined, look at the Implementation-Title
if name == "" && manifest.ImplTitle != "" {
name = manifest.ImplTitle
} }
return name return name
} }
@ -105,14 +117,14 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string { func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
var version string var version string
switch { switch {
case manifest.ImplVersion != "": case manifest.Main["Implementation-Version"] != "":
version = manifest.ImplVersion version = manifest.Main["Implementation-Version"]
case filenameObj.version() != "": case filenameObj.version() != "":
version = filenameObj.version() version = filenameObj.version()
case manifest.SpecVersion != "": case manifest.Main["Specification-Version"] != "":
version = manifest.SpecVersion version = manifest.Main["Specification-Version"]
case manifest.Extra["Plugin-Version"] != "": case manifest.Main["Plugin-Version"] != "":
version = manifest.Extra["Plugin-Version"] version = manifest.Main["Plugin-Version"]
} }
return version return version
} }

View File

@ -17,35 +17,39 @@ func TestParseJavaManifest(t *testing.T) {
{ {
fixture: "test-fixtures/manifest/small", fixture: "test-fixtures/manifest/small",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
"Manifest-Version": "1.0",
},
}, },
}, },
{ {
fixture: "test-fixtures/manifest/standard-info", fixture: "test-fixtures/manifest/standard-info",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Name: "the-best-name", "Name": "the-best-name",
SpecTitle: "the-spec-title", "Manifest-Version": "1.0",
SpecVersion: "the-spec-version", "Specification-Title": "the-spec-title",
SpecVendor: "the-spec-vendor", "Specification-Version": "the-spec-version",
ImplTitle: "the-impl-title", "Specification-Vendor": "the-spec-vendor",
ImplVersion: "the-impl-version", "Implementation-Title": "the-impl-title",
ImplVendor: "the-impl-vendor", "Implementation-Version": "the-impl-version",
"Implementation-Vendor": "the-impl-vendor",
},
}, },
}, },
{ {
fixture: "test-fixtures/manifest/extra-info", fixture: "test-fixtures/manifest/extra-info",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Extra: map[string]string{ "Manifest-Version": "1.0",
"Archiver-Version": "Plexus Archiver", "Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3", "Created-By": "Apache Maven 3.6.3",
}, },
Sections: []map[string]string{ Sections: map[string]map[string]string{
{ "thing-1": {
"Built-By": "?", "Built-By": "?",
}, },
{ "2": {
"Build-Jdk": "14.0.1", "Build-Jdk": "14.0.1",
"Main-Class": "hello.HelloWorld", "Main-Class": "hello.HelloWorld",
}, },
@ -55,8 +59,8 @@ func TestParseJavaManifest(t *testing.T) {
{ {
fixture: "test-fixtures/manifest/continuation", fixture: "test-fixtures/manifest/continuation",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
Extra: map[string]string{ "Manifest-Version": "1.0",
"Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin", "Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
}, },
}, },
@ -65,8 +69,10 @@ func TestParseJavaManifest(t *testing.T) {
// regression test, we should always keep the full version // regression test, we should always keep the full version
fixture: "test-fixtures/manifest/version-with-date", fixture: "test-fixtures/manifest/version-with-date",
expected: pkg.JavaManifest{ expected: pkg.JavaManifest{
ManifestVersion: "1.0", Main: map[string]string{
ImplVersion: "1.3 2244 October 5 2005", "Manifest-Version": "1.0",
"Implementation-Version": "1.3 2244 October 5 2005",
},
}, },
}, },
} }
@ -78,7 +84,7 @@ func TestParseJavaManifest(t *testing.T) {
t.Fatalf("could not open fixture: %+v", err) t.Fatalf("could not open fixture: %+v", err)
} }
actual, err := parseJavaManifest(fixture) actual, err := parseJavaManifest(test.fixture, fixture)
if err != nil { if err != nil {
t.Fatalf("failed to parse manifest: %+v", err) t.Fatalf("failed to parse manifest: %+v", err)
} }

View File

@ -2,6 +2,7 @@ Manifest-Version: 1.0
Archiver-Version: Plexus Archiver Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3 Created-By: Apache Maven 3.6.3
Name: thing-1
Built-By: ? Built-By: ?
Build-Jdk: 14.0.1 Build-Jdk: 14.0.1

View File

@ -22,16 +22,8 @@ type PomProperties struct {
// JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file. // JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file.
type JavaManifest struct { type JavaManifest struct {
Name string `mapstructure:"Name" json:"name,omitempty"` Main map[string]string `json:"main,omitempty"`
ManifestVersion string `mapstructure:"Manifest-Version" json:"manifestVersion,omitempty"` Sections map[string]map[string]string `json:"sections,omitempty"`
SpecTitle string `mapstructure:"Specification-Title" json:"specificationTitle,omitempty"`
SpecVersion string `mapstructure:"Specification-Version" json:"specificationVersion,omitempty"`
SpecVendor string `mapstructure:"Specification-Vendor" json:"specificationVendor,omitempty"`
ImplTitle string `mapstructure:"Implementation-Title" json:"implementationTitle,omitempty"`
ImplVersion string `mapstructure:"Implementation-Version" json:"implementationVersion,omitempty"`
ImplVendor string `mapstructure:"Implementation-Vendor" json:"implementationVendor,omitempty"`
Extra map[string]string `mapstructure:",remain" json:"extraFields,omitempty"`
Sections []map[string]string `json:"sections,omitempty"`
} }
func (m JavaMetadata) PackageURL() string { func (m JavaMetadata) PackageURL() string {