Files
syft/internal/archive/glob.go
T

391 lines
10 KiB
Go

package archive
import (
"path"
"strings"
"sync"
"sync/atomic"
syftindex "github.com/anchore/syft/internal/index"
)
// A glob is parsed once into segments, cached by pattern since catalogers ask the same few hundred
// globs of every archive. A brace group inside a segment, `g{o,o.exe}`, makes that segment a set of
// alternatives, each looked up on its own; only a group spanning `/` expands the whole pattern.
// The literal text at either end of every alternative is looked up in the base-name indexes: files
// for the last segment, directories for the rest. The smallest set found is the candidate set, and
// each candidate is verified segment by segment up its parent chain. Only a pattern with no literal
// text anywhere falls back to every file.
// globSegment is one `/`-delimited piece of a pattern.
type globSegment struct {
alts []globAlt // the texts a name may match; one unless the segment has a brace group
any bool // `**`: zero or more directories
// indexed reports every alternative has literal text to look up; exact that every lookup is the
// whole answer for its alternative: a plain name, `prefix*` or `*suffix`
indexed, exact bool
}
// globAlt is one alternative text of a segment.
type globAlt struct {
text string // ready for path.Match
// prefix and suffix are the literal text before the first and after the last metacharacter, the
// necessary conditions a name index can check. For a plain text prefix is the whole of it.
prefix, suffix string
plain bool
}
// maxCachedGlobs bounds the parse cache, which is sized for the catalogers' own patterns.
const maxCachedGlobs = 4096
var (
parsedGlobs sync.Map // pattern -> [][]globSegment
parsedGlobCount atomic.Int64
)
// parseGlob returns the segment lists a pattern spells out: one, unless a brace group spans `/`.
// Leading slashes are dropped since paths are archive-relative; an empty pattern has none.
func parseGlob(pattern string) [][]globSegment {
if cached, ok := parsedGlobs.Load(pattern); ok {
return cached.([][]globSegment)
}
var out [][]globSegment
if braceSpansSeparator(pattern) {
for _, alternative := range expandBraces(pattern) {
out = append(out, parseSegments(alternative))
}
} else if segments := parseSegments(pattern); segments != nil {
out = [][]globSegment{segments}
}
if parsedGlobCount.Add(1) <= maxCachedGlobs {
parsedGlobs.Store(pattern, out)
}
return out
}
func parseSegments(pattern string) []globSegment {
pattern = strings.TrimLeft(pattern, "/")
if pattern == "" {
return nil
}
var segments []globSegment
for _, text := range strings.Split(pattern, "/") {
s := globSegment{any: text == "**", indexed: true, exact: true}
if !s.any {
texts := []string{text}
if strings.IndexByte(text, '{') >= 0 {
texts = expandBraces(text)
}
for _, t := range texts {
a := globAlt{text: strings.ReplaceAll(t, "[!", "[^")} // doublestar negation, for path.Match
a.prefix, a.suffix, a.plain = literalEnds(a.text)
s.indexed = s.indexed && (a.prefix != "" || a.suffix != "")
s.exact = s.exact && (a.plain || a.text == a.prefix+"*" || a.text == "*"+a.suffix)
s.alts = append(s.alts, a)
}
}
segments = append(segments, s)
}
return segments
}
// braceSpansSeparator reports whether a `/` sits inside a brace group, so the group's alternatives
// have different segment counts.
func braceSpansSeparator(pattern string) bool {
depth := 0
for i := 0; i < len(pattern); i++ {
switch pattern[i] {
case '\\':
i++
case '{':
depth++
case '}':
depth = max(depth-1, 0)
case '/':
if depth > 0 {
return true
}
}
}
return false
}
// literalEnds returns the literal text before the first metacharacter and after the last one, and
// whether there were none. A character class is one metacharacter however long, as is an escape pair.
func literalEnds(seg string) (prefix, suffix string, plain bool) {
first, lastEnd := -1, -1
mark := func(start, end int) {
if first < 0 {
first = start
}
lastEnd = end
}
for i := 0; i < len(seg); {
switch seg[i] {
case '*', '?':
mark(i, i+1)
i++
case '\\':
mark(i, min(i+2, len(seg)))
i += 2
case '[':
// a `]` right after `[` or `[^` is a member of the class, not its end
j := i + 1
if j < len(seg) && seg[j] == '^' {
j++
}
if j < len(seg) && seg[j] == ']' {
j++
}
end := strings.IndexByte(seg[j:], ']')
if end < 0 {
mark(i, len(seg))
i = len(seg)
} else {
mark(i, j+end+1)
i = j + end + 1
}
default:
i++
}
}
if first < 0 {
return seg, "", true
}
return seg[:first], seg[lastEnd:], false
}
// expandBraces returns every pattern a brace group spells out, nested groups included. A pattern
// without a complete group is returned as is.
func expandBraces(pattern string) []string {
start, depth := -1, 0
for i := 0; i < len(pattern); i++ {
switch pattern[i] {
case '\\':
i++
case '{':
if depth == 0 {
start = i
}
depth++
case '}':
if depth == 0 {
continue
}
depth--
if depth == 0 {
var out []string
for _, alt := range splitAlternatives(pattern[start+1 : i]) {
out = append(out, expandBraces(pattern[:start]+alt+pattern[i+1:])...)
}
return out
}
}
}
return []string{pattern}
}
// splitAlternatives splits a brace group's body on the commas at its top level.
func splitAlternatives(body string) []string {
var out []string
depth, last := 0, 0
for i := 0; i < len(body); i++ {
switch body[i] {
case '\\':
i++
case '{':
depth++
case '}':
depth--
case ',':
if depth == 0 {
out = append(out, body[last:i])
last = i + 1
}
}
}
return append(out, body[last:])
}
// lookup finds the nodes a segment admits in a base-name index: for each alternative the smaller of
// what its prefix and suffix find, or the name itself when it is plain. It reports false for a
// segment with an alternative that has no literal text, which admits every name.
func lookup(idx *syftindex.PrefixSuffix[[]*node], s globSegment) ([]*node, bool) {
if !s.indexed {
return nil, false
}
var out []*node
for _, a := range s.alts {
switch {
case a.plain:
out = append(out, idx.Get(a.text)...)
case a.suffix == "":
out = append(out, flatten(idx.ByPrefix(a.prefix))...)
case a.prefix == "":
out = append(out, flatten(idx.BySuffix(a.suffix))...)
default:
byPrefix, bySuffix := flatten(idx.ByPrefix(a.prefix)), flatten(idx.BySuffix(a.suffix))
if len(bySuffix) < len(byPrefix) {
byPrefix = bySuffix
}
out = append(out, byPrefix...)
}
}
return out, true
}
func flatten(groups [][]*node) []*node {
var out []*node
for _, group := range groups {
out = append(out, group...)
}
return out
}
// exactByName reports whether the pattern is `**/<segment>` with a segment the file-name index answers
// completely, so its lookup needs no verification.
func exactByName(segments []globSegment) bool {
return len(segments) == 2 && segments[0].any && segments[1].exact
}
// narrowEnough is a candidate count small enough that looking for a smaller set is not worth it.
const narrowEnough = 64
// globCandidates returns the files a pattern can match, and whether they are exactly the answer.
func (r *Resolver) globCandidates(segments []globSegment) (candidates []*node, exact bool) {
last := segments[len(segments)-1]
best := r.files
if !last.any {
if files, ok := lookup(&r.names, last); ok {
if exactByName(segments) || len(files) <= narrowEnough {
return files, exactByName(segments)
}
best = files
}
}
// a directory segment narrows to what lies under the directories it names; take it only when
// that is smaller than what the file name gave, stopping the walk as soon as it is not
for _, s := range segments[:len(segments)-1] {
if s.any {
continue
}
dirs, ok := lookup(&r.dirs, s)
if !ok || len(dirs) >= len(best) {
continue
}
if under := filesUnder(dirs, len(best)-1); under != nil {
best = under
}
}
return best, false
}
// filesUnder collects the files anywhere beneath the given directories, or nil once more than limit
// are found.
func filesUnder(dirs []*node, limit int) []*node {
var out []*node
stack := append([]*node(nil), dirs...)
for len(stack) > 0 {
n := stack[len(stack)-1]
stack = stack[:len(stack)-1]
for _, child := range n.children {
if child.isDir {
stack = append(stack, child)
continue
}
if child.header != nil {
if len(out) >= limit {
return nil
}
out = append(out, child)
}
}
}
return out
}
// matchNode reports whether the segments match the node's path, read up its parent chain.
func matchNode(segments []globSegment, n *node) (bool, error) {
var buf [16]string
names := buf[:0]
for m := n; m.parent != nil; m = m.parent {
names = append(names, m.name)
}
for i, j := 0, len(names)-1; i < j; i, j = i+1, j-1 {
names[i], names[j] = names[j], names[i]
}
return matchSegments(segments, names)
}
// matchSegments matches segments against path components. The segments after the last `**` sit at
// fixed positions from the end, so they are matched against the tail first, which settles the common
// `**/x` in one comparison; what remains ends in `**` and is matched greedily, backing up to the most
// recent `**` on a mismatch.
func matchSegments(segments []globSegment, names []string) (bool, error) {
lastAny := -1
for i, s := range segments {
if s.any {
lastAny = i
}
}
tail := segments[lastAny+1:]
if len(names) < len(tail) {
return false, nil
}
names, tailNames := names[:len(names)-len(tail)], names[len(names)-len(tail):]
for i, s := range tail {
if ok, err := s.match(tailNames[i]); !ok || err != nil {
return false, err
}
}
if lastAny < 0 {
return len(names) == 0, nil
}
segments = segments[:lastAny+1]
si, ni := 0, 0
starSeg, starName := -1, 0
for ni < len(names) {
if si < len(segments) {
if segments[si].any {
starSeg, starName = si, ni
si++
continue
}
ok, err := segments[si].match(names[ni])
if err != nil {
return false, err
}
if ok {
si++
ni++
continue
}
}
if starSeg < 0 {
return false, nil
}
starName++
si, ni = starSeg+1, starName
}
for ; si < len(segments); si++ {
if !segments[si].any {
return false, nil
}
}
return true, nil
}
// match reports whether any of the segment's alternatives matches a name.
func (s globSegment) match(name string) (bool, error) {
for _, a := range s.alts {
if ok, err := path.Match(a.text, name); ok || err != nil {
return ok, err
}
}
return false, nil
}