mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
SPDX wrote `FileName` from `RealPath` only, so two archives' `META-INF/MANIFEST.MF` became two SPDX files with the same name and nothing saying which archive each came from. Worse, a consumer reading just the name would take a nested file for one at that path in the scan root. - a file inside an archive is now named by its full chain, using the same `:` convention and escaping as `ArchivePath` / java's `VirtualPath`: `app.war:WEB-INF/lib/a.jar:META-INF/MANIFEST.MF` - `FileComment` gains an `archivePath: ...` line (next to the existing `layerID: ...`, one per line), so decoding recovers `ArchivePath` and the entry's own `RealPath` without guessing where the chain ends - files outside archives are unchanged Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
108 lines
4.2 KiB
Go
108 lines
4.2 KiB
Go
package archive
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
)
|
|
|
|
// Traversal describes the archive whose contents a cataloger is running against. The archive
|
|
// cataloger task puts one on the context before running catalogers inside an archive, so a cataloger
|
|
// such as java can describe the archive it is inside without owning the recursion.
|
|
type Traversal struct {
|
|
// Location is the archive file's location within its parent filesystem, including the access path
|
|
// it was reached by. Its ArchivePath names the archives above it, so VirtualPath(Location) is the
|
|
// full chain from the scan root as seen through that access path. The ArchivePath of the files
|
|
// inside it is built from the real path instead.
|
|
Location file.Location
|
|
|
|
// Digests are of the archive file itself, taken during extraction; a cataloger running inside the
|
|
// archive cannot compute them since the archive is not inside itself.
|
|
Digests []file.Digest
|
|
|
|
// Parent is the traversal of the archive this one is nested in, nil for an archive at the scan root.
|
|
Parent *Traversal
|
|
|
|
// Packages are those the catalogers found describing this archive (located at it), filled in once
|
|
// they have run. Catalogers inside a nested archive read them through Parent, as java does to link a
|
|
// nested jar to the jar that holds it.
|
|
Packages []pkg.Package
|
|
}
|
|
|
|
// ContentsArchivePath is the ArchivePath every file inside the archive carries: the chain to it, built
|
|
// from real paths. Outside any archive (a nil Traversal) it is blank, as for files at the scan root.
|
|
func (t *Traversal) ContentsArchivePath() string {
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
return VirtualPath(file.NewLocationFromCoordinates(t.Location.Coordinates))
|
|
}
|
|
|
|
// VirtualPath returns the colon-delimited chain of archives from the scan root to loc, ending in loc
|
|
// itself: "app.war:WEB-INF/lib/dep.jar". Outside any archive it is the location's path unchanged. In an
|
|
// entry name '%' is escaped as "%25" and then ':' as "%3A", so splitting on ':' recovers the archive
|
|
// boundaries and no two entry names share an escaped form. The leading slash is dropped to match the
|
|
// java cataloger, which joins slash-less zip entry names onto the containing archive's path.
|
|
func VirtualPath(loc file.Location) string {
|
|
if loc.ArchivePath == "" {
|
|
return loc.Path()
|
|
}
|
|
return loc.ArchivePath + ":" + strings.TrimPrefix(escapeEntryPath(loc.Path()), "/")
|
|
}
|
|
|
|
var (
|
|
entryPathEscaper = strings.NewReplacer("%", "%25", ":", "%3A")
|
|
entryPathUnescaper = strings.NewReplacer("%3A", ":", "%25", "%")
|
|
)
|
|
|
|
func escapeEntryPath(p string) string {
|
|
return entryPathEscaper.Replace(p)
|
|
}
|
|
|
|
// UnescapeEntryPath reverses the escaping VirtualPath applies to one segment of the chain.
|
|
func UnescapeEntryPath(p string) string {
|
|
return entryPathUnescaper.Replace(p)
|
|
}
|
|
|
|
type traversalCtxKey struct{}
|
|
|
|
func WithTraversal(ctx context.Context, t *Traversal) context.Context {
|
|
return context.WithValue(ctx, traversalCtxKey{}, t)
|
|
}
|
|
|
|
// TraversalFromContext returns the archive traversal on the context, or nil outside an archive.
|
|
func TraversalFromContext(ctx context.Context) *Traversal {
|
|
t, _ := ctx.Value(traversalCtxKey{}).(*Traversal)
|
|
return t
|
|
}
|
|
|
|
type budgetCtxKey struct{}
|
|
|
|
// WithBudget sets the decompression budget that Extract spends from, for this archive and every archive
|
|
// extracted beneath it.
|
|
func WithBudget(ctx context.Context, b *Budget) context.Context {
|
|
return context.WithValue(ctx, budgetCtxKey{}, b)
|
|
}
|
|
|
|
func budgetFromContext(ctx context.Context) *Budget {
|
|
b, _ := ctx.Value(budgetCtxKey{}).(*Budget)
|
|
return b
|
|
}
|
|
|
|
type nestedCatalogingCtxKey struct{}
|
|
|
|
// WithNestedCataloging marks the context of a scan whose archive cataloger task will extract every
|
|
// archive and run the catalogers inside it. syft.CreateSBOM sets it before any task runs, so a
|
|
// cataloger that would otherwise open archives itself can leave them to the task instead.
|
|
func WithNestedCataloging(ctx context.Context) context.Context {
|
|
return context.WithValue(ctx, nestedCatalogingCtxKey{}, true)
|
|
}
|
|
|
|
// NestedCatalogingEnabled reports whether the archive cataloger task is extracting archives in this scan.
|
|
func NestedCatalogingEnabled(ctx context.Context) bool {
|
|
enabled, _ := ctx.Value(nestedCatalogingCtxKey{}).(bool)
|
|
return enabled
|
|
}
|