diff --git a/imgbom/analyzer/bundler/analyzer.go b/imgbom/analyzer/bundler/analyzer.go
index 7c4dceec1..6505fb84d 100644
--- a/imgbom/analyzer/bundler/analyzer.go
+++ b/imgbom/analyzer/bundler/analyzer.go
@@ -13,7 +13,7 @@ type Analyzer struct {
func NewAnalyzer() *Analyzer {
globParserDispatch := map[string]common.ParserFn{
- "*/Gemfile.lock": ParseGemfileLockEntries,
+ "*/Gemfile.lock": parseGemfileLockEntries,
}
return &Analyzer{
diff --git a/imgbom/analyzer/bundler/parse_gemfile_lock.go b/imgbom/analyzer/bundler/parse_gemfile_lock.go
index 297d72959..9921c4261 100644
--- a/imgbom/analyzer/bundler/parse_gemfile_lock.go
+++ b/imgbom/analyzer/bundler/parse_gemfile_lock.go
@@ -11,7 +11,7 @@ import (
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
-func ParseGemfileLockEntries(reader io.Reader) ([]pkg.Package, error) {
+func parseGemfileLockEntries(reader io.Reader) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
diff --git a/imgbom/analyzer/bundler/parse_gemfile_lock_test.go b/imgbom/analyzer/bundler/parse_gemfile_lock_test.go
index cf360f47d..5eb10b73c 100644
--- a/imgbom/analyzer/bundler/parse_gemfile_lock_test.go
+++ b/imgbom/analyzer/bundler/parse_gemfile_lock_test.go
@@ -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)
if err != nil {
t.Fatalf("failed to parse gemfile lock: %+v", err)
}
diff --git a/imgbom/analyzer/common/generic_analyzer.go b/imgbom/analyzer/common/generic_analyzer.go
index b9a61a385..c7d666ed0 100644
--- a/imgbom/analyzer/common/generic_analyzer.go
+++ b/imgbom/analyzer/common/generic_analyzer.go
@@ -9,6 +9,8 @@ import (
"github.com/anchore/stereoscope/pkg/tree"
)
+// TODO: put under test...
+
type GenericAnalyzer struct {
globParserDispatch map[string]ParserFn
pathParserDispatch map[string]ParserFn
diff --git a/imgbom/analyzer/controller.go b/imgbom/analyzer/controller.go
index 9c445c34d..7defd110e 100644
--- a/imgbom/analyzer/controller.go
+++ b/imgbom/analyzer/controller.go
@@ -3,6 +3,7 @@ package analyzer
import (
"github.com/anchore/imgbom/imgbom/analyzer/bundler"
"github.com/anchore/imgbom/imgbom/analyzer/dpkg"
+ "github.com/anchore/imgbom/imgbom/analyzer/python"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/imgbom/internal/log"
@@ -30,6 +31,7 @@ func newController() controller {
}
ctrlr.add(dpkg.NewAnalyzer())
ctrlr.add(bundler.NewAnalyzer())
+ ctrlr.add(python.NewAnalyzer())
return ctrlr
}
diff --git a/imgbom/analyzer/dpkg/analyzer.go b/imgbom/analyzer/dpkg/analyzer.go
index 7af4cc810..af6a91179 100644
--- a/imgbom/analyzer/dpkg/analyzer.go
+++ b/imgbom/analyzer/dpkg/analyzer.go
@@ -13,7 +13,7 @@ type Analyzer struct {
func NewAnalyzer() *Analyzer {
pathParserDispatch := map[string]common.ParserFn{
- "/var/lib/dpkg/status": ParseDpkgStatus,
+ "/var/lib/dpkg/status": parseDpkgStatus,
}
return &Analyzer{
diff --git a/imgbom/analyzer/dpkg/parse_dpkg_status.go b/imgbom/analyzer/dpkg/parse_dpkg_status.go
index cee6e5b33..4414ef56b 100644
--- a/imgbom/analyzer/dpkg/parse_dpkg_status.go
+++ b/imgbom/analyzer/dpkg/parse_dpkg_status.go
@@ -12,7 +12,7 @@ import (
var errEndOfPackages = fmt.Errorf("no more packages to read")
-func ParseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
+func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
buffedReader := bufio.NewReader(reader)
var packages = make([]pkg.Package, 0)
diff --git a/imgbom/analyzer/dpkg/parse_dpkg_status_test.go b/imgbom/analyzer/dpkg/parse_dpkg_status_test.go
index 339ac980b..6a2abc542 100644
--- a/imgbom/analyzer/dpkg/parse_dpkg_status_test.go
+++ b/imgbom/analyzer/dpkg/parse_dpkg_status_test.go
@@ -90,7 +90,7 @@ func TestMultiplePackages(t *testing.T) {
}
}()
- pkgs, err := ParseDpkgStatus(file)
+ pkgs, err := parseDpkgStatus(file)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
diff --git a/imgbom/analyzer/python/analyzer.go b/imgbom/analyzer/python/analyzer.go
new file mode 100644
index 000000000..5f4323fb4
--- /dev/null
+++ b/imgbom/analyzer/python/analyzer.go
@@ -0,0 +1,35 @@
+package python
+
+import (
+ "github.com/anchore/imgbom/imgbom/analyzer/common"
+ "github.com/anchore/imgbom/imgbom/pkg"
+ "github.com/anchore/stereoscope/pkg/file"
+ "github.com/anchore/stereoscope/pkg/tree"
+)
+
+type Analyzer struct {
+ analyzer common.GenericAnalyzer
+}
+
+func NewAnalyzer() *Analyzer {
+ globParserDispatch := map[string]common.ParserFn{
+ "*egg-info/PKG-INFO": parseEggMetadata,
+ "*dist-info/METADATA": parseWheelMetadata,
+ }
+
+ return &Analyzer{
+ analyzer: common.NewGenericAnalyzer(nil, globParserDispatch),
+ }
+}
+
+func (a *Analyzer) Name() string {
+ return "python-analyzer"
+}
+
+func (a *Analyzer) SelectFiles(trees []tree.FileTreeReader) []file.Reference {
+ return a.analyzer.SelectFiles(trees)
+}
+
+func (a *Analyzer) Analyze(contents map[file.Reference]string) ([]pkg.Package, error) {
+ return a.analyzer.Analyze(contents, a.Name())
+}
diff --git a/imgbom/analyzer/python/parse_wheel_egg.go b/imgbom/analyzer/python/parse_wheel_egg.go
new file mode 100644
index 000000000..b2f311d64
--- /dev/null
+++ b/imgbom/analyzer/python/parse_wheel_egg.go
@@ -0,0 +1,90 @@
+package python
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/anchore/imgbom/imgbom/pkg"
+)
+
+func parseWheelMetadata(reader io.Reader) ([]pkg.Package, error) {
+ packages, err := parseWheelOrEggMetadata(reader)
+ for idx := range packages {
+ packages[idx].Type = pkg.WheelPkg
+ }
+ return packages, err
+}
+
+func parseEggMetadata(reader io.Reader) ([]pkg.Package, error) {
+ packages, err := parseWheelOrEggMetadata(reader)
+ for idx := range packages {
+ packages[idx].Type = pkg.EggPkg
+ }
+ return packages, err
+}
+
+func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
+ fields := make(map[string]string)
+ var key string
+
+ scanner := bufio.NewScanner(reader)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ line = strings.TrimRight(line, "\n")
+
+ // empty line indicates end of entry
+ if len(line) == 0 {
+ // if the entry has not started, keep parsing lines
+ if len(fields) == 0 {
+ continue
+ }
+ break
+ }
+
+ switch {
+ case strings.HasPrefix(line, " "):
+ // a field-body continuation
+ if len(key) == 0 {
+ return nil, fmt.Errorf("no match for continuation: line: '%s'", line)
+ }
+
+ val, ok := fields[key]
+ if !ok {
+ return nil, fmt.Errorf("no previous key exists, expecting: %s", key)
+ }
+ // concatenate onto previous value
+ val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line))
+ fields[key] = val
+ default:
+ // parse a new key (note, duplicate keys are overridden)
+ if i := strings.Index(line, ":"); i > 0 {
+ key = strings.TrimSpace(line[0:i])
+ val := strings.TrimSpace(line[i+1:])
+
+ fields[key] = val
+ } else {
+ return nil, fmt.Errorf("cannot parse field from line: '%s'", line)
+ }
+ }
+
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("failed to parse python wheel/egg: %w", err)
+ }
+
+ p := pkg.Package{
+ Name: fields["Name"],
+ Version: fields["Version"],
+ Language: pkg.Python,
+ }
+
+ if license, ok := fields["License"]; ok && license != "" {
+ p.Licenses = []string{license}
+ }
+
+ return []pkg.Package{p}, nil
+}
diff --git a/imgbom/analyzer/python/parse_wheel_egg_test.go b/imgbom/analyzer/python/parse_wheel_egg_test.go
new file mode 100644
index 000000000..20afa6725
--- /dev/null
+++ b/imgbom/analyzer/python/parse_wheel_egg_test.go
@@ -0,0 +1,91 @@
+package python
+
+import (
+ "os"
+ "testing"
+
+ "github.com/anchore/imgbom/imgbom/pkg"
+)
+
+func assertPkgsEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg.Package) {
+ t.Helper()
+ if len(actual) != 1 {
+ for _, a := range actual {
+ t.Log(" ", a)
+ }
+ t.Fatalf("unexpected package count: %d!=%d", len(actual), 1)
+ }
+
+ for _, a := range actual {
+ expectedPkg, ok := expected[a.Name]
+ if !ok {
+ t.Errorf("unexpected package found: '%s'", a.Name)
+ }
+
+ if expectedPkg.Version != a.Version {
+ t.Errorf("unexpected package version: '%s'", a.Version)
+ }
+
+ if a.Language != expectedPkg.Language {
+ t.Errorf("bad language: '%+v'", a.Language)
+ }
+
+ if a.Type != expectedPkg.Type {
+ t.Errorf("bad package type: %+v", a.Type)
+ }
+
+ if len(a.Licenses) < 1 {
+ t.Errorf("bad package licenses count: '%+v'", a.Licenses)
+ } else if a.Licenses[0] != expectedPkg.Licenses[0] {
+ t.Errorf("bad package licenses: '%+v'", a.Licenses)
+ }
+
+ }
+}
+func TestParseEggMetadata(t *testing.T) {
+ expected := map[string]pkg.Package{
+ "requests": {
+ Name: "requests",
+ Version: "2.22.0",
+ Language: pkg.Python,
+ Type: pkg.EggPkg,
+ Licenses: []string{"Apache 2.0"},
+ },
+ }
+ fixture, err := os.Open("test-fixtures/egg-info/PKG-INFO")
+ if err != nil {
+ t.Fatalf("failed to open fixture: %+v", err)
+ }
+
+ actual, err := parseEggMetadata(fixture)
+ if err != nil {
+ t.Fatalf("failed to parse egg-info: %+v", err)
+ }
+
+ assertPkgsEqual(t, actual, expected)
+
+}
+
+func TestParseWheelMetadata(t *testing.T) {
+ expected := map[string]pkg.Package{
+ "Pygments": {
+ Name: "Pygments",
+ Version: "2.6.1",
+ Language: pkg.Python,
+ Type: pkg.WheelPkg,
+ Licenses: []string{"BSD License"},
+ },
+ }
+ fixture, err := os.Open("test-fixtures/dist-info/METADATA")
+ if err != nil {
+ t.Fatalf("failed to open fixture: %+v", err)
+ }
+
+ actual, err := parseWheelMetadata(fixture)
+ if err != nil {
+ t.Fatalf("failed to parse dist-info: %+v", err)
+ }
+
+ assertPkgsEqual(t, actual, expected)
+
+}
diff --git a/imgbom/analyzer/python/test-fixtures/dist-info/METADATA b/imgbom/analyzer/python/test-fixtures/dist-info/METADATA
new file mode 100644
index 000000000..924780dfd
--- /dev/null
+++ b/imgbom/analyzer/python/test-fixtures/dist-info/METADATA
@@ -0,0 +1,47 @@
+Metadata-Version: 2.1
+Name: Pygments
+Version: 2.6.1
+Summary: Pygments is a syntax highlighting package written in Python.
+Home-page: https://pygments.org/
+Author: Georg Brandl
+Author-email: georg@python.org
+License: BSD License
+Keywords: syntax highlighting
+Platform: any
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: End Users/Desktop
+Classifier: Intended Audience :: System Administrators
+Classifier: Development Status :: 6 - Mature
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Text Processing :: Filters
+Classifier: Topic :: Utilities
+Requires-Python: >=3.5
+
+
+Pygments
+~~~~~~~~
+
+Pygments is a syntax highlighting package written in Python.
+
+It is a generic syntax highlighter suitable for use in code hosting, forums,
+wikis or other applications that need to prettify source code. Highlights
+are:
+
+* a wide range of over 500 languages and other text formats is supported
+* special attention is paid to details, increasing quality by a fair amount
+* support for new languages and formats are added easily
+* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
+* it is usable as a command-line tool and as a library
+
+:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
+:license: BSD, see LICENSE for details.
+
diff --git a/imgbom/analyzer/python/test-fixtures/egg-info/PKG-INFO b/imgbom/analyzer/python/test-fixtures/egg-info/PKG-INFO
new file mode 100644
index 000000000..a73770668
--- /dev/null
+++ b/imgbom/analyzer/python/test-fixtures/egg-info/PKG-INFO
@@ -0,0 +1,134 @@
+Metadata-Version: 2.1
+Name: requests
+Version: 2.22.0
+Summary: Python HTTP for Humans.
+Home-page: http://python-requests.org
+Author: Kenneth Reitz
+Author-email: me@kennethreitz.org
+License: Apache 2.0
+Description: Requests: HTTP for Humans™
+ ==========================
+
+ [![image](https://img.shields.io/pypi/v/requests.svg)](https://pypi.org/project/requests/)
+ [![image](https://img.shields.io/pypi/l/requests.svg)](https://pypi.org/project/requests/)
+ [![image](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests/)
+ [![codecov.io](https://codecov.io/github/requests/requests/coverage.svg?branch=master)](https://codecov.io/github/requests/requests)
+ [![image](https://img.shields.io/github/contributors/requests/requests.svg)](https://github.com/requests/requests/graphs/contributors)
+ [![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz)
+
+ Requests is the only *Non-GMO* HTTP library for Python, safe for human
+ consumption.
+
+ ![image](https://farm5.staticflickr.com/4317/35198386374_1939af3de6_k_d.jpg)
+
+ Behold, the power of Requests:
+
+ ``` {.sourceCode .python}
+ >>> import requests
+ >>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
+ >>> r.status_code
+ 200
+ >>> r.headers['content-type']
+ 'application/json; charset=utf8'
+ >>> r.encoding
+ 'utf-8'
+ >>> r.text
+ u'{"type":"User"...'
+ >>> r.json()
+ {u'disk_usage': 368627, u'private_gists': 484, ...}
+ ```
+
+ See [the similar code, sans Requests](https://gist.github.com/973705).
+
+ [![image](https://raw.githubusercontent.com/requests/requests/master/docs/_static/requests-logo-small.png)](http://docs.python-requests.org/)
+
+ Requests allows you to send *organic, grass-fed* HTTP/1.1 requests,
+ without the need for manual labor. There's no need to manually add query
+ strings to your URLs, or to form-encode your POST data. Keep-alive and
+ HTTP connection pooling are 100% automatic, thanks to
+ [urllib3](https://github.com/shazow/urllib3).
+
+ Besides, all the cool kids are doing it. Requests is one of the most
+ downloaded Python packages of all time, pulling in over 11,000,000
+ downloads every month. You don't want to be left out!
+
+ Feature Support
+ ---------------
+
+ Requests is ready for today's web.
+
+ - International Domains and URLs
+ - Keep-Alive & Connection Pooling
+ - Sessions with Cookie Persistence
+ - Browser-style SSL Verification
+ - Basic/Digest Authentication
+ - Elegant Key/Value Cookies
+ - Automatic Decompression
+ - Automatic Content Decoding
+ - Unicode Response Bodies
+ - Multipart File Uploads
+ - HTTP(S) Proxy Support
+ - Connection Timeouts
+ - Streaming Downloads
+ - `.netrc` Support
+ - Chunked Requests
+
+ Requests officially supports Python 2.7 & 3.4–3.7, and runs great on
+ PyPy.
+
+ Installation
+ ------------
+
+ To install Requests, simply use [pipenv](http://pipenv.org/) (or pip, of
+ course):
+
+ ``` {.sourceCode .bash}
+ $ pipenv install requests
+ ✨🍰✨
+ ```
+
+ Satisfaction guaranteed.
+
+ Documentation
+ -------------
+
+ Fantastic documentation is available at
+ , for a limited time only.
+
+ How to Contribute
+ -----------------
+
+ 1. Become more familiar with the project by reading our [Contributor's Guide](http://docs.python-requests.org/en/latest/dev/contributing/) and our [development philosophy](http://docs.python-requests.org/en/latest/dev/philosophy/).
+ 2. Check for open issues or open a fresh issue to start a discussion
+ around a feature idea or a bug. There is a [Contributor
+ Friendly](https://github.com/requests/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open)
+ tag for issues that should be ideal for people who are not very
+ familiar with the codebase yet.
+ 3. Fork [the repository](https://github.com/requests/requests) on
+ GitHub to start making your changes to the **master** branch (or
+ branch off of it).
+ 4. Write a test which shows that the bug was fixed or that the feature
+ works as expected.
+ 5. Send a pull request and bug the maintainer until it gets merged and
+ published. :) Make sure to add yourself to
+ [AUTHORS](https://github.com/requests/requests/blob/master/AUTHORS.rst).
+
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Natural Language :: English
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
+Description-Content-Type: text/markdown
+Provides-Extra: security
+Provides-Extra: socks
\ No newline at end of file
diff --git a/imgbom/pkg/type.go b/imgbom/pkg/type.go
index 4573ce92a..ce35aa12c 100644
--- a/imgbom/pkg/type.go
+++ b/imgbom/pkg/type.go
@@ -5,8 +5,10 @@ const (
ApkPkg
BundlerPkg
DebPkg
+ EggPkg
PacmanPkg
RpmPkg
+ WheelPkg
)
type Type uint
@@ -14,18 +16,22 @@ type Type uint
var typeStr = []string{
"UnknownPackage",
"apk",
- "bundler",
+ "bundle",
"deb",
+ "egg",
"pacman",
"rpm",
+ "wheel",
}
var AllPkgs = []Type{
ApkPkg,
BundlerPkg,
DebPkg,
+ EggPkg,
PacmanPkg,
RpmPkg,
+ WheelPkg,
}
func (t Type) String() string {
diff --git a/integration/fixture_image_bundler_test.go b/integration/fixture_image_language_pkgs_test.go
similarity index 86%
rename from integration/fixture_image_bundler_test.go
rename to integration/fixture_image_language_pkgs_test.go
index 5cc0ca354..15248215b 100644
--- a/integration/fixture_image_bundler_test.go
+++ b/integration/fixture_image_language_pkgs_test.go
@@ -11,7 +11,7 @@ import (
"github.com/anchore/imgbom/imgbom/scope"
)
-func TestBundlerImage(t *testing.T) {
+func TestLanguageImage(t *testing.T) {
img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-language-pkgs")
defer cleanup()
@@ -26,6 +26,22 @@ func TestBundlerImage(t *testing.T) {
pkgLanguage pkg.Language
pkgInfo map[string]string
}{
+ {
+ name: "find python wheel packages",
+ pkgType: pkg.WheelPkg,
+ pkgLanguage: pkg.Python,
+ pkgInfo: map[string]string{
+ "Pygments": "2.6.1",
+ },
+ },
+ {
+ name: "find python egg packages",
+ pkgType: pkg.EggPkg,
+ pkgLanguage: pkg.Python,
+ pkgInfo: map[string]string{
+ "requests": "2.22.0",
+ },
+ },
{
name: "find bundler packages",
pkgType: pkg.BundlerPkg,
@@ -88,12 +104,7 @@ func TestBundlerImage(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
- if catalog.PackageCount() != len(c.pkgInfo) {
- for a := range catalog.Enumerate(c.pkgType) {
- t.Log(" ", a)
- }
- t.Fatalf("unexpected package count: %d!=%d", catalog.PackageCount(), len(c.pkgInfo))
- }
+ pkgCount := 0
for a := range catalog.Enumerate(c.pkgType) {
@@ -113,6 +124,14 @@ func TestBundlerImage(t *testing.T) {
if a.Type != c.pkgType {
t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type)
}
+ pkgCount++
+ }
+
+ if pkgCount != len(c.pkgInfo) {
+ for a := range catalog.Enumerate(c.pkgType) {
+ t.Log(" ", a)
+ }
+ t.Fatalf("unexpected package count: %d!=%d", pkgCount, len(c.pkgInfo))
}
})
diff --git a/integration/test-fixtures/image-language-pkgs/Dockerfile b/integration/test-fixtures/image-language-pkgs/Dockerfile
index 2a33c4540..770e60b56 100644
--- a/integration/test-fixtures/image-language-pkgs/Dockerfile
+++ b/integration/test-fixtures/image-language-pkgs/Dockerfile
@@ -1,2 +1,2 @@
FROM scratch
-COPY Gemfile.lock .
\ No newline at end of file
+COPY . .
\ No newline at end of file
diff --git a/integration/test-fixtures/image-language-pkgs/dist-info/METADATA b/integration/test-fixtures/image-language-pkgs/dist-info/METADATA
new file mode 100644
index 000000000..924780dfd
--- /dev/null
+++ b/integration/test-fixtures/image-language-pkgs/dist-info/METADATA
@@ -0,0 +1,47 @@
+Metadata-Version: 2.1
+Name: Pygments
+Version: 2.6.1
+Summary: Pygments is a syntax highlighting package written in Python.
+Home-page: https://pygments.org/
+Author: Georg Brandl
+Author-email: georg@python.org
+License: BSD License
+Keywords: syntax highlighting
+Platform: any
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: End Users/Desktop
+Classifier: Intended Audience :: System Administrators
+Classifier: Development Status :: 6 - Mature
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Text Processing :: Filters
+Classifier: Topic :: Utilities
+Requires-Python: >=3.5
+
+
+Pygments
+~~~~~~~~
+
+Pygments is a syntax highlighting package written in Python.
+
+It is a generic syntax highlighter suitable for use in code hosting, forums,
+wikis or other applications that need to prettify source code. Highlights
+are:
+
+* a wide range of over 500 languages and other text formats is supported
+* special attention is paid to details, increasing quality by a fair amount
+* support for new languages and formats are added easily
+* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
+* it is usable as a command-line tool and as a library
+
+:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
+:license: BSD, see LICENSE for details.
+
diff --git a/integration/test-fixtures/image-language-pkgs/egg-info/PKG-INFO b/integration/test-fixtures/image-language-pkgs/egg-info/PKG-INFO
new file mode 100644
index 000000000..a73770668
--- /dev/null
+++ b/integration/test-fixtures/image-language-pkgs/egg-info/PKG-INFO
@@ -0,0 +1,134 @@
+Metadata-Version: 2.1
+Name: requests
+Version: 2.22.0
+Summary: Python HTTP for Humans.
+Home-page: http://python-requests.org
+Author: Kenneth Reitz
+Author-email: me@kennethreitz.org
+License: Apache 2.0
+Description: Requests: HTTP for Humans™
+ ==========================
+
+ [![image](https://img.shields.io/pypi/v/requests.svg)](https://pypi.org/project/requests/)
+ [![image](https://img.shields.io/pypi/l/requests.svg)](https://pypi.org/project/requests/)
+ [![image](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests/)
+ [![codecov.io](https://codecov.io/github/requests/requests/coverage.svg?branch=master)](https://codecov.io/github/requests/requests)
+ [![image](https://img.shields.io/github/contributors/requests/requests.svg)](https://github.com/requests/requests/graphs/contributors)
+ [![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz)
+
+ Requests is the only *Non-GMO* HTTP library for Python, safe for human
+ consumption.
+
+ ![image](https://farm5.staticflickr.com/4317/35198386374_1939af3de6_k_d.jpg)
+
+ Behold, the power of Requests:
+
+ ``` {.sourceCode .python}
+ >>> import requests
+ >>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
+ >>> r.status_code
+ 200
+ >>> r.headers['content-type']
+ 'application/json; charset=utf8'
+ >>> r.encoding
+ 'utf-8'
+ >>> r.text
+ u'{"type":"User"...'
+ >>> r.json()
+ {u'disk_usage': 368627, u'private_gists': 484, ...}
+ ```
+
+ See [the similar code, sans Requests](https://gist.github.com/973705).
+
+ [![image](https://raw.githubusercontent.com/requests/requests/master/docs/_static/requests-logo-small.png)](http://docs.python-requests.org/)
+
+ Requests allows you to send *organic, grass-fed* HTTP/1.1 requests,
+ without the need for manual labor. There's no need to manually add query
+ strings to your URLs, or to form-encode your POST data. Keep-alive and
+ HTTP connection pooling are 100% automatic, thanks to
+ [urllib3](https://github.com/shazow/urllib3).
+
+ Besides, all the cool kids are doing it. Requests is one of the most
+ downloaded Python packages of all time, pulling in over 11,000,000
+ downloads every month. You don't want to be left out!
+
+ Feature Support
+ ---------------
+
+ Requests is ready for today's web.
+
+ - International Domains and URLs
+ - Keep-Alive & Connection Pooling
+ - Sessions with Cookie Persistence
+ - Browser-style SSL Verification
+ - Basic/Digest Authentication
+ - Elegant Key/Value Cookies
+ - Automatic Decompression
+ - Automatic Content Decoding
+ - Unicode Response Bodies
+ - Multipart File Uploads
+ - HTTP(S) Proxy Support
+ - Connection Timeouts
+ - Streaming Downloads
+ - `.netrc` Support
+ - Chunked Requests
+
+ Requests officially supports Python 2.7 & 3.4–3.7, and runs great on
+ PyPy.
+
+ Installation
+ ------------
+
+ To install Requests, simply use [pipenv](http://pipenv.org/) (or pip, of
+ course):
+
+ ``` {.sourceCode .bash}
+ $ pipenv install requests
+ ✨🍰✨
+ ```
+
+ Satisfaction guaranteed.
+
+ Documentation
+ -------------
+
+ Fantastic documentation is available at
+ , for a limited time only.
+
+ How to Contribute
+ -----------------
+
+ 1. Become more familiar with the project by reading our [Contributor's Guide](http://docs.python-requests.org/en/latest/dev/contributing/) and our [development philosophy](http://docs.python-requests.org/en/latest/dev/philosophy/).
+ 2. Check for open issues or open a fresh issue to start a discussion
+ around a feature idea or a bug. There is a [Contributor
+ Friendly](https://github.com/requests/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open)
+ tag for issues that should be ideal for people who are not very
+ familiar with the codebase yet.
+ 3. Fork [the repository](https://github.com/requests/requests) on
+ GitHub to start making your changes to the **master** branch (or
+ branch off of it).
+ 4. Write a test which shows that the bug was fixed or that the feature
+ works as expected.
+ 5. Send a pull request and bug the maintainer until it gets merged and
+ published. :) Make sure to add yourself to
+ [AUTHORS](https://github.com/requests/requests/blob/master/AUTHORS.rst).
+
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Natural Language :: English
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
+Description-Content-Type: text/markdown
+Provides-Extra: security
+Provides-Extra: socks
\ No newline at end of file