add java cataloger

This commit is contained in:
Alex Goodman 2020-07-07 18:04:27 -04:00
parent e040fc8930
commit e55db9247e
No known key found for this signature in database
GPG Key ID: 86E2870463D5E890
95 changed files with 2120 additions and 45 deletions

View File

@ -74,24 +74,37 @@ jobs:
chmod 755 ${HOME}/.local/bin/docker chmod 755 ${HOME}/.local/bin/docker
- run: - run:
name: run unit tests name: build cache key for java test-fixture blobs
command: make unit command: |
cd imgbom/cataloger/java/test-fixtures &&\
- run: make packages.fingerprint
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
- restore_cache: - restore_cache:
keys: keys:
- integration-test-tar-cache-{{ checksum "integration/test-fixtures/tar-cache.key" }} - unit-test-java-cache-{{ checksum "imgbom/cataloger/java/test-fixtures/packages.fingerprint" }}
- run: - run:
name: run integration tests name: run unit tests
command: | command: make unit
docker version
make integration
- save_cache: - 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/packages.fingerprint" }}
paths:
- "imgbom/cataloger/java/test-fixtures/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: paths:
- "integration/test-fixtures/tar-cache" - "integration/test-fixtures/tar-cache"

7
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import (
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"}) 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) pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)

View File

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

View File

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

View File

@ -39,7 +39,7 @@ func (r *testResolver) FilesByGlob(patterns ...string) ([]file.Reference, error)
return []file.Reference{ref}, nil 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) contents, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -6,5 +6,5 @@ import (
"github.com/anchore/imgbom/imgbom/pkg" "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 // 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(io.Reader) ([]pkg.Package, error) type ParserFn func(string, io.Reader) ([]pkg.Package, error)

View File

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

View File

@ -12,7 +12,7 @@ import (
var errEndOfPackages = fmt.Errorf("no more packages to read") 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) buffedReader := bufio.NewReader(reader)
var packages = make([]pkg.Package, 0) 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 { if err != nil {
t.Fatal("Unable to read file contents: ", err) t.Fatal("Unable to read file contents: ", err)
} }

View File

@ -0,0 +1,90 @@
package java
import (
"path/filepath"
"regexp"
"strings"
"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
}
func newJavaArchiveFilename(raw string) archiveFilename {
return archiveFilename{
raw: raw,
}
}
func (a archiveFilename) normalize() string {
// trim the file extension and remove any path prefixes
return strings.TrimSuffix(filepath.Base(a.raw), "."+a.extension())
}
func (a archiveFilename) fields() []map[string]string {
name := a.normalize()
matches := versionPattern.FindAllStringSubmatch(name, -1)
items := 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]
}
}
items = append(items, item)
}
return items
}
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":
return pkg.JarPkg
case "war":
return pkg.WarPkg
case "ear":
return pkg.EarPkg
case "jpi":
return pkg.JpiPkg
case "hpi":
return pkg.HpiPkg
default:
return pkg.UnknownPkg
}
}
func (a archiveFilename) version() string {
fields := a.fields()
// there should be only one version, if there is more or less then something is wrong
if len(fields) != 1 {
return ""
}
return fields[0]["version"]
}
func (a archiveFilename) name() string {
fields := a.fields()
// there should be only one name, if there is more or less then something is wrong
if len(fields) != 1 {
return ""
}
return 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.JarPkg,
},
{
filename: "pkg-extra-field-maven-4.3.2.war",
version: "4.3.2",
extension: "war",
name: "pkg-extra-field-maven",
ty: pkg.WarPkg,
},
{
filename: "pkg-extra-field-maven-4.3.2-rc1.ear",
version: "4.3.2-rc1",
extension: "ear",
name: "pkg-extra-field-maven",
ty: pkg.EarPkg,
},
{
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.JpiPkg,
},
{
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.HpiPkg,
},
}
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": parseJar,
"*.war": parseWar,
"*.ear": parseEar,
"*.jpi": parseJpi,
"*.hpi": parseHpi,
}
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,101 @@
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)
for scanner.Scan() {
line := scanner.Text()
// ignore empty lines
if strings.TrimSpace(line) == "" {
continue
}
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
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("unable read java manifest: %w", err)
}
if err := mapstructure.Decode(manifestMap, &manifest); err != nil {
return nil, fmt.Errorf("unable 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: %w", 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()
}
var version string
switch {
case manifest.ImplVersion != "":
version = manifest.ImplVersion
case filenameObj.version() != "":
version = filenameObj.version()
case manifest.SpecVersion != "":
version = manifest.SpecVersion
}
return &pkg.Package{
Name: name,
Version: version,
Language: pkg.Java,
Metadata: pkg.JavaMetadata{
Manifest: manifest,
},
}, nil
}

View File

@ -0,0 +1,77 @@
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",
},
},
},
}
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,136 @@
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 parseJar(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
return parseJavaArchive(virtualPath, reader)
}
func parseWar(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
return parseJavaArchive(virtualPath, reader)
}
func parseEar(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
return parseJavaArchive(virtualPath, reader)
}
func parseJpi(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
return parseJavaArchive(virtualPath, reader)
}
func parseHpi(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
return parseJavaArchive(virtualPath, reader)
}
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,202 @@
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 generateFixture(t *testing.T, fixturePath string) {
if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
// fixture already exists...
return
}
makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/")
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")
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
}{
{
fixture: "test-fixtures/packages/example-app-gradle-0.1.0.jar",
expected: map[string]pkg.Package{
"example-app-gradle": {
Name: "example-app-gradle",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JarPkg,
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
},
},
},
},
},
{
fixture: "test-fixtures/packages/example-app-maven-0.1.0.jar",
expected: map[string]pkg.Package{
"example-app-maven": {
Name: "example-app-maven",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JarPkg,
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-app-maven/pom.properties",
GroupID: "org.anchore",
ArtifactID: "example-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) {
generateFixture(t, test.fixture)
fixture, err := os.Open(test.fixture)
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseJar(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse egg-info: %+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-app-") {
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
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 read pom.properties: %w", err)
}
if err := mapstructure.Decode(propMap, &props); err != nil {
return nil, fmt.Errorf("unable parse pom.propertoes: %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-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-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,3 @@
/packages/*
# maven when running in a volume may spit out directories like this
\?/

View File

@ -0,0 +1,35 @@
all: packages/example-app-maven-0.1.0.jar packages/example-app-gradle-0.1.0.jar packages/example-jenkins-plugin.hpi
clean: clean-examples
rm -f packages/*
clean-examples: clean-gradle clean-maven clean-jenkins
.PHONY: maven gradle clean clean-gradle clean-maven clean-jenkins clean-examples
# Maven...
packages/example-app-maven-0.1.0.jar:
./run-example-app-maven.sh
clean-maven:
rm -rf example-app/target example-app/dependency-reduced-pom.xml
# Gradle...
packages/example-app-gradle-0.1.0.jar:
./run-example-app-gradle.sh
clean-gradle:
rm -rf example-app/.gradle example-app/build
# Jenkins plugin
packages/example-jenkins-plugin.hpi, packages/example-jenkins-plugin.jar:
./run-example-jenkins-plugin-maven.sh
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
packages.fingerprint: clean-examples
@mkdir -p packages
find example-* -type f -exec sha256sum {} \; > packages.fingerprint
sha256sum packages.fingerprint

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-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-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=Name
French=Franz\u00F6sisch
FrenchDescr=Markieren f\u00FCr Begr\u00FC\u00DFung auf franz\u00F6sisch

View File

@ -0,0 +1,3 @@
Name=Nombre
French=Franc\u00E9s
FrenchDescr=Compruebe si debemos decir hola en franc\u00E9s

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,3 @@
Name=Nome
French=Francese
FrenchDescr=Mostra il messagio in francese

View File

@ -0,0 +1,3 @@
Name=Nome
French=Franc\u00EAs
FrenchDescr=Marque se devemos falar ol\u00E1 em franc\u00EAs

View File

@ -0,0 +1,3 @@
Name=Namn
French=Franska
FrenchDescr=S\u00E4tt om vi ska s\u00E4ga hej p\u00E5 Franska

View File

@ -0,0 +1,3 @@
Name=\u0130sim
French=Frans\u0131zca
FrenchDescr=Frans\u0131zca olarak merhaba demeliyim diye sor

View File

@ -0,0 +1,6 @@
# 如果您希望在 Jenkins 组织当中托管您的插件,那我们推荐您将本地化文件提交到 https://github.com/jenkinsci/localization-zh-cn-plugin 中。
# 如果您希望私有维护插件,可以参考本例子的方式放置中文的本地化文件。
Name=\u540d\u5b57
French=\u6cd5\u8bed
FrenchDescr=\u68c0\u67e5\u6211\u4eec\u662f\u5426\u5e94\u8be5\u7528\u6cd5\u8bed\u6253\u4e2a\u62db\u547c

View File

@ -0,0 +1,5 @@
<!--如果您希望在 Jenkins 组织当中托管您的插件,那我们推荐您将本地化文件提交到 https://github.com/jenkinsci/localization-zh-cn-plugin 中。-->
<!--如果您希望私有维护插件,可以参考本例子的方式放置中文的本地化文件。-->
<div>
你的名字。
</div>

View File

@ -0,0 +1,3 @@
<div>
Ob die Begrüßung auf französisch angegeben werden soll.
</div>

View File

@ -0,0 +1,5 @@
<!--如果您希望在 Jenkins 组织当中托管您的插件,那我们推荐您将本地化文件提交到 https://github.com/jenkinsci/localization-zh-cn-plugin 中。-->
<!--如果您希望私有维护插件,可以参考本例子的方式放置中文的本地化文件。-->
<div>
使用法语
</div>

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=Bitte geben Sie einen Namen an
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=Der Name ist zu kurz.
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=Sind Sie wirklich franz\u00F6sisch?
HelloWorldBuilder.DescriptorImpl.DisplayName=\u201EHallo Welt\u201C sagen

View File

@ -0,0 +1,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=Establecer un nombre
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=\u00BFNo es el nombre demasiado corto?
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=\u00BFEs usted realmente franc\u00E9s?
HelloWorldBuilder.DescriptorImpl.DisplayName=Di hola mundo

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,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=Nome \u00E8 richiesto
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=Nome dev'essere al meno 4 charatteri
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=Vuole mostrare il messagio in francese?
HelloWorldBuilder.DescriptorImpl.DisplayName=Nome di mostrare

View File

@ -0,0 +1,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=Insira um nome
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=Este nome n\u00E3o \u00E9 muito curto?
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=Voc\u00EA \u00E9 realmente franc\u00EAs?
HelloWorldBuilder.DescriptorImpl.DisplayName=Diz ol\u00E1 mundo

View File

@ -0,0 +1,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=Ange ett namn
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=\u00C4r inte namnet lite f\u00F6r kort?
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=\u00C4r du egentligen Fransk?
HelloWorldBuilder.DescriptorImpl.DisplayName=S\u00E4g Hej V\u00E4rlden

View File

@ -0,0 +1,5 @@
HelloWorldBuilder.DescriptorImpl.errors.missingName=L\u00FCtfen bir isim girin
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=\u0130sminiz \u00E7ok k\u0131sa de\u011Fil mi?
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=Ger\u00E7ekten Frans\u0131z m\u0131s\u0131n\u0131z?
HelloWorldBuilder.DescriptorImpl.DisplayName=Merhaba d\u00FCnya de

View File

@ -0,0 +1,8 @@
# 如果您希望在 Jenkins 组织当中托管您的插件,那我们推荐您将本地化文件提交到 https://github.com/jenkinsci/localization-zh-cn-plugin 中。
# 如果您希望私有维护插件,可以参考本例子的方式放置中文的本地化文件。
HelloWorldBuilder.DescriptorImpl.errors.missingName=\u8bf7\u586b\u5199\u540d\u5b57
HelloWorldBuilder.DescriptorImpl.warnings.tooShort=\u540d\u5b57\u662f\u4e0d\u662f\u592a\u77ed\u4e86\uff1f
HelloWorldBuilder.DescriptorImpl.warnings.reallyFrench=\u4f60\u771f\u7684\u662f\u6cd5\u56fd\u4eba\u5417\uff1f
HelloWorldBuilder.DescriptorImpl.DisplayName=\u8bf4\uff1a\u4f60\u597d\uff0c\u4e16\u754c

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-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-app-maven
version=0.1.0

View File

@ -0,0 +1,18 @@
#!/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)
CTRID=$(docker create -u "$(id -u):$(id -g)" -v /example-app -w /example-app gradle:jdk gradle build)
function cleanup() {
docker rm "${CTRID}"
}
trap cleanup EXIT
set +e
docker cp "$(pwd)/example-app" "${CTRID}:/"
docker start -a "${CTRID}"
mkdir -p packages
docker cp "${CTRID}:/example-app/build/libs/example-app-gradle-0.1.0.jar" packages/

View File

@ -0,0 +1,18 @@
#!/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)
CTRID=$(docker create -u "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/.m2 -v /example-app -w /example-app maven:openjdk mvn -Duser.home=/tmp -DskipTests package)
function cleanup() {
docker rm "${CTRID}"
}
trap cleanup EXIT
set +e
docker cp "$(pwd)/example-app" "${CTRID}:/"
docker start -a "${CTRID}"
mkdir -p packages
docker cp "${CTRID}:/example-app/target/example-app-maven-0.1.0.jar" packages/

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)
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 packages
docker cp "${CTRID}:/example-jenkins-plugin/target/example-jenkins-plugin.hpi" packages/
docker cp "${CTRID}:/example-jenkins-plugin/target/example-jenkins-plugin.jar" packages/

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import (
rpmdb "github.com/knqyf263/go-rpmdb/pkg" 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") f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create temp rpmdb file: %w", err) 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) t.Fatalf("failed to open fixture: %+v", err)
} }
actual, err := parseRpmDB(fixture) actual, err := parseRpmDB(fixture.Name(), fixture)
if err != nil { if err != nil {
t.Fatalf("failed to parse rpmdb: %+v", err) t.Fatalf("failed to parse rpmdb: %+v", err)
} }

View File

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

View File

@ -14,3 +14,30 @@ type RpmMetadata struct {
Arch string `mapstructure:"Arch"` Arch string `mapstructure:"Arch"`
Release string `mapstructure:"Release"` 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 type ID int64
// TODO: add field to trace which cataloger detected this // 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 { type Package struct {
id ID id ID // this is set when a package is added to the catalog
Name string Name string
Version string Version string
FoundBy string FoundBy string

View File

@ -9,6 +9,11 @@ const (
//PacmanPkg //PacmanPkg
RpmPkg RpmPkg
WheelPkg WheelPkg
JarPkg
WarPkg
EarPkg
JpiPkg
HpiPkg
) )
type Type uint type Type uint
@ -22,6 +27,11 @@ var typeStr = []string{
//"pacman", //"pacman",
"rpm", "rpm",
"wheel", "wheel",
"jar",
"war",
"ear",
"jpi",
"hpi",
} }
var AllPkgs = []Type{ var AllPkgs = []Type{
@ -32,6 +42,11 @@ var AllPkgs = []Type{
//PacmanPkg, //PacmanPkg,
RpmPkg, RpmPkg,
WheelPkg, WheelPkg,
JarPkg,
WarPkg,
EarPkg,
JpiPkg,
HpiPkg,
} }
func (t Type) String() string { func (t Type) String() string {

View File

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

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!

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

@ -0,0 +1,190 @@
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
}
// nolint:funlen
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)
}
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 package internal
type Set map[string]struct{} type StringSet map[string]struct{}
func NewStringSet() Set { func NewStringSet() StringSet {
return make(Set) return make(StringSet)
} }
func NewStringSetFromSlice(start []string) Set { func NewStringSetFromSlice(start []string) StringSet {
ret := make(Set) ret := make(StringSet)
for _, s := range start { for _, s := range start {
ret.Add(s) ret.Add(s)
} }
return ret return ret
} }
func (s Set) Add(i string) { func (s StringSet) Add(i string) {
s[i] = struct{}{} s[i] = struct{}{}
} }
func (s Set) Remove(i string) { func (s StringSet) Remove(i string) {
delete(s, i) delete(s, i)
} }
func (s Set) Contains(i string) bool { func (s StringSet) Contains(i string) bool {
_, ok := s[i] _, ok := s[i]
return ok return ok
} }

View File

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