mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 16:06:36 +01:00
feat: initial recursive archive cataloging impl
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// Notify receives structured events from archive extraction. A nil Notify is the no-op.
|
||||
//
|
||||
// The signature is deliberately `func(any)` rather than a set of typed callbacks or an interface with
|
||||
// a method per event. Extraction is a hot path - a scan of a big image enters thousands of archives -
|
||||
// and what instrumentation costs there has to be nothing when it is off. A single func field is one
|
||||
// nil check, and a handler that cares about one event type asserts for it and ignores the rest, so
|
||||
// adding an event never changes a signature or breaks a handler that did not ask for it.
|
||||
//
|
||||
// Guard the call rather than relying on a nil check inside the callee:
|
||||
//
|
||||
// if notify != nil {
|
||||
// notify(EntriesOverflowed{Archive: name, Bytes: n})
|
||||
// }
|
||||
//
|
||||
// Building the event value is what costs - putting a struct in an `any` allocates - so the guard is
|
||||
// what makes "off" free rather than merely cheap. Send exists for the paths where that does not
|
||||
// matter.
|
||||
type Notify func(msg any)
|
||||
|
||||
// Send delivers one event, doing nothing when there is no handler. Convenient, but it builds the
|
||||
// event whether or not anyone is listening: on a hot path, guard the call instead.
|
||||
func (n Notify) Send(msg any) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
n(msg)
|
||||
}
|
||||
|
||||
// Tee returns a Notify that delivers to each of the given handlers, skipping the nil ones, and nil
|
||||
// when none of them are live - so a tee of nothing stays free.
|
||||
func Tee(handlers ...Notify) Notify {
|
||||
var live []Notify
|
||||
for _, h := range handlers {
|
||||
if h != nil {
|
||||
live = append(live, h)
|
||||
}
|
||||
}
|
||||
switch len(live) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return live[0]
|
||||
}
|
||||
return func(msg any) {
|
||||
for _, h := range live {
|
||||
h(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OverflowReason says why content was written to disk rather than held.
|
||||
type OverflowReason string
|
||||
|
||||
const (
|
||||
// MemoryLimitReached means the scan was already holding as much as the memory limit admits.
|
||||
MemoryLimitReached OverflowReason = "memory limit reached"
|
||||
|
||||
// MemoryHoldsNothing means the memory limit is zero, so nothing is ever held and every archive's
|
||||
// content goes to disk by policy rather than by pressure.
|
||||
MemoryHoldsNothing OverflowReason = "memory limit is zero"
|
||||
)
|
||||
|
||||
// ContentOverflowed reports that one archive's own bytes were written to disk instead of being held in
|
||||
// memory. Bytes is the size of the archive.
|
||||
type ContentOverflowed struct {
|
||||
Archive string
|
||||
Bytes int64
|
||||
Reason OverflowReason
|
||||
}
|
||||
|
||||
// EntriesOverflowed reports that the entries an archive's store was holding have been written to its
|
||||
// overflow blob. Entries and Bytes are what moved, not what the archive holds in total: a store spills
|
||||
// everything it is holding at once, and may spill more than once as it fills again.
|
||||
type EntriesOverflowed struct {
|
||||
Archive string
|
||||
Entries int
|
||||
Bytes int64
|
||||
Reason OverflowReason
|
||||
}
|
||||
|
||||
// Skipped reports that an archive contributed nothing, and why. Not a failure - the scan
|
||||
// continues - but the packages inside it are absent from the SBOM, which is worth being able to see.
|
||||
type Skipped struct {
|
||||
Archive string
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Truncated reports that an archive was cataloged from part of its contents, naming the bound
|
||||
// that stopped it.
|
||||
type Truncated struct {
|
||||
Archive string
|
||||
Reason TruncationReason
|
||||
}
|
||||
|
||||
// LogNotify is the default handler: it writes each event to the debug log. Anything it does not
|
||||
// recognize is ignored rather than logged blindly, so an event added for one consumer does not turn
|
||||
// into noise for everyone.
|
||||
func LogNotify(msg any) {
|
||||
switch e := msg.(type) {
|
||||
case ContentOverflowed:
|
||||
log.WithFields("archive", e.Archive, "bytes", e.Bytes, "reason", string(e.Reason)).
|
||||
Debug("archive content written to disk rather than held in memory")
|
||||
case EntriesOverflowed:
|
||||
log.WithFields("archive", e.Archive, "entries", e.Entries, "bytes", e.Bytes, "reason", string(e.Reason)).
|
||||
Debug("archive entries written to disk rather than held in memory")
|
||||
case Skipped:
|
||||
log.WithFields("archive", e.Archive, "reason", e.Reason).Debug("archive skipped")
|
||||
case Truncated:
|
||||
log.WithFields("archive", e.Archive, "reason", string(e.Reason)).Debug("archive cataloged from part of its contents")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user