github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/internal/task/selection.go (about) 1 package task 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/scylladb/go-set/strset" 8 9 "github.com/anchore/syft/internal/log" 10 "github.com/anchore/syft/syft/cataloging/pkgcataloging" 11 ) 12 13 // Selection represents the users request for a subset of tasks to run and the resulting set of task names that were 14 // selected. Additionally, all tokens that were matched on to reach the returned conclusion are also provided. 15 type Selection struct { 16 Request pkgcataloging.SelectionRequest 17 Result *strset.Set 18 TokensByTask map[string]TokenSelection 19 } 20 21 // TokenSelection represents the tokens that were matched on to either include or exclude a given task (based on expression evaluation). 22 type TokenSelection struct { 23 SelectedOn *strset.Set 24 DeselectedOn *strset.Set 25 } 26 27 func newTokenSelection(selected, deselected []string) TokenSelection { 28 return TokenSelection{ 29 SelectedOn: strset.New(selected...), 30 DeselectedOn: strset.New(deselected...), 31 } 32 } 33 34 func (ts *TokenSelection) merge(other ...TokenSelection) { 35 for _, o := range other { 36 if ts.SelectedOn != nil { 37 ts.SelectedOn.Add(o.SelectedOn.List()...) 38 } 39 if ts.DeselectedOn != nil { 40 ts.DeselectedOn.Add(o.DeselectedOn.List()...) 41 } 42 } 43 } 44 45 func newSelection() Selection { 46 return Selection{ 47 Result: strset.New(), 48 TokensByTask: make(map[string]TokenSelection), 49 } 50 } 51 52 // Select parses the given expressions as two sets: expressions that represent a "set" operation, and expressions that 53 // represent all other operations. The parsed expressions are then evaluated against the given tasks to return 54 // a subset (or the same) set of tasks. 55 func Select(allTasks []Task, selectionRequest pkgcataloging.SelectionRequest) ([]Task, Selection, error) { 56 nodes := newExpressionsFromSelectionRequest(newExpressionContext(allTasks), selectionRequest) 57 58 finalTasks, selection := selectByExpressions(allTasks, nodes) 59 60 selection.Request = selectionRequest 61 62 return finalTasks, selection, nodes.Validate() 63 } 64 65 // selectByExpressions the set of tasks to run based on the given expression(s). 66 func selectByExpressions(ts tasks, nodes Expressions) (tasks, Selection) { 67 if len(nodes) == 0 { 68 return ts, newSelection() 69 } 70 71 finalSet := newSet() 72 selectionSet := newSet() 73 addSet := newSet() 74 removeSet := newSet() 75 76 allSelections := make(map[string]TokenSelection) 77 78 nodes = nodes.Clone() 79 sort.Sort(nodes) 80 81 for i, node := range nodes { 82 if len(node.Errors) > 0 { 83 continue 84 } 85 selectedTasks, selections := evaluateExpression(ts, node) 86 87 for name, ss := range selections { 88 if selection, exists := allSelections[name]; exists { 89 ss.merge(selection) 90 } 91 allSelections[name] = ss 92 } 93 94 if len(selectedTasks) == 0 { 95 log.WithFields("selection", fmt.Sprintf("%q", node.String())).Warn("no cataloger tasks selected found for given selection (this might be a misconfiguration)") 96 } 97 98 switch node.Operation { 99 case SetOperation: 100 finalSet.Add(selectedTasks...) 101 case AddOperation, "": 102 addSet.Add(selectedTasks...) 103 case RemoveOperation: 104 removeSet.Add(selectedTasks...) 105 case SubSelectOperation: 106 selectionSet.Add(selectedTasks...) 107 default: 108 nodes[i].Errors = append(nodes[i].Errors, ErrInvalidOperator) 109 } 110 } 111 112 if len(selectionSet.tasks) > 0 { 113 finalSet.Intersect(selectionSet.Tasks()...) 114 } 115 finalSet.Remove(removeSet.Tasks()...) 116 finalSet.Add(addSet.Tasks()...) 117 118 finalTasks := finalSet.Tasks() 119 120 return finalTasks, Selection{ 121 Result: strset.New(finalTasks.Names()...), 122 TokensByTask: allSelections, 123 } 124 } 125 126 // evaluateExpression returns the set of tasks that match the given expression (as well as all tokens that were matched 127 // on to reach the returned conclusion). 128 func evaluateExpression(ts tasks, node Expression) ([]Task, map[string]TokenSelection) { 129 selection := make(map[string]TokenSelection) 130 var finalTasks []Task 131 132 for _, t := range ts { 133 if !isSelected(t, node.Operand) { 134 continue 135 } 136 137 s := newTokenSelection(nil, nil) 138 139 switch node.Operation { 140 case SetOperation, SubSelectOperation, AddOperation: 141 s.SelectedOn.Add(node.Operand) 142 case RemoveOperation: 143 s.DeselectedOn.Add(node.Operand) 144 } 145 146 finalTasks = append(finalTasks, t) 147 148 if og, exists := selection[t.Name()]; exists { 149 s.merge(og) 150 } 151 152 selection[t.Name()] = s 153 } 154 return finalTasks, selection 155 } 156 157 // isSelected returns true if the given task matches the given token. If the token is "all" then the task is always selected. 158 func isSelected(td Task, token string) bool { 159 if token == "all" { 160 return true 161 } 162 163 if ts, ok := td.(Selector); ok { 164 // use the selector to verify all tags 165 if ts.HasAllSelectors(token) { 166 return true 167 } 168 } 169 170 // only do exact name matching 171 if td.Name() == token { 172 return true 173 } 174 175 return false 176 }