diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index d0a5ee678..e351c8392 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -5,7 +5,7 @@ inputs: go-version: description: "Go version to install" required: true - default: "1.21.x" + default: "1.22.x" go-dependencies: description: "Download go dependencies" required: true diff --git a/go.mod b/go.mod index fccab6a05..6a5fbf45f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/anchore/syft -go 1.21.0 +go 1.22.0 require ( github.com/CycloneDX/cyclonedx-go v0.8.0 diff --git a/syft/internal/fileresolver/directory_indexer.go b/syft/internal/fileresolver/directory_indexer.go index 92495ab77..2d9101999 100644 --- a/syft/internal/fileresolver/directory_indexer.go +++ b/syft/internal/fileresolver/directory_indexer.go @@ -9,13 +9,11 @@ import ( "path/filepath" "strings" - "github.com/moby/sys/mountinfo" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/filetree" - "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/event" @@ -43,7 +41,8 @@ func newDirectoryIndexer(path, base string, visitors ...PathIndexVisitor) *direc []PathIndexVisitor{ requireFileInfo, disallowByFileType, - newUnixSystemMountFinder().disallowUnixSystemRuntimePath}, + skipPathsByMountTypeAndName(path), + }, visitors..., ), errPaths: make(map[string]error), @@ -450,57 +449,6 @@ func (r *directoryIndexer) disallowRevisitingVisitor(_, path string, _ os.FileIn return nil } -type unixSystemMountFinder struct { - disallowedMountPaths []string -} - -func newUnixSystemMountFinder() unixSystemMountFinder { - infos, err := mountinfo.GetMounts(nil) - if err != nil { - log.WithFields("error", err).Warnf("unable to get system mounts") - return unixSystemMountFinder{} - } - - return unixSystemMountFinder{ - disallowedMountPaths: keepUnixSystemMountPaths(infos), - } -} - -func keepUnixSystemMountPaths(infos []*mountinfo.Info) []string { - var mountPaths []string - for _, info := range infos { - if info == nil { - continue - } - // we're only interested in ignoring the logical filesystems typically found at these mount points: - // - /proc - // - procfs - // - proc - // - /sys - // - sysfs - // - /dev - // - devfs - BSD/darwin flavored systems and old linux systems - // - devtmpfs - driver core maintained /dev tmpfs - // - udev - userspace implementation that replaced devfs - // - tmpfs - used for /dev in special instances (within a container) - - switch info.FSType { - case "proc", "procfs", "sysfs", "devfs", "devtmpfs", "udev", "tmpfs": - log.WithFields("mountpoint", info.Mountpoint).Debug("ignoring system mountpoint") - - mountPaths = append(mountPaths, info.Mountpoint) - } - } - return mountPaths -} - -func (f unixSystemMountFinder) disallowUnixSystemRuntimePath(_, path string, _ os.FileInfo, _ error) error { - if internal.HasAnyOfPrefixes(path, f.disallowedMountPaths...) { - return fs.SkipDir - } - return nil -} - func disallowByFileType(_, _ string, info os.FileInfo, _ error) error { if info == nil { // we can't filter out by filetype for non-existent files diff --git a/syft/internal/fileresolver/directory_indexer_test.go b/syft/internal/fileresolver/directory_indexer_test.go index f30925d00..3b736b667 100644 --- a/syft/internal/fileresolver/directory_indexer_test.go +++ b/syft/internal/fileresolver/directory_indexer_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/moby/sys/mountinfo" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -462,145 +461,3 @@ func relativePath(basePath, givenPath string) string { return relPath } - -func Test_disallowUnixSystemRuntimePath(t *testing.T) { - unixSubject := unixSystemMountFinder{ - // mock out detecting the mount points - disallowedMountPaths: []string{"/proc", "/sys", "/dev"}, - } - - tests := []struct { - name string - path string - base string - expected error - }{ - { - name: "relative path to proc is allowed", - path: "proc/place", - }, - { - name: "relative path within proc is not allowed", - path: "/proc/place", - expected: fs.SkipDir, - }, - { - name: "path exactly to proc is not allowed", - path: "/proc", - expected: fs.SkipDir, - }, - { - name: "similar to proc", - path: "/pro/c", - }, - { - name: "similar to proc", - path: "/pro", - }, - { - name: "dev is not allowed", - path: "/dev", - expected: fs.SkipDir, - }, - { - name: "sys is not allowed", - path: "/sys", - expected: fs.SkipDir, - }, - { - name: "unrelated allowed path", - path: "/something/sys", - }, - { - name: "do not consider base when matching paths (non-matching)", - base: "/a/b/c", - path: "/a/b/c/dev", - }, - { - name: "do not consider base when matching paths (matching)", - base: "/a/b/c", - path: "/dev", - expected: fs.SkipDir, - }, - } - for _, test := range tests { - t.Run(test.path, func(t *testing.T) { - assert.Equal(t, test.expected, unixSubject.disallowUnixSystemRuntimePath(test.base, test.path, nil, nil)) - }) - } -} - -func Test_keepUnixSystemMountPaths(t *testing.T) { - - tests := []struct { - name string - infos []*mountinfo.Info - want []string - }{ - { - name: "all valid filesystems", - infos: []*mountinfo.Info{ - { - Mountpoint: "/etc/hostname", - FSType: "/dev/vda1", - }, - { - Mountpoint: "/sys/fs/cgroup", - FSType: "cgroup", - }, - { - Mountpoint: "/", - FSType: "overlay", - }, - }, - want: nil, - }, - { - name: "no valid filesystems", - infos: []*mountinfo.Info{ - { - Mountpoint: "/proc", - FSType: "proc", - }, - { - Mountpoint: "/proc-2", - FSType: "procfs", - }, - { - Mountpoint: "/sys", - FSType: "sysfs", - }, - { - Mountpoint: "/dev", - FSType: "devfs", - }, - { - Mountpoint: "/dev-u", - FSType: "udev", - }, - { - Mountpoint: "/dev-tmp", - FSType: "devtmpfs", - }, - { - Mountpoint: "/run", - FSType: "tmpfs", - }, - }, - want: []string{ - "/proc", - "/proc-2", - "/sys", - "/dev", - "/dev-u", - "/dev-tmp", - "/run", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, keepUnixSystemMountPaths(tt.infos)) - }) - } -} diff --git a/syft/internal/fileresolver/path_skipper.go b/syft/internal/fileresolver/path_skipper.go new file mode 100644 index 000000000..4ae9bb361 --- /dev/null +++ b/syft/internal/fileresolver/path_skipper.go @@ -0,0 +1,157 @@ +package fileresolver + +import ( + "io/fs" + "os" + "sort" + "strings" + + "github.com/moby/sys/mountinfo" + + "github.com/anchore/syft/internal/log" +) + +type pathSkipper struct { + // scanTarget is the root path that is being scanned (without any base-path logic applied). + scanTarget string + + // ignorableMountTypes is a set of mount types that should be ignored. Optionally a list of paths (the map values) + // can be provided that this mount type should be ignored at. For example in some containers /dev is mounted + // as a tmpfs and should be ignored, but /tmp should not be ignored. An empty list of paths means that paths + // within the mount type should always be ignored. + ignorableMountTypes map[string][]string + + // current mount paths for the current system + mounts []*mountinfo.Info + mountsByType map[string][]*mountinfo.Info +} + +// skipPathsByMountTypeAndName accepts the root path and returns a PathIndexVisitor that will skip paths based +// the filesystem type, the mountpoint, and configured blocklist paths for each filesystem type. +// This will help syft dodge filesystem topologies that have the potential to make the search space much bigger in +// areas known to not traditionally contain files of interest (installed software). It is meant to allow scanning +// "/" on a unix host to succeed, while also not causing any files in a narrow directory scan to be skipped unnecessarily. +func skipPathsByMountTypeAndName(root string) PathIndexVisitor { + infos, err := mountinfo.GetMounts(nil) + if err != nil { + log.WithFields("error", err).Warnf("unable to get system mounts") + return func(_ string, _ string, _ os.FileInfo, _ error) error { + return nil + } + } + + return newPathSkipperFromMounts(root, infos).pathIndexVisitor +} + +func newPathSkipperFromMounts(root string, infos []*mountinfo.Info) pathSkipper { + // we're only interested in ignoring the logical filesystems typically found at these mount points: + // - /proc + // - procfs + // - proc + // - /sys + // - sysfs + // - /dev + // - devfs - BSD/darwin flavored systems and old linux systems + // - devtmpfs - driver core maintained /dev tmpfs + // - udev - userspace implementation that replaced devfs + // - tmpfs - used for /dev in special instances (within a container) + ignorableMountTypes := map[string][]string{ + "proc": nil, + "procfs": nil, + "sysfs": nil, + "devfs": nil, + "devtmpfs": nil, + "udev": nil, + // note: there should be no order required (e.g. search /sys/thing before /sys) since that would imply that + // we could not ignore a nested path within a path that would be ignored anyway. + "tmpfs": {"/run", "/dev", "/var/run", "/var/lock", "/sys"}, + } + + // The longest path is the most specific path, e.g. + // if / is mounted as tmpfs, but /home/syft/permanent is mounted as ext4, + // then the mount type for /home/syft/permanent/foo is ext4, and the mount info + // stating that /home/syft/permanent is ext4 has the longer mount point. + sort.Slice(infos, func(i, j int) bool { + return len(infos[i].Mountpoint) > len(infos[j].Mountpoint) + }) + + mountsByType := make(map[string][]*mountinfo.Info) + + for _, mi := range infos { + mountsByType[mi.FSType] = append(mountsByType[mi.FSType], mi) + } + + return pathSkipper{ + scanTarget: root, + ignorableMountTypes: ignorableMountTypes, + mounts: infos, + mountsByType: mountsByType, + } +} + +func (ps pathSkipper) pathIndexVisitor(_ string, givenPath string, _ os.FileInfo, _ error) error { + for _, mi := range ps.mounts { + conditionalPaths, ignorable := ps.ignorableMountTypes[mi.FSType] + + if len(conditionalPaths) == 0 { + // Rule 1: ignore any path within a mount point that is of the given filesystem type unconditionally + if !containsPath(givenPath, mi.Mountpoint) { + continue + } + + if !ignorable { + // we've matched on the most specific path at this point, which means we should stop searching + // mount points for this path + break + } + + log.WithFields( + "path", givenPath, + "mountpoint", mi.Mountpoint, + "fs", mi.FSType, + ).Debug("ignoring path based on mountpoint filesystem type") + + return fs.SkipDir + } + + // Rule 2: ignore any path within a mount point that is of the given filesystem type, only if + // the path is on a known blocklist of paths for that filesystem type. + // For example: /dev can be mounted as a tmpfs, which should always be skipped. + for _, conditionalPath := range conditionalPaths { + if !containsPath(givenPath, conditionalPath) { + continue + } + + log.WithFields( + "path", givenPath, + "mountpoint", mi.Mountpoint, + "fs", mi.FSType, + "condition", conditionalPath, + ).Debug("ignoring path based on mountpoint filesystem type") + + return fs.SkipDir + } + } + + return nil +} + +func containsPath(p1, p2 string) bool { + p1Clean := simpleClean(p1) + p2Clean := simpleClean(p2) + if p1Clean == p2Clean { + return true + } + return strings.HasPrefix(p1Clean, p2Clean+"/") +} + +func simpleClean(p string) string { + p = strings.TrimSpace(p) + if p == "" { + return "." + } + if p == "/" { + return "/" + } + return strings.TrimSuffix(p, "/") +} diff --git a/syft/internal/fileresolver/path_skipper_test.go b/syft/internal/fileresolver/path_skipper_test.go new file mode 100644 index 000000000..d4e501eca --- /dev/null +++ b/syft/internal/fileresolver/path_skipper_test.go @@ -0,0 +1,395 @@ +package fileresolver + +import ( + "io/fs" + "testing" + + "github.com/moby/sys/mountinfo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_newPathSkipper(t *testing.T) { + type expect struct { + path string + wantErr assert.ErrorAssertionFunc + } + unixSubject := []*mountinfo.Info{ + { + Mountpoint: "/proc", + FSType: "procfs", + }, + { + Mountpoint: "/sys", + FSType: "sysfs", + }, + { + Mountpoint: "/dev", + FSType: "devfs", + }, + { + Mountpoint: "/", + FSType: "/dev/disk3s1s1", + }, + { + Mountpoint: "/dev/shm", + FSType: "shm", + }, + { + Mountpoint: "/tmp", + FSType: "tmpfs", + }, + } + + tests := []struct { + name string + root string + base string + mounts []*mountinfo.Info + want []expect + }{ + { + name: "happy path", + root: "/somewhere", + mounts: []*mountinfo.Info{ + { + Mountpoint: "/home/somewhere/else", + FSType: "/dev/disk3s6", + }, + { + Mountpoint: "/somewhere", + FSType: "/dev/disk3s7", + }, + }, + want: []expect{ + { + // within a known mountpoint with valid type (1) + path: "/somewhere/dev", + }, + { + // is a known mountpoint with valid type + path: "/somewhere", + }, + { + // within a known mountpoint with valid type (2) + path: "/home/somewhere/else/too", + }, + { + // outside of any known mountpoint should not be an error + path: "/bogus", + }, + }, + }, + { + name: "ignore paths within a scan target", + root: "/somewhere", + mounts: []*mountinfo.Info{ + { + Mountpoint: "/somewhere/doesnt/matter/proc", + FSType: "procfs", + }, + { + Mountpoint: "/somewhere", + FSType: "/dev/disk3s7", + }, + }, + want: []expect{ + { + // within a known mountpoint with valid type (1) + path: "/somewhere/dev", + }, + { + // is a known mountpoint with valid type + path: "/somewhere", + }, + { + // mountpoint that should be ignored + path: "/somewhere/doesnt/matter/proc", + wantErr: assertSkipErr(), + }, + { + // within a mountpoint that should be ignored + path: "/somewhere/doesnt/matter/proc", + wantErr: assertSkipErr(), + }, + }, + }, + { + name: "nested mountpoints behave correctly", + root: "/somewhere", + mounts: []*mountinfo.Info{ + { + Mountpoint: "/somewhere/dev", + FSType: "devfs", + }, + { + Mountpoint: "/somewhere/dev/includeme", + FSType: "/dev/disk3s7", + }, + }, + want: []expect{ + { + // is a known mountpoint with valid type + path: "/somewhere/dev", + wantErr: assertSkipErr(), + }, + { + // is a known mountpoint with valid type + path: "/somewhere/dev/includeme", + }, + { + // within a known mountpoint with valid type + path: "/somewhere/dev/includeme/too!", + }, + }, + }, + { + name: "keep some tmpfs mounts conditionally", + root: "/", + mounts: []*mountinfo.Info{ + { + Mountpoint: "/run/somewhere", + FSType: "tmpfs", + }, + { + Mountpoint: "/run/terrafirma", + FSType: "/dev/disk3s8", + }, + { + Mountpoint: "/tmp", + FSType: "tmpfs", + }, + { + Mountpoint: "/else/othertmp", + FSType: "tmpfs", + }, + { + Mountpoint: "/else/othertmp/includeme", + FSType: "/dev/disk3s7", + }, + }, + want: []expect{ + { + // since /run is explicitly ignored, this should be skipped + path: "/run/somewhere/else", + wantErr: assertSkipErr(), + }, + { + path: "/run/terrafirma", + }, + { + path: "/run/terrafirma/nested", + }, + { + path: "/tmp", + }, + { + path: "/else/othertmp/includeme", + }, + { + path: "/else/othertmp/includeme/nested", + }, + { + // no mount path, so we should include it + path: "/somewhere/dev/includeme", + }, + { + // keep additional tmpfs mounts that are not explicitly ignored + path: "/else/othertmp", + }, + }, + }, + { + name: "ignore known trixy tmpfs paths", + root: "/", + mounts: []*mountinfo.Info{ + { + Mountpoint: "/", + FSType: "/dev/disk3s7", + }, + { + Mountpoint: "/dev", + FSType: "tmpfs", + }, + { + Mountpoint: "/run", + FSType: "tmpfs", + }, + { + Mountpoint: "/var/run", + FSType: "tmpfs", + }, + { + Mountpoint: "/var/lock", + FSType: "tmpfs", + }, + { + Mountpoint: "/sys", + FSType: "tmpfs", + }, + { + Mountpoint: "/tmp", + FSType: "tmpfs", + }, + }, + want: []expect{ + { + path: "/dev", + wantErr: assertSkipErr(), + }, + { + path: "/run", + wantErr: assertSkipErr(), + }, + { + path: "/var/run", + wantErr: assertSkipErr(), + }, + { + path: "/var/lock", + wantErr: assertSkipErr(), + }, + { + path: "/sys", + wantErr: assertSkipErr(), + }, + // show that we honor ignoring nested paths + { + path: "/sys/nested", + wantErr: assertSkipErr(), + }, + // show that paths outside of the known mountpoints are not skipped + { + path: "/stuff", + }, + // show that we allow other tmpfs paths that are not on the blocklist + { + path: "/tmp/allowed", + }, + // show sibling paths with same prefix (e.g. /sys vs /system) to that of not allowed paths are not skipped + { + path: "/system", + }, + }, + }, + { + name: "test unix paths", + mounts: unixSubject, + root: "/", + want: []expect{ + { + // relative path to proc is allowed + path: "proc/place", + }, + { + // relative path within proc is not allowed + path: "/proc/place", + wantErr: assertSkipErr(), + }, + { + // path exactly to proc is not allowed + path: "/proc", + wantErr: assertSkipErr(), + }, + { + // similar to proc + path: "/pro/c", + }, + { + // similar to proc + path: "/pro", + }, + { + // dev is not allowed + path: "/dev", + wantErr: assertSkipErr(), + }, + { + // sys is not allowed + path: "/sys", + wantErr: assertSkipErr(), + }, + }, + }, + { + name: "test unix paths with base", + mounts: unixSubject, + root: "/", + base: "/a/b/c", + want: []expect{ + { + // do not consider base when matching paths (non-matching) + path: "/a/b/c/dev", + }, + { + // do not consider base when matching paths (matching) + path: "/dev", + wantErr: assertSkipErr(), + }, + }, + }, + { + name: "mimic nixos setup", + root: "/", + mounts: []*mountinfo.Info{ + { + Mountpoint: "/", + FSType: "tmpfs", // this is an odd setup, but valid + }, + { + Mountpoint: "/home", + FSType: "/dev/disk3s7", + }, + }, + want: []expect{ + { + path: "/home/somewhere", + }, + { + path: "/home", + }, + { + path: "/somewhere", + }, + { + // still not allowed... + path: "/run", + wantErr: assertSkipErr(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.base == "" { + tt.base = tt.root + } + + require.NotEmpty(t, tt.want) + ps := newPathSkipperFromMounts(tt.root, tt.mounts) + + for _, exp := range tt.want { + t.Run(exp.path, func(t *testing.T) { + + got := ps.pathIndexVisitor(tt.base, exp.path, nil, nil) + if exp.wantErr == nil { + assert.NoError(t, got) + return + } + exp.wantErr(t, got) + + }) + } + }) + } +} + +func assertSkipErr() assert.ErrorAssertionFunc { + return assertErrorIs(fs.SkipDir) +} + +func assertErrorIs(want error) assert.ErrorAssertionFunc { + return func(t assert.TestingT, got error, msgAndArgs ...interface{}) bool { + return assert.ErrorIs(t, got, want, msgAndArgs...) + } +} diff --git a/test/cli/archive_test.go b/test/cli/archive_test.go new file mode 100644 index 000000000..a35b4ead2 --- /dev/null +++ b/test/cli/archive_test.go @@ -0,0 +1,60 @@ +package cli + +import ( + "archive/tar" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestArchiveScan(t *testing.T) { + tests := []struct { + name string + args []string + archiveFixture string + env map[string]string + assertions []traitAssertion + }{ + { + name: "scan an archive within the temp dir", + args: []string{ + "scan", + "-o", + "json", + "file:" + createArchive(t, "test-fixtures/archive", t.TempDir()), + }, + assertions: []traitAssertion{ + assertSuccessfulReturnCode, + assertJsonReport, + assertPackageCount(1), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd, stdout, stderr := runSyft(t, test.env, test.args...) + for _, traitAssertionFn := range test.assertions { + traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) + } + logOutputOnFailure(t, cmd, stdout, stderr) + }) + } +} + +func createArchive(t *testing.T, path string, destDir string) string { + // create a tarball of the test fixtures (not by shelling out) + archivePath := filepath.Join(destDir, "test.tar") + + fh, err := os.Create(archivePath) + require.NoError(t, err) + defer fh.Close() + + writer := tar.NewWriter(fh) + require.NoError(t, writer.AddFS(os.DirFS(path))) + require.NoError(t, writer.Close()) + + return archivePath +} diff --git a/test/cli/test-fixtures/archive/dist-info/METADATA b/test/cli/test-fixtures/archive/dist-info/METADATA new file mode 100644 index 000000000..924780dfd --- /dev/null +++ b/test/cli/test-fixtures/archive/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/test/cli/test-fixtures/archive/dist-info/RECORD b/test/cli/test-fixtures/archive/dist-info/RECORD new file mode 100644 index 000000000..af233f390 --- /dev/null +++ b/test/cli/test-fixtures/archive/dist-info/RECORD @@ -0,0 +1,5 @@ +../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220 +Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449 +Pygments-2.6.1.dist-info/RECORD,, +pygments/__pycache__/__init__.cpython-38.pyc,, +pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778 \ No newline at end of file diff --git a/test/cli/test-fixtures/archive/dist-info/top_level.txt b/test/cli/test-fixtures/archive/dist-info/top_level.txt new file mode 100644 index 000000000..1e09fdb90 --- /dev/null +++ b/test/cli/test-fixtures/archive/dist-info/top_level.txt @@ -0,0 +1 @@ +top-level-pkg \ No newline at end of file