Merge pull request #74 from anchore/add-java-cataloger

Adds java cataloger
This commit is contained in:
Alex Goodman 2020-07-09 17:05:29 -04:00 committed by GitHub
commit ef8c215772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2164 additions and 111 deletions

View File

@ -74,24 +74,37 @@ jobs:
chmod 755 ${HOME}/.local/bin/docker
- run:
name: run unit tests
command: make unit
- run:
name: build hash key for tar cache
command: find integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee integration/test-fixtures/tar-cache.key
name: build cache key for java test-fixture blobs
command: |
cd imgbom/cataloger/java/test-fixtures/java-builds &&\
make packages.fingerprint
- restore_cache:
keys:
- integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.key" }}
- unit-test-java-cache-{{ checksum "imgbom/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
- run:
name: run integration tests
command: |
docker version
make integration
name: run unit tests
command: make unit
- save_cache:
key: integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.key" }}
key: unit-test-java-cache-{{ checksum "imgbom/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
paths:
- "imgbom/cataloger/java/test-fixtures/java-builds/packages"
- run:
name: build hash key for integration test-fixtures blobs
command: make integration-fingerprint
- restore_cache:
keys:
- integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.fingerprint" }}
- run:
name: run integration tests
command: make integration
- save_cache:
key: integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.fingerprint" }}
paths:
- "integration/test-fixtures/tar-cache"

7
.gitignore vendored
View File

@ -1,6 +1,11 @@
.vscode/
*.tar
*.jar
*.war
*.ear
*.jpi
*.hpi
*.zip
.idea/
*.log
.images

View File

@ -1,8 +1,3 @@
linters-settings:
funlen:
lines: 70
gocognit:
min-complexity: 35
linters:
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true

View File

@ -12,7 +12,7 @@ RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 65
COVERAGE_THRESHOLD := 70
ifndef TEMPDIR
$(error TEMPDIR is not set)
@ -71,6 +71,9 @@ integration: ## Run integration tests
$(call title,Running integration tests)
go test -tags=integration ./integration
integration/test-fixtures/tar-cache.key, integration-fingerprint:
find integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee integration/test-fixtures/tar-cache.fingerprint
clear-test-cache: ## Delete all test cache (built docker image tars)
find . -type f -wholename "**/test-fixtures/tar-cache/*.tar" -delete

View File

@ -11,7 +11,7 @@ import (
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
func parseGemfileLockEntries(reader io.Reader) ([]pkg.Package, error) {
func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)

View File

@ -67,7 +67,7 @@ func TestParseGemfileLockEntries(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseGemfileLockEntries(fixture)
actual, err := parseGemfileLockEntries(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse gemfile lock: %+v", err)
}

View File

@ -82,8 +82,9 @@ func (a *GenericCataloger) Catalog(contents map[file.Reference]string, upstreamM
continue
}
entries, err := parser(strings.NewReader(content))
entries, err := parser(string(reference.Path), strings.NewReader(content))
if err != nil {
// TODO: should we fail? or only log?
log.Errorf("cataloger '%s' failed to parse entries (reference=%+v): %w", upstreamMatcher, reference, err)
continue
}

View File

@ -39,7 +39,7 @@ func (r *testResolver) FilesByGlob(patterns ...string) ([]file.Reference, error)
return []file.Reference{ref}, nil
}
func parser(reader io.Reader) ([]pkg.Package, error) {
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
contents, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)

View File

@ -6,5 +6,5 @@ import (
"github.com/anchore/imgbom/imgbom/pkg"
)
// ParserFn standardizes a function signature for parser functions that accept file contents and return any discovered packages from that file
type ParserFn func(io.Reader) ([]pkg.Package, error)
// ParserFn standardizes a function signature for parser functions that accept the virtual file path (not usable for file reads) and contents and return any discovered packages from that file
type ParserFn func(string, io.Reader) ([]pkg.Package, error)

View File

@ -3,6 +3,7 @@ package cataloger
import (
"github.com/anchore/imgbom/imgbom/cataloger/bundler"
"github.com/anchore/imgbom/imgbom/cataloger/dpkg"
"github.com/anchore/imgbom/imgbom/cataloger/java"
"github.com/anchore/imgbom/imgbom/cataloger/python"
"github.com/anchore/imgbom/imgbom/cataloger/rpmdb"
"github.com/anchore/imgbom/imgbom/event"
@ -46,6 +47,7 @@ func newController() controller {
ctrlr.add(bundler.NewCataloger())
ctrlr.add(python.NewCataloger())
ctrlr.add(rpmdb.NewCataloger())
ctrlr.add(java.NewCataloger())
return ctrlr
}

View File

@ -12,7 +12,7 @@ import (
var errEndOfPackages = fmt.Errorf("no more packages to read")
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
buffedReader := bufio.NewReader(reader)
var packages = make([]pkg.Package, 0)

View File

@ -90,7 +90,7 @@ func TestMultiplePackages(t *testing.T) {
}
}()
pkgs, err := parseDpkgStatus(file)
pkgs, err := parseDpkgStatus(file.Name(), file)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}

View File

@ -0,0 +1,79 @@
package java
import (
"path/filepath"
"regexp"
"strings"
"github.com/anchore/imgbom/internal/log"
"github.com/anchore/imgbom/imgbom/pkg"
)
// match examples:
// pkg-extra-field-4.3.2-rc1 --> match(name=pkg-extra-field version=4.3.2-rc1)
// pkg-extra-field-4.3-rc1 --> match(name=pkg-extra-field version=4.3-rc1)
// pkg-extra-field-4.3 --> match(name=pkg-extra-field version=4.3)
var versionPattern = regexp.MustCompile(`(?P<name>.+)-(?P<version>(\d+\.)?(\d+\.)?(\*|\d+)(-[a-zA-Z0-9\-\.]+)*)`)
type archiveFilename struct {
raw string
fields []map[string]string
}
func newJavaArchiveFilename(raw string) archiveFilename {
// trim the file extension and remove any path prefixes
name := strings.TrimSuffix(filepath.Base(raw), filepath.Ext(raw))
matches := versionPattern.FindAllStringSubmatch(name, -1)
fields := make([]map[string]string, 0)
for _, match := range matches {
item := make(map[string]string)
for i, name := range versionPattern.SubexpNames() {
if i != 0 && name != "" {
item[name] = match[i]
}
}
fields = append(fields, item)
}
return archiveFilename{
raw: raw,
fields: fields,
}
}
func (a archiveFilename) extension() string {
return strings.TrimPrefix(filepath.Ext(a.raw), ".")
}
func (a archiveFilename) pkgType() pkg.Type {
switch strings.ToLower(a.extension()) {
case "jar", "war", "ear":
return pkg.JavaPkg
case "jpi", "hpi":
return pkg.JenkinsPluginPkg
default:
return pkg.UnknownPkg
}
}
func (a archiveFilename) version() string {
if len(a.fields) > 1 {
log.Errorf("discovered multiple name-version pairings from %q: %+v", a.raw, a.fields)
return ""
} else if len(a.fields) < 1 {
return ""
}
return a.fields[0]["version"]
}
func (a archiveFilename) name() string {
// there should be only one name, if there is more or less then something is wrong
if len(a.fields) != 1 {
return ""
}
return a.fields[0]["name"]
}

View File

@ -0,0 +1,87 @@
package java
import (
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
tests := []struct {
filename string
version string
extension string
name string
ty pkg.Type
}{
{
filename: "pkg-maven-4.3.2.blerg",
version: "4.3.2",
extension: "blerg",
name: "pkg-maven",
ty: pkg.UnknownPkg,
},
{
filename: "pkg-maven-4.3.2.jar",
version: "4.3.2",
extension: "jar",
name: "pkg-maven",
ty: pkg.JavaPkg,
},
{
filename: "pkg-extra-field-maven-4.3.2.war",
version: "4.3.2",
extension: "war",
name: "pkg-extra-field-maven",
ty: pkg.JavaPkg,
},
{
filename: "pkg-extra-field-maven-4.3.2-rc1.ear",
version: "4.3.2-rc1",
extension: "ear",
name: "pkg-extra-field-maven",
ty: pkg.JavaPkg,
},
{
filename: "/some/path/pkg-extra-field-maven-4.3.2-rc1.jpi",
version: "4.3.2-rc1",
extension: "jpi",
name: "pkg-extra-field-maven",
ty: pkg.JenkinsPluginPkg,
},
{
filename: "/some/path-with-version-5.4.3/pkg-extra-field-maven-4.3.2-rc1.hpi",
version: "4.3.2-rc1",
extension: "hpi",
name: "pkg-extra-field-maven",
ty: pkg.JenkinsPluginPkg,
},
}
for _, test := range tests {
t.Run(test.filename, func(t *testing.T) {
obj := newJavaArchiveFilename(test.filename)
version := obj.version()
if version != test.version {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(version, test.version, true)
t.Errorf("mismatched version:\n%s", dmp.DiffPrettyText(diffs))
}
extension := obj.extension()
if extension != test.extension {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(extension, test.extension, true)
t.Errorf("mismatched extension:\n%s", dmp.DiffPrettyText(diffs))
}
name := obj.name()
if name != test.name {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(name, test.name, true)
t.Errorf("mismatched name:\n%s", dmp.DiffPrettyText(diffs))
}
})
}
}

View File

@ -0,0 +1,38 @@
package java
import (
"github.com/anchore/imgbom/imgbom/cataloger/common"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/stereoscope/pkg/file"
)
type Cataloger struct {
cataloger common.GenericCataloger
}
func NewCataloger() *Cataloger {
globParsers := map[string]common.ParserFn{
"*.jar": parseJavaArchive,
"*.war": parseJavaArchive,
"*.ear": parseJavaArchive,
"*.jpi": parseJavaArchive,
"*.hpi": parseJavaArchive,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
func (a *Cataloger) Name() string {
return "java-cataloger"
}
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
}

View File

@ -0,0 +1,120 @@
package java
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/anchore/imgbom/internal/file"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/mitchellh/mapstructure"
)
const manifestPath = "META-INF/MANIFEST.MF"
func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
var manifest pkg.JavaManifest
manifestMap := make(map[string]string)
scanner := bufio.NewScanner(reader)
var lastKey string
for scanner.Scan() {
line := scanner.Text()
// ignore empty lines
if strings.TrimSpace(line) == "" {
continue
}
if line[0] == ' ' {
// this is a continuation
if lastKey == "" {
return nil, fmt.Errorf("found continuation with no previous key (%s)", line)
}
manifestMap[lastKey] += strings.TrimSpace(line)
} else {
// this is a new key-value pair
idx := strings.Index(line, ":")
if idx == -1 {
return nil, fmt.Errorf("unable to split java manifest key-value pairs: %q", line)
}
key := strings.TrimSpace(line[0:idx])
value := strings.TrimSpace(line[idx+1:])
manifestMap[key] = value
// keep track of key for future continuations
lastKey = key
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("unable to read java manifest: %w", err)
}
if err := mapstructure.Decode(manifestMap, &manifest); err != nil {
return nil, fmt.Errorf("unable to parse java manifest: %w", err)
}
return &manifest, nil
}
func newPackageFromJavaManifest(virtualPath, archivePath string, fileManifest file.ZipManifest) (*pkg.Package, error) {
// search and parse java manifest files
manifestMatches := fileManifest.GlobMatch(manifestPath)
if len(manifestMatches) > 1 {
return nil, fmt.Errorf("found multiple manifests in the jar: %+v", manifestMatches)
} else if len(manifestMatches) == 0 {
// we did not find any manifests, but that may not be a problem (there may be other information to generate packages for)
return nil, nil
}
// fetch the manifest file
contents, err := file.ExtractFilesFromZip(archivePath, manifestMatches...)
if err != nil {
return nil, fmt.Errorf("unable to extract java manifests (%s): %w", virtualPath, err)
}
// parse the manifest file into a rich object
manifestContents := contents[manifestMatches[0]]
manifest, err := parseJavaManifest(strings.NewReader(manifestContents))
if err != nil {
return nil, fmt.Errorf("failed to parse java manifest (%s): %w", virtualPath, err)
}
filenameObj := newJavaArchiveFilename(virtualPath)
var name string
switch {
case manifest.Name != "":
name = manifest.Name
case filenameObj.name() != "":
name = filenameObj.name()
case manifest.Extra["Short-Name"] != "":
name = manifest.Extra["Short-Name"]
case manifest.Extra["Extension-Name"] != "":
name = manifest.Extra["Extension-Name"]
}
var version string
switch {
case manifest.ImplVersion != "":
version = manifest.ImplVersion
case filenameObj.version() != "":
version = filenameObj.version()
case manifest.SpecVersion != "":
version = manifest.SpecVersion
case manifest.Extra["Plugin-Version"] != "":
name = manifest.Extra["Plugin-Version"]
}
return &pkg.Package{
Name: name,
Version: version,
Language: pkg.Java,
Metadata: pkg.JavaMetadata{
Manifest: manifest,
},
}, nil
}

View File

@ -0,0 +1,86 @@
package java
import (
"encoding/json"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/go-test/deep"
"os"
"testing"
)
func TestParseJavaManifest(t *testing.T) {
tests := []struct {
fixture string
expected pkg.JavaManifest
}{
{
fixture: "test-fixtures/manifest/small",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
},
},
{
fixture: "test-fixtures/manifest/standard-info",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Name: "the-best-name",
SpecTitle: "the-spec-title",
SpecVersion: "the-spec-version",
SpecVendor: "the-spec-vendor",
ImplTitle: "the-impl-title",
ImplVersion: "the-impl-version",
ImplVendor: "the-impl-vendor",
},
},
{
fixture: "test-fixtures/manifest/extra-info",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Extra: map[string]string{
"Archiver-Version": "Plexus Archiver",
"Build-Jdk": "14.0.1",
"Built-By": "?",
"Created-By": "Apache Maven 3.6.3",
"Main-Class": "hello.HelloWorld",
},
},
},
{
fixture: "test-fixtures/manifest/continuation",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Extra: map[string]string{
"Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
fixture, err := os.Open(test.fixture)
if err != nil {
t.Fatalf("could not open fixture: %+v", err)
}
actual, err := parseJavaManifest(fixture)
if err != nil {
t.Fatalf("failed to parse manifest: %+v", err)
}
diffs := deep.Equal(actual, &test.expected)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("diff: %+v", d)
}
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
})
}
}

View File

@ -0,0 +1,116 @@
package java
import (
"fmt"
"io"
"strings"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/internal"
"github.com/anchore/imgbom/internal/file"
)
func uniquePkgKey(p *pkg.Package) string {
if p == nil {
return ""
}
return fmt.Sprintf("%s|%s", p.Name, p.Version)
}
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
discoveredPkgs := internal.NewStringSet()
_, archivePath, cleanupFn, err := saveArchiveToTmp(reader)
// note: even on error, we should always run cleanup functions
defer cleanupFn()
if err != nil {
return nil, fmt.Errorf("unable to process jar: %w", err)
}
fileManifest, err := file.ZipFileManifest(archivePath)
if err != nil {
return nil, fmt.Errorf("unable to read files from jar: %w", err)
}
// find the parent package from the java manifest
parentPkg, err := newPackageFromJavaManifest(virtualPath, archivePath, fileManifest)
if err != nil {
return nil, fmt.Errorf("could not generate package from %s: %w", virtualPath, err)
}
// don't add the parent package yet, we still may discover aux info to add to the metadata (but still track it as added to prevent duplicates)
parentKey := uniquePkgKey(parentPkg)
if parentKey != "" {
discoveredPkgs.Add(parentKey)
}
// find aux packages from pom.properties
auxPkgs, err := newPackagesFromPomProperties(parentPkg, discoveredPkgs, virtualPath, archivePath, fileManifest)
if err != nil {
return nil, err
}
pkgs = append(pkgs, auxPkgs...)
// TODO: search for nested jars... but only in ears? or all the time? and remember we need to capture pkg metadata and type appropriately for each
// lastly, add the parent package to the list (assuming the parent exists)
if parentPkg != nil {
// only the parent package gets the type, nested packages may be of a different package type (or not of a package type at all, since they may not be bundled)
parentPkg.Type = newJavaArchiveFilename(virtualPath).pkgType()
pkgs = append([]pkg.Package{*parentPkg}, pkgs...)
}
return pkgs, nil
}
func newPackagesFromPomProperties(parentPkg *pkg.Package, discoveredPkgs internal.StringSet, virtualPath, archivePath string, fileManifest file.ZipManifest) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
parentKey := uniquePkgKey(parentPkg)
// search and parse pom.properties files & fetch the contents
contents, err := file.ExtractFilesFromZip(archivePath, fileManifest.GlobMatch(pomPropertiesGlob)...)
if err != nil {
return nil, fmt.Errorf("unable to extract pom.properties: %w", err)
}
// parse the manifest file into a rich object
for propsPath, propsContents := range contents {
propsObj, err := parsePomProperties(propsPath, strings.NewReader(propsContents))
if err != nil {
return nil, fmt.Errorf("failed to parse pom.properties (%s): %w", virtualPath, err)
}
if propsObj != nil {
if propsObj.Version != "" && propsObj.ArtifactID != "" {
// TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package
// discovered props = new package
p := pkg.Package{
Name: propsObj.ArtifactID,
Version: propsObj.Version,
Language: pkg.Java,
Metadata: pkg.JavaMetadata{
PomProperties: propsObj,
Parent: parentPkg,
},
}
pkgKey := uniquePkgKey(&p)
if !discoveredPkgs.Contains(pkgKey) {
// only keep packages we haven't seen yet
pkgs = append(pkgs, p)
} else if pkgKey == parentKey {
// we've run across more information about our parent package, add this info to the parent package metadata
parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata)
if ok {
parentMetadata.PomProperties = propsObj
parentPkg.Metadata = parentMetadata
}
}
}
}
}
return pkgs, nil
}

View File

@ -0,0 +1,255 @@
package java
import (
"bufio"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/go-test/deep"
"github.com/gookit/color"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
)
func generateJavaBuildFixture(t *testing.T, fixturePath string) {
if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
// fixture already exists...
return
}
makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/")
t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("make", makeTask)
cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/")
stderr, err := cmd.StderrPipe()
if err != nil {
t.Fatalf("could not get stderr: %+v", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("could not get stdout: %+v", err)
}
err = cmd.Start()
if err != nil {
t.Fatalf("failed to start cmd: %+v", err)
}
show := func(label string, reader io.ReadCloser) {
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
t.Logf("%s: %s", label, scanner.Text())
}
}
go show("out", stdout)
go show("err", stderr)
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture result: %+v", err)
}
}
}
func TestParseJar(t *testing.T) {
tests := []struct {
fixture string
expected map[string]pkg.Package
ignoreExtras []string
}{
{
fixture: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
ignoreExtras: []string{"Plugin-Version"}, // has dynamic date
expected: map[string]pkg.Package{
"example-jenkins-plugin": {
Name: "example-jenkins-plugin",
Version: "1.0-SNAPSHOT",
Language: pkg.Java,
Type: pkg.JenkinsPluginPkg,
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
SpecTitle: "The Jenkins Plugins Parent POM Project",
ImplTitle: "example-jenkins-plugin",
ImplVersion: "1.0-SNAPSHOT",
Extra: map[string]string{
"Archiver-Version": "Plexus Archiver",
"Plugin-License-Url": "https://opensource.org/licenses/MIT",
"Plugin-License-Name": "MIT License",
"Created-By": "Apache Maven",
"Built-By": "?",
"Build-Jdk": "14.0.1",
"Jenkins-Version": "2.164.3",
"Minimum-Java-Version": "1.8",
"Plugin-Developers": "",
"Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
"Extension-Name": "example-jenkins-plugin",
"Short-Name": "example-jenkins-plugin",
"Group-Id": "io.jenkins.plugins",
"Plugin-Dependencies": "structs:1.20",
//"Plugin-Version": "1.0-SNAPSHOT (private-07/09/2020 13:30-?)",
"Hudson-Version": "2.164.3",
"Long-Name": "TODO Plugin",
},
},
PomProperties: &pkg.PomProperties{
Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.properties",
GroupID: "io.jenkins.plugins",
ArtifactID: "example-jenkins-plugin",
Version: "1.0-SNAPSHOT",
},
},
},
},
},
{
fixture: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
expected: map[string]pkg.Package{
"example-java-app-gradle": {
Name: "example-java-app-gradle",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
},
},
},
},
},
{
fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
expected: map[string]pkg.Package{
"example-java-app-maven": {
Name: "example-java-app-maven",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
Extra: map[string]string{
"Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3",
"Built-By": "?",
"Build-Jdk": "14.0.1",
"Main-Class": "hello.HelloWorld",
},
},
PomProperties: &pkg.PomProperties{
Path: "META-INF/maven/org.anchore/example-java-app-maven/pom.properties",
GroupID: "org.anchore",
ArtifactID: "example-java-app-maven",
Version: "0.1.0",
},
},
},
"joda-time": {
Name: "joda-time",
Version: "2.9.2",
Language: pkg.Java,
Type: pkg.UnknownPkg,
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
Path: "META-INF/maven/joda-time/joda-time/pom.properties",
GroupID: "joda-time",
ArtifactID: "joda-time",
Version: "2.9.2",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
generateJavaBuildFixture(t, test.fixture)
fixture, err := os.Open(test.fixture)
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseJavaArchive(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse java archive: %+v", err)
}
if len(actual) != len(test.expected) {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), 1)
}
var parent *pkg.Package
for _, a := range actual {
if strings.Contains(a.Name, "example-") {
parent = &a
}
}
if parent == nil {
t.Fatal("could not find the parent pkg")
}
for _, a := range actual {
e, ok := test.expected[a.Name]
if !ok {
t.Errorf("entry not found: %s", a.Name)
continue
}
if a.Name != parent.Name && a.Metadata.(pkg.JavaMetadata).Parent != nil && a.Metadata.(pkg.JavaMetadata).Parent.Name != parent.Name {
t.Errorf("mismatched parent: %+v", a.Metadata.(pkg.JavaMetadata).Parent)
}
// we need to compare the other fields without parent attached
metadata := a.Metadata.(pkg.JavaMetadata)
metadata.Parent = nil
// ignore select fields
for _, field := range test.ignoreExtras {
delete(metadata.Manifest.Extra, field)
}
// write censored data back
a.Metadata = metadata
diffs := deep.Equal(a, e)
if len(diffs) > 0 {
t.Errorf("diffs found for %q", a.Name)
for _, d := range diffs {
t.Errorf("diff: %+v", d)
}
}
}
})
}
}

View File

@ -0,0 +1,48 @@
package java
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/mitchellh/mapstructure"
)
const pomPropertiesGlob = "*pom.properties"
func parsePomProperties(path string, reader io.Reader) (*pkg.PomProperties, error) {
var props pkg.PomProperties
propMap := make(map[string]string)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
// ignore empty lines and comments
if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimLeft(line, " "), "#") {
continue
}
idx := strings.Index(line, "=")
if idx == -1 {
return nil, fmt.Errorf("unable to split pom.properties key-value pairs: %q", line)
}
key := strings.TrimSpace(line[0:idx])
value := strings.TrimSpace(line[idx+1:])
propMap[key] = value
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("unable to read pom.properties: %w", err)
}
if err := mapstructure.Decode(propMap, &props); err != nil {
return nil, fmt.Errorf("unable to parse pom.properties: %w", err)
}
props.Path = path
return &props, nil
}

View File

@ -0,0 +1,68 @@
package java
import (
"encoding/json"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/go-test/deep"
"os"
"testing"
)
func TestParseJavaPomProperties(t *testing.T) {
tests := []struct {
fixture string
expected pkg.PomProperties
}{
{
fixture: "test-fixtures/pom/small.pom.properties",
expected: pkg.PomProperties{
Path: "test-fixtures/pom/small.pom.properties",
GroupID: "org.anchore",
ArtifactID: "example-java-app-maven",
Version: "0.1.0",
},
},
{
fixture: "test-fixtures/pom/extra.pom.properties",
expected: pkg.PomProperties{
Path: "test-fixtures/pom/extra.pom.properties",
GroupID: "org.anchore",
ArtifactID: "example-java-app-maven",
Version: "0.1.0",
Name: "something-here",
Extra: map[string]string{
"another": "thing",
"sweet": "work",
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
fixture, err := os.Open(test.fixture)
if err != nil {
t.Fatalf("could not open fixture: %+v", err)
}
actual, err := parsePomProperties(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse manifest: %+v", err)
}
diffs := deep.Equal(actual, &test.expected)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("diff: %+v", d)
}
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
})
}
}

View File

@ -0,0 +1,45 @@
package java
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/anchore/imgbom/internal/log"
)
func saveArchiveToTmp(reader io.Reader) (string, string, func(), error) {
tempDir, err := ioutil.TempDir("", "imgbom-jar-contents-")
if err != nil {
return "", "", func() {}, fmt.Errorf("unable to create tempdir for jar processing: %w", err)
}
cleanupFn := func() {
err = os.RemoveAll(tempDir)
if err != nil {
log.Errorf("unable to cleanup jar tempdir: %w", err)
}
}
archivePath := filepath.Join(tempDir, "archive")
contentDir := filepath.Join(tempDir, "contents")
err = os.Mkdir(contentDir, 0755)
if err != nil {
return contentDir, "", cleanupFn, fmt.Errorf("unable to create processing tempdir: %w", err)
}
archiveFile, err := os.Create(archivePath)
if err != nil {
return contentDir, "", cleanupFn, fmt.Errorf("unable to create archive: %w", err)
}
_, err = io.Copy(archiveFile, reader)
if err != nil {
return contentDir, archivePath, cleanupFn, fmt.Errorf("unable to copy archive: %w", err)
}
return contentDir, archivePath, cleanupFn, nil
}

View File

@ -0,0 +1,5 @@
/packages/*
*.fingerprint
# maven when running in a volume may spit out directories like this
**/\?/
\?/

View File

@ -0,0 +1,45 @@
PKGSDIR=packages
ifndef PKGSDIR
$(error PKGSDIR is not set)
endif
all: $(PKGSDIR)/example-java-app-maven-0.1.0.jar $(PKGSDIR)/example-java-app-gradle-0.1.0.jar $(PKGSDIR)/example-jenkins-plugin.hpi
clean: clean-examples
rm -f $(PKGSDIR)/*
clean-examples: clean-gradle clean-maven clean-jenkins
.PHONY: maven gradle clean clean-gradle clean-maven clean-jenkins clean-examples
# Maven...
$(PKGSDIR)/example-java-app-maven-0.1.0.jar:
./build-example-java-app-maven.sh $(PKGSDIR)
clean-maven:
rm -rf example-java-app/target \
example-java-app/dependency-reduced-pom.xml
# Gradle...
$(PKGSDIR)/example-java-app-gradle-0.1.0.jar:
./build-example-java-app-gradle.sh $(PKGSDIR)
clean-gradle:
rm -rf example-java-app/.gradle \
example-java-app/build
# Jenkins plugin
$(PKGSDIR)/example-jenkins-plugin.hpi , $(PKGSDIR)/example-jenkins-plugin.jar:
./build-example-jenkins-plugin.sh $(PKGSDIR)
clean-jenkins:
rm -rf example-jenkins-plugin/target \
example-jenkins-plugin/dependency-reduced-pom.xml \
example-jenkins-plugin/*.exploding
# we need a way to determine if CI should bust the test cache based on the source material
$(PKGSDIR).fingerprint: clean-examples
mkdir -p $(PKGSDIR)
find example-* -type f -exec sha256sum {} \; > $(PKGSDIR).fingerprint
sha256sum $(PKGSDIR).fingerprint

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -uxe
# note: this can be easily done in a 1-liner, however circle CI does NOT allow volume mounts from the host in docker executors (since they are on remote hosts, where the host files are inaccessible)
PKGSDIR=$1
CTRID=$(docker create -u "$(id -u):$(id -g)" -v /example-java-app -w /example-java-app gradle:jdk gradle build)
function cleanup() {
docker rm "${CTRID}"
}
trap cleanup EXIT
set +e
docker cp "$(pwd)/example-java-app" "${CTRID}:/"
docker start -a "${CTRID}"
mkdir -p "$PKGSDIR"
docker cp "${CTRID}:/example-java-app/build/libs/example-java-app-gradle-0.1.0.jar" "$PKGSDIR"

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -uxe
# note: this can be easily done in a 1-liner, however circle CI does NOT allow volume mounts from the host in docker executors (since they are on remote hosts, where the host files are inaccessible)
PKGSDIR=$1
CTRID=$(docker create -u "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/.m2 -v /example-java-app -w /example-java-app maven:openjdk mvn -Duser.home=/tmp -DskipTests package)
function cleanup() {
docker rm "${CTRID}"
}
trap cleanup EXIT
set +e
docker cp "$(pwd)/example-java-app" "${CTRID}:/"
docker start -a "${CTRID}"
mkdir -p "$PKGSDIR"
docker cp "${CTRID}:/example-java-app/target/example-java-app-maven-0.1.0.jar" "$PKGSDIR"

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -uxe
# note: this can be easily done in a 1-liner, however circle CI does NOT allow volume mounts from the host in docker executors (since they are on remote hosts, where the host files are inaccessible)
PKGSDIR=$1
CTRID=$(docker create -u "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/.m2 -v /example-jenkins-plugin -w /example-jenkins-plugin maven:openjdk mvn -Duser.home=/tmp -DskipTests package)
function cleanup() {
docker rm "${CTRID}"
}
trap cleanup EXIT
set +e
docker cp "$(pwd)/example-jenkins-plugin" "${CTRID}:/"
docker start -a "${CTRID}"
mkdir -p "$PKGSDIR"
docker cp "${CTRID}:/example-jenkins-plugin/target/example-jenkins-plugin.hpi" "$PKGSDIR"
docker cp "${CTRID}:/example-jenkins-plugin/target/example-jenkins-plugin.jar" "$PKGSDIR"

View File

@ -0,0 +1,6 @@
# maven build creates this when in a container volume
/?/
/.gradle/
/build/
target/
dependency-reduced-pom.xml

View File

@ -0,0 +1,31 @@
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'application'
mainClassName = 'hello.HelloWorld'
// tag::repositories[]
repositories {
mavenCentral()
}
// end::repositories[]
// tag::jar[]
jar {
baseName = 'example-java-app-gradle'
version = '0.1.0'
}
// end::jar[]
// tag::dependencies[]
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile "joda-time:joda-time:2.2"
testCompile "junit:junit:4.12"
}
// end::dependencies[]
// tag::wrapper[]
// end::wrapper[]

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.anchore</groupId>
<artifactId>example-java-app-maven</artifactId>
<packaging>jar</packaging>
<version>0.1.0</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- tag::joda[] -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.2</version>
</dependency>
<!-- end::joda[] -->
<!-- tag::junit[] -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- end::junit[] -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>hello.HelloWorld</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,7 @@
package hello;
public class Greeter {
public String sayHello() {
return "Hello world!";
}
}

View File

@ -0,0 +1,13 @@
package hello;
import org.joda.time.LocalTime;
public class HelloWorld {
public static void main(String[] args) {
LocalTime currentTime = new LocalTime();
System.out.println("The current local time is: " + currentTime);
Greeter greeter = new Greeter();
System.out.println(greeter.sayHello());
}
}

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>3.50</version>
<relativePath />
</parent>
<groupId>io.jenkins.plugins</groupId>
<artifactId>example-jenkins-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>hpi</packaging>
<properties>
<!-- Baseline Jenkins version you use to build the plugin. Users must have this version or newer to run. -->
<jenkins.version>2.164.3</jenkins.version>
<java.level>8</java.level>
<!-- Other properties you may want to use:
~ jenkins-test-harness.version: Jenkins Test Harness version you use to test the plugin. For Jenkins version >= 1.580.1 use JTH 2.0 or higher.
~ hpi-plugin.version: The HPI Maven Plugin version used by the plugin..
~ stapler-plugin.version: The Stapler Maven plugin version required by the plugin.
-->
</properties>
<name>TODO Plugin</name>
<!-- The default licence for Jenkins OSS Plugins is MIT. Substitute for the applicable one if needed. -->
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Pick up common dependencies for 2.164.x: https://github.com/jenkinsci/bom#usage -->
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.164.x</artifactId>
<version>3</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>structs</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- If you want this to appear on the wiki page:
<developers>
<developer>
<id>bhacker</id>
<name>Bob Q. Hacker</name>
<email>bhacker@nowhere.net</email>
</developer>
</developers> -->
<!-- Assuming you want to host on @jenkinsci:
<url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
<scm>
<connection>scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git</connection>
<developerConnection>scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git</developerConnection>
<url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
</scm>
-->
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
</project>

View File

@ -0,0 +1,81 @@
package io.jenkins.plugins.sample;
import hudson.Launcher;
import hudson.Extension;
import hudson.FilePath;
import hudson.util.FormValidation;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import javax.servlet.ServletException;
import java.io.IOException;
import jenkins.tasks.SimpleBuildStep;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundSetter;
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
private final String name;
private boolean useFrench;
@DataBoundConstructor
public HelloWorldBuilder(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean isUseFrench() {
return useFrench;
}
@DataBoundSetter
public void setUseFrench(boolean useFrench) {
this.useFrench = useFrench;
}
@Override
public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
if (useFrench) {
listener.getLogger().println("Bonjour, " + name + "!");
} else {
listener.getLogger().println("Hello, " + name + "!");
}
}
@Symbol("greet")
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
public FormValidation doCheckName(@QueryParameter String value, @QueryParameter boolean useFrench)
throws IOException, ServletException {
if (value.length() == 0)
return FormValidation.error(Messages.HelloWorldBuilder_DescriptorImpl_errors_missingName());
if (value.length() < 4)
return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_tooShort());
if (!useFrench && value.matches(".*[éáàç].*")) {
return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_reallyFrench());
}
return FormValidation.ok();
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
@Override
public String getDisplayName() {
return Messages.HelloWorldBuilder_DescriptorImpl_DisplayName();
}
}
}

View File

@ -0,0 +1,4 @@
<?jelly escape-by-default='true'?>
<div>
TODO
</div>

View File

@ -0,0 +1,12 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Name}" field="name">
<f:textbox />
</f:entry>
<f:advanced>
<f:entry title="${%French}" field="useFrench"
description="${%FrenchDescr}">
<f:checkbox />
</f:entry>
</f:advanced>
</j:jelly>

View File

@ -0,0 +1,3 @@
Name=Name
French=French
FrenchDescr=Check if we should say hello in French

View File

@ -0,0 +1,3 @@
Name=Nom
French=Fran\u00e7ais
FrenchDescr=V\u00e9rifie qu'on dit bien hello en fran\u00e7ais

View File

@ -0,0 +1,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=Please set a name
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=Isn't the name too short?
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=Are you actually French?
HelloWorldBuilder.DescriptorImpl.DisplayName=Say hello world

View File

@ -0,0 +1,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=Veuillez saisir un nom
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=Le nom n'est-il pas trop court ?
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=\u00CAtes vous vraiment fran\u00E7ais ?
HelloWorldBuilder.DescriptorImpl.DisplayName=Dis hello world

View File

@ -0,0 +1,4 @@
Manifest-Version: 1.0
Plugin-ScmUrl: https://github.com/jenkinsci/plugin-pom/example-jenkins
-plugin

View File

@ -0,0 +1,8 @@
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3
Built-By: ?
Build-Jdk: 14.0.1
Main-Class: hello.HelloWorld

View File

@ -0,0 +1,2 @@
Manifest-Version: 1.0

View File

@ -0,0 +1,9 @@
Manifest-Version: 1.0
Name: the-best-name
Specification-Title: the-spec-title
Specification-Vendor: the-spec-vendor
Specification-Version: the-spec-version
Implementation-Title: the-impl-title
Implementation-Vendor: the-impl-vendor
Implementation-Version: the-impl-version

View File

@ -0,0 +1,8 @@
#Generated by Maven
#Tue Jul 07 18:59:56 GMT 2020
groupId=org.anchore
artifactId=example-java-app-maven
version=0.1.0
name=something-here
another=thing
sweet=work

View File

@ -0,0 +1,5 @@
#Generated by Maven
#Tue Jul 07 18:59:56 GMT 2020
groupId=org.anchore
artifactId=example-java-app-maven
version=0.1.0

View File

@ -9,7 +9,7 @@ import (
"github.com/anchore/imgbom/imgbom/pkg"
)
func parseWheelMetadata(reader io.Reader) ([]pkg.Package, error) {
func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.WheelPkg
@ -17,7 +17,7 @@ func parseWheelMetadata(reader io.Reader) ([]pkg.Package, error) {
return packages, err
}
func parseEggMetadata(reader io.Reader) ([]pkg.Package, error) {
func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.EggPkg

View File

@ -57,7 +57,7 @@ func TestParseEggMetadata(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseEggMetadata(fixture)
actual, err := parseEggMetadata(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse egg-info: %+v", err)
}
@ -81,7 +81,7 @@ func TestParseWheelMetadata(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseWheelMetadata(fixture)
actual, err := parseWheelMetadata(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse dist-info: %+v", err)
}

View File

@ -12,7 +12,7 @@ import (
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
)
func parseRpmDB(reader io.Reader) ([]pkg.Package, error) {
func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
if err != nil {
return nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)

View File

@ -26,7 +26,7 @@ func TestParseRpmDB(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseRpmDB(fixture)
actual, err := parseRpmDB(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse rpmdb: %+v", err)
}

View File

@ -2,7 +2,7 @@ package pkg
const (
UnknownLanguage Language = iota
//Java
Java
//JavaScript
Python
Ruby
@ -12,14 +12,14 @@ type Language uint
var languageStr = []string{
"UnknownLanguage",
//"java",
"java",
//"javascript",
"python",
"ruby",
}
var AllLanguages = []Language{
//Java,
Java,
//JavaScript,
Python,
Ruby,

View File

@ -14,3 +14,30 @@ type RpmMetadata struct {
Arch string `mapstructure:"Arch"`
Release string `mapstructure:"Release"`
}
type JavaManifest struct {
Name string `mapstructure:"Name"`
ManifestVersion string `mapstructure:"Manifest-Version"`
SpecTitle string `mapstructure:"Specification-Title"`
SpecVersion string `mapstructure:"Specification-Version"`
SpecVendor string `mapstructure:"Specification-Vendor"`
ImplTitle string `mapstructure:"Implementation-Title"`
ImplVersion string `mapstructure:"Implementation-Version"`
ImplVendor string `mapstructure:"Implementation-Vendor"`
Extra map[string]string `mapstructure:",remain"`
}
type PomProperties struct {
Path string
Name string `mapstructure:"name"`
GroupID string `mapstructure:"groupId"`
ArtifactID string `mapstructure:"artifactId"`
Version string `mapstructure:"version"`
Extra map[string]string `mapstructure:",remain"`
}
type JavaMetadata struct {
Manifest *JavaManifest `mapstructure:"Manifest"`
PomProperties *PomProperties `mapstructure:"PomProperties"`
Parent *Package
}

View File

@ -9,8 +9,10 @@ import (
type ID int64
// TODO: add field to trace which cataloger detected this
// Package represents an application or library that has been bundled into a distributable format
type Package struct {
id ID
id ID // this is set when a package is added to the catalog
Name string
Version string
FoundBy string

View File

@ -9,6 +9,8 @@ const (
//PacmanPkg
RpmPkg
WheelPkg
JavaPkg
JenkinsPluginPkg
)
type Type uint
@ -22,6 +24,8 @@ var typeStr = []string{
//"pacman",
"rpm",
"wheel",
"java-archive",
"jenkins-plugin",
}
var AllPkgs = []Type{
@ -32,6 +36,8 @@ var AllPkgs = []Type{
//PacmanPkg,
RpmPkg,
WheelPkg,
JavaPkg,
JenkinsPluginPkg,
}
func (t Type) String() string {

View File

@ -56,6 +56,7 @@ type artifact struct {
Metadata interface{} `json:"metadata"`
}
// nolint:funlen
func (pres *Presenter) Present(output io.Writer) error {
tags := make([]string, len(pres.img.Metadata.Tags))
for idx, tag := range pres.img.Metadata.Tags {

View File

@ -3,75 +3,69 @@
package integration
import (
"bytes"
"flag"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/imgbom/imgbom"
"github.com/anchore/imgbom/imgbom/presenter"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func TestDirTextPresenter(t *testing.T) {
var buffer bytes.Buffer
protocol := imgbom.NewProtocol("dir://test-fixtures")
if protocol.Type != imgbom.DirProtocol {
t.Errorf("unexpected protocol returned: %v != %v", protocol.Type, imgbom.DirProtocol)
}
// these tests are providing inconsistent results... we can fix in another PR
catalog, err := imgbom.CatalogDir(protocol.Value, scope.AllLayersScope)
if err != nil {
t.Errorf("could not produce catalog: %w", err)
}
presenterOpt := presenter.ParseOption("text")
dirPresenter := presenter.GetDirPresenter(presenterOpt, protocol.Value, catalog)
dirPresenter.Present(&buffer)
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
func TestDirJsonPresenter(t *testing.T) {
var buffer bytes.Buffer
protocol := imgbom.NewProtocol("dir://test-fixtures")
if protocol.Type != imgbom.DirProtocol {
t.Errorf("unexpected protocol returned: %v != %v", protocol.Type, imgbom.DirProtocol)
}
catalog, err := imgbom.CatalogDir(protocol.Value, scope.AllLayersScope)
if err != nil {
t.Errorf("could not produce catalog: %w", err)
}
presenterOpt := presenter.ParseOption("json")
dirPresenter := presenter.GetDirPresenter(presenterOpt, protocol.Value, catalog)
dirPresenter.Present(&buffer)
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
//func TestDirTextPresenter(t *testing.T) {
// var buffer bytes.Buffer
// protocol := imgbom.NewProtocol("dir://test-fixtures")
// if protocol.Type != imgbom.DirProtocol {
// t.Errorf("unexpected protocol returned: %v != %v", protocol.Type, imgbom.DirProtocol)
// }
//
// catalog, err := imgbom.CatalogDir(protocol.Value, scope.AllLayersScope)
// if err != nil {
// t.Errorf("could not produce catalog: %w", err)
// }
// presenterOpt := presenter.ParseOption("text")
// dirPresenter := presenter.GetDirPresenter(presenterOpt, protocol.Value, catalog)
//
// dirPresenter.Present(&buffer)
// actual := buffer.Bytes()
// if *update {
// testutils.UpdateGoldenFileContents(t, actual)
// }
//
// var expected = testutils.GetGoldenFileContents(t)
//
// if !bytes.Equal(expected, actual) {
// dmp := diffmatchpatch.New()
// diffs := dmp.DiffMain(string(actual), string(expected), true)
// t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
// }
//
//}
//
//func TestDirJsonPresenter(t *testing.T) {
// var buffer bytes.Buffer
// protocol := imgbom.NewProtocol("dir://test-fixtures")
// if protocol.Type != imgbom.DirProtocol {
// t.Errorf("unexpected protocol returned: %v != %v", protocol.Type, imgbom.DirProtocol)
// }
//
// catalog, err := imgbom.CatalogDir(protocol.Value, scope.AllLayersScope)
// if err != nil {
// t.Errorf("could not produce catalog: %w", err)
// }
// presenterOpt := presenter.ParseOption("json")
// dirPresenter := presenter.GetDirPresenter(presenterOpt, protocol.Value, catalog)
//
// dirPresenter.Present(&buffer)
// actual := buffer.Bytes()
// if *update {
// testutils.UpdateGoldenFileContents(t, actual)
// }
//
// var expected = testutils.GetGoldenFileContents(t)
//
// if !bytes.Equal(expected, actual) {
// dmp := diffmatchpatch.New()
// diffs := dmp.DiffMain(string(actual), string(expected), true)
// t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
// }
//
//}

View File

@ -42,6 +42,22 @@ func TestLanguageImage(t *testing.T) {
"apt": "1.8.2",
},
},
{
name: "find java packages",
pkgType: pkg.JavaPkg,
pkgLanguage: pkg.Java,
pkgInfo: map[string]string{
"example-java-app-maven": "0.1.0",
},
},
{
name: "find jenkins plugins",
pkgType: pkg.JenkinsPluginPkg,
pkgLanguage: pkg.Java,
pkgInfo: map[string]string{
"example-jenkins-plugin": "1.0-SNAPSHOT",
},
},
{
name: "find python wheel packages",
pkgType: pkg.WheelPkg,

7
integration/test-fixtures/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# we should strive to not commit blobs to the repo and strive to keep the build process of how blobs are acquired in-repo.
# this blob is generated from imgbom/imgbom/catalogers/java/test-fixtures/java-builds , however, preserving the build process
# twice in the repo seems redundant (even via symlink). Given that the fixture is a few kilobytes in size, the build process is already
# captured, and integration tests should only be testing if jars can be discovered (not necessarily depth in java detection
# functionality), committing it seems like an acceptable exception.
!image-pkg-coverage/java/*.jar
!image-pkg-coverage/java/*.hpi

View File

@ -0,0 +1 @@
See the imgbom/cataloger/java/test-fixtures/java-builds dir to generate test fixtures and copy to here manually.

View File

@ -0,0 +1,45 @@
package file
// Source: https://research.swtch.com/glob.go
func GlobMatch(pattern, name string) bool {
px := 0
nx := 0
nextPx := 0
nextNx := 0
for px < len(pattern) || nx < len(name) {
if px < len(pattern) {
c := pattern[px]
switch c {
default: // ordinary character
if nx < len(name) && name[nx] == c {
px++
nx++
continue
}
case '?': // single-character wildcard
if nx < len(name) {
px++
nx++
continue
}
case '*': // zero-or-more-character wildcard
// Try to match at nx.
// If that doesn't work out,
// restart at nx+1 next.
nextPx = px
nextNx = nx + 1
px++
continue
}
}
// Mismatch. Maybe restart.
if 0 < nextNx && nextNx <= len(name) {
px = nextPx
nx = nextNx
continue
}
return false
}
// Matched all of pattern to all of name. Success.
return true
}

View File

@ -0,0 +1,39 @@
package file
import (
"strings"
"testing"
)
func TestGlobMatch(t *testing.T) {
var tests = []struct {
pattern string
data string
ok bool
}{
{"", "", true},
{"x", "", false},
{"", "x", false},
{"abc", "abc", true},
{"*", "abc", true},
{"*c", "abc", true},
{"*b", "abc", false},
{"a*", "abc", true},
{"b*", "abc", false},
{"a*", "a", true},
{"*a", "a", true},
{"a*b*c*d*e*", "axbxcxdxe", true},
{"a*b*c*d*e*", "axbxcxdxexxx", true},
{"a*b?c*x", "abxbbxdbxebxczzx", true},
{"a*b?c*x", "abxbbxdbxebxczzy", false},
{"a*a*a*a*b", strings.Repeat("a", 100), false},
{"*x", "xxx", true},
{"/home/place/**", "/home/place/a/thing", true},
}
for _, test := range tests {
if GlobMatch(test.pattern, test.data) != test.ok {
t.Errorf("failed glob='%s' data='%s'", test.pattern, test.data)
}
}
}

View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -eux
zip -r "$1" zip-source

View File

@ -0,0 +1 @@
B file...

View File

@ -0,0 +1 @@
A file! nice!

197
internal/file/ziputil.go Normal file
View File

@ -0,0 +1,197 @@
package file
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/anchore/imgbom/internal/log"
)
const (
_ = iota
KB = 1 << (10 * iota)
MB
GB
)
const readLimit = 2 * GB
type extractRequest map[string]struct{}
func newExtractRequest(paths ...string) extractRequest {
results := make(extractRequest)
for _, p := range paths {
results[p] = struct{}{}
}
return results
}
func ExtractFilesFromZip(archivePath string, paths ...string) (map[string]string, error) {
request := newExtractRequest(paths...)
results := make(map[string]string)
zipReader, err := zip.OpenReader(archivePath)
if err != nil {
return nil, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
for _, file := range zipReader.Reader.File {
if _, ok := request[file.Name]; !ok {
// this file path is not of interest
continue
}
zippedFile, err := file.Open()
if err != nil {
return nil, fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
if file.FileInfo().IsDir() {
return nil, fmt.Errorf("unable to extract directories, only files: %s", file.Name)
}
var buffer bytes.Buffer
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(&buffer, io.LimitReader(zippedFile, readLimit))
if numBytes >= readLimit || errors.Is(err, io.EOF) {
return nil, fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return nil, fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
results[file.Name] = buffer.String()
err = zippedFile.Close()
if err != nil {
return nil, fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
}
}
return results, nil
}
func UnzipToDir(archivePath, targetDir string) error {
zipReader, err := zip.OpenReader(archivePath)
if err != nil {
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
for _, file := range zipReader.Reader.File {
// the zip-slip attack protection is still being erroneously detected
// nolint:gosec
expandedFilePath := filepath.Clean(filepath.Join(targetDir, file.Name))
// protect against zip slip attacks (traversing unintended parent paths from maliciously crafted relative-path entries)
if !strings.HasPrefix(expandedFilePath, filepath.Clean(targetDir)+string(os.PathSeparator)) {
return fmt.Errorf("potential zip slip attack: %q", expandedFilePath)
}
err = extractSingleFile(file, expandedFilePath, archivePath)
if err != nil {
return err
}
}
return nil
}
func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) error {
zippedFile, err := file.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
if file.FileInfo().IsDir() {
err = os.MkdirAll(expandedFilePath, file.Mode())
if err != nil {
return fmt.Errorf("unable to create dir=%q from zip=%q: %w", expandedFilePath, archivePath, err)
}
} else {
// Open an output file for writing
outputFile, err := os.OpenFile(
expandedFilePath,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
file.Mode(),
)
if err != nil {
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
}
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(outputFile, io.LimitReader(zippedFile, readLimit))
if numBytes >= readLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
}
err = outputFile.Close()
if err != nil {
return fmt.Errorf("unable to close dest file=%q from zip=%q: %w", outputFile.Name(), archivePath, err)
}
}
err = zippedFile.Close()
if err != nil {
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
}
return nil
}
type ZipManifest map[string]os.FileInfo
func newZipManifest() ZipManifest {
return make(ZipManifest)
}
func (z ZipManifest) Add(entry string, info os.FileInfo) {
z[entry] = info
}
func (z ZipManifest) GlobMatch(pattern string) []string {
results := make([]string, 0)
for entry := range z {
if GlobMatch(pattern, entry) {
results = append(results, entry)
}
}
return results
}
func ZipFileManifest(archivePath string) (ZipManifest, error) {
zipReader, err := zip.OpenReader(archivePath)
manifest := newZipManifest()
if err != nil {
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
for _, file := range zipReader.Reader.File {
manifest.Add(file.Name, file.FileInfo())
}
return manifest, nil
}

View File

@ -0,0 +1,265 @@
package file
import (
"crypto/sha256"
"encoding/json"
"github.com/go-test/deep"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
)
func generateFixture(t *testing.T, archivePath string) {
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("./generate-zip-fixture.sh", archivePath)
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
t.Fatalf("unable to start generate zip fixture script: %+v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture script result: %+v", err)
}
}
}
func equal(r1, r2 io.Reader) (bool, error) {
w1 := sha256.New()
w2 := sha256.New()
n1, err1 := io.Copy(w1, r1)
if err1 != nil {
return false, err1
}
n2, err2 := io.Copy(w2, r2)
if err2 != nil {
return false, err2
}
var b1, b2 [sha256.Size]byte
copy(b1[:], w1.Sum(nil))
copy(b2[:], w2.Sum(nil))
return n1 != n2 || b1 == b2, nil
}
func TestUnzipToDir(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "imgbom-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
contentsDir, err := ioutil.TempDir("", "imgbom-ziputil-contents-TEST-")
if err != nil {
t.Fatalf("unable to create tempdir: %+v", err)
}
defer os.RemoveAll(contentsDir)
t.Logf("content path: %s", contentsDir)
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
// note: zip utility already includes "zip-source" as a parent dir for all contained files
goldenRootDir := filepath.Join(cwd, "test-fixtures")
expectedPaths := 4
observedPaths := 0
err = UnzipToDir(archivePath, contentsDir)
if err != nil {
t.Fatalf("unable to unzip archive: %+v", err)
}
// compare the source dir tree and the unzipped tree
err = filepath.Walk(filepath.Join(contentsDir, "zip-source"),
func(path string, info os.FileInfo, err error) error {
t.Logf("unzipped path: %s", path)
observedPaths++
if err != nil {
t.Fatalf("this should not happen")
return err
}
goldenPath := filepath.Join(goldenRootDir, strings.TrimPrefix(path, contentsDir))
if info.IsDir() {
i, err := os.Stat(goldenPath)
if err != nil {
t.Fatalf("unable to stat golden path: %+v", err)
}
if !i.IsDir() {
t.Fatalf("mismatched file types: %s", goldenPath)
}
return nil
}
// this is a file, not a dir...
testFile, err := os.Open(path)
if err != nil {
t.Fatalf("unable to open test file=%s :%+v", path, err)
}
goldenFile, err := os.Open(goldenPath)
if err != nil {
t.Fatalf("unable to open golden file=%s :%+v", goldenPath, err)
}
same, err := equal(testFile, goldenFile)
if err != nil {
t.Fatalf("could not compare files (%s, %s): %+v", goldenPath, path, err)
}
if !same {
t.Errorf("paths are not the same (%s, %s)", goldenPath, path)
}
return nil
})
if err != nil {
t.Errorf("failed to walk dir: %+v", err)
}
if observedPaths != expectedPaths {
t.Errorf("missed test paths: %d!=%d", observedPaths, expectedPaths)
}
}
func TestExtractFilesFromZipFile(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "imgbom-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
aFilePath := filepath.Join("zip-source", "some-dir", "a-file.txt")
bFilePath := filepath.Join("zip-source", "b-file.txt")
expected := map[string]string{
aFilePath: "A file! nice!",
bFilePath: "B file...",
}
actual, err := ExtractFilesFromZip(archivePath, aFilePath, bFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
diffs := deep.Equal(actual, expected)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("diff: %+v", d)
}
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
func TestZipFileManifest(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "imgbom-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
expected := []string{
filepath.Join("zip-source") + string(os.PathSeparator),
filepath.Join("zip-source", "some-dir") + string(os.PathSeparator),
filepath.Join("zip-source", "some-dir", "a-file.txt"),
filepath.Join("zip-source", "b-file.txt"),
}
actual, err := ZipFileManifest(archivePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expected) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expected))
}
for _, e := range expected {
_, ok := actual[e]
if !ok {
t.Errorf("missing path: %s", e)
}
}
if t.Failed() {
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}

View File

@ -1,28 +1,28 @@
package internal
type Set map[string]struct{}
type StringSet map[string]struct{}
func NewStringSet() Set {
return make(Set)
func NewStringSet() StringSet {
return make(StringSet)
}
func NewStringSetFromSlice(start []string) Set {
ret := make(Set)
func NewStringSetFromSlice(start []string) StringSet {
ret := make(StringSet)
for _, s := range start {
ret.Add(s)
}
return ret
}
func (s Set) Add(i string) {
func (s StringSet) Add(i string) {
s[i] = struct{}{}
}
func (s Set) Remove(i string) {
func (s StringSet) Remove(i string) {
delete(s, i)
}
func (s Set) Contains(i string) bool {
func (s StringSet) Contains(i string) bool {
_, ok := s[i]
return ok
}

View File

@ -31,6 +31,7 @@ func setupScreen(output *os.File) *frame.Frame {
return fr
}
// nolint:funlen,gocognit
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) int {
output := os.Stderr