github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/internal/task/expression.go (about) 1 package task 2 3 import ( 4 "fmt" 5 "regexp" 6 "sort" 7 8 "github.com/hashicorp/go-multierror" 9 "github.com/scylladb/go-set/strset" 10 11 "github.com/anchore/syft/syft/cataloging/pkgcataloging" 12 ) 13 14 var expressionNodePattern = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-+]*)+$`) 15 16 const ( 17 SetOperation Operation = "set" 18 AddOperation Operation = "add" 19 SubSelectOperation Operation = "sub-select" 20 RemoveOperation Operation = "remove" 21 ) 22 23 var ( 24 ErrEmptyToken = fmt.Errorf("no value given") 25 ErrInvalidToken = fmt.Errorf("invalid token given: only alphanumeric characters and hyphens are allowed") 26 ErrInvalidOperator = fmt.Errorf("invalid operator given") 27 ErrUnknownNameOrTag = fmt.Errorf("unknown name or tag given") 28 ErrTagsNotAllowed = fmt.Errorf("tags are not allowed with this operation (must use exact names)") 29 ErrNamesNotAllowed = fmt.Errorf("names are not allowed with this operation (must use tags)") 30 ErrAllNotAllowed = fmt.Errorf("cannot use the 'all' operand in this context") 31 ) 32 33 // ErrInvalidExpression represents an expression that cannot be parsed or can be parsed but is logically invalid. 34 type ErrInvalidExpression struct { 35 Expression string 36 Operation Operation 37 Err error 38 } 39 40 func (e ErrInvalidExpression) Error() string { 41 return fmt.Sprintf("invalid expression: %q: %s", e.Expression, e.Err.Error()) 42 } 43 44 func newErrInvalidExpression(exp string, op Operation, err error) ErrInvalidExpression { 45 return ErrInvalidExpression{ 46 Expression: exp, 47 Operation: op, 48 Err: err, 49 } 50 } 51 52 // Expression represents a single operation-operand pair with (all validation errors). 53 // E.g. "+foo", "-bar", or "something" are all expressions. Some validations are relevant to not only the 54 // syntax (operation and operator) but other are sensitive to the context of the operand (e.g. if a given operand 55 // is a tag or a name, validated against the operation). 56 type Expression struct { 57 Operation Operation 58 Operand string 59 Errors []error 60 } 61 62 // Operation represents the type of operation to perform on the operand (set, add, remove, sub-select). 63 type Operation string 64 65 // Expressions represents a list of expressions. 66 type Expressions []Expression 67 68 // expressionContext represents all information needed to validate an expression (e.g. the set of all tasks and their tags). 69 type expressionContext struct { 70 Names *strset.Set 71 Tags *strset.Set 72 } 73 74 func newExpressionContext(ts []Task) *expressionContext { 75 ec := &expressionContext{ 76 Names: strset.New(tasks(ts).Names()...), 77 Tags: strset.New(tasks(ts).Tags()...), 78 } 79 80 ec.Tags.Add("all") 81 82 return ec 83 } 84 85 // newExpression creates a new validated Expression object relative to the task names and tags. 86 func (ec expressionContext) newExpression(exp string, operation Operation, token string) Expression { 87 if token == "" { 88 return Expression{ 89 Operation: operation, 90 Operand: token, 91 Errors: []error{newErrInvalidExpression(exp, operation, ErrEmptyToken)}, 92 } 93 } 94 95 if !isValidNode(token) { 96 return Expression{ 97 Operation: operation, 98 Operand: token, 99 Errors: []error{newErrInvalidExpression(exp, operation, ErrInvalidToken)}, 100 } 101 } 102 103 var err error 104 switch operation { 105 case SetOperation, RemoveOperation: 106 // names and tags allowed 107 if !ec.Tags.Has(token) && !ec.Names.Has(token) { 108 err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag) 109 } 110 case AddOperation: 111 // only names are allowed 112 if !ec.Names.Has(token) { 113 if ec.Tags.Has(token) { 114 err = newErrInvalidExpression(exp, operation, ErrTagsNotAllowed) 115 } else { 116 err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag) 117 } 118 } 119 case SubSelectOperation: 120 if token == "all" { 121 // special case: we cannot sub-select all (this is most likely a misconfiguration and the user intended to use the set operation) 122 err = newErrInvalidExpression(exp, operation, ErrAllNotAllowed) 123 } else if !ec.Tags.Has(token) { 124 // only tags are allowed... 125 if ec.Names.Has(token) { 126 err = newErrInvalidExpression(exp, operation, ErrNamesNotAllowed) 127 } else { 128 err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag) 129 } 130 } 131 } 132 133 var errs []error 134 if err != nil { 135 errs = append(errs, err) 136 } 137 138 return Expression{ 139 Operation: operation, 140 Operand: token, 141 Errors: errs, 142 } 143 } 144 145 func newExpressionsFromSelectionRequest(nc *expressionContext, selectionRequest pkgcataloging.SelectionRequest) Expressions { 146 var all Expressions 147 148 for _, exp := range selectionRequest.DefaultNamesOrTags { 149 all = append(all, nc.newExpression(exp, SetOperation, exp)) 150 } 151 152 for _, exp := range selectionRequest.SubSelectTags { 153 all = append(all, nc.newExpression(exp, SubSelectOperation, exp)) 154 } 155 156 for _, exp := range selectionRequest.AddNames { 157 all = append(all, nc.newExpression(exp, AddOperation, exp)) 158 } 159 160 for _, exp := range selectionRequest.RemoveNamesOrTags { 161 all = append(all, nc.newExpression(exp, RemoveOperation, exp)) 162 } 163 164 sort.Sort(all) 165 return all 166 } 167 168 func isValidNode(s string) bool { 169 return expressionNodePattern.Match([]byte(s)) 170 } 171 172 func (e Expressions) Clone() Expressions { 173 clone := make(Expressions, len(e)) 174 copy(clone, e) 175 return clone 176 } 177 178 func (e Expression) String() string { 179 var op string 180 switch e.Operation { 181 case AddOperation: 182 op = "+" 183 case RemoveOperation: 184 op = "-" 185 case SubSelectOperation: 186 op = "" 187 case SetOperation: 188 op = "" 189 default: 190 op = "?" 191 } 192 return op + e.Operand 193 } 194 195 func (e Expressions) Len() int { 196 return len(e) 197 } 198 199 func (e Expressions) Swap(i, j int) { 200 e[i], e[j] = e[j], e[i] 201 } 202 203 // order of operations 204 var orderOfOps = map[Operation]int{ 205 SetOperation: 1, 206 SubSelectOperation: 2, 207 RemoveOperation: 3, 208 AddOperation: 4, 209 } 210 211 func (e Expressions) Less(i, j int) bool { 212 ooi := orderOfOps[e[i].Operation] 213 ooj := orderOfOps[e[j].Operation] 214 215 if ooi != ooj { 216 return ooi < ooj 217 } 218 219 return i < j 220 } 221 222 func (e Expressions) Errors() (errs []error) { 223 for _, n := range e { 224 if len(n.Errors) > 0 { 225 errs = append(errs, n.Errors...) 226 } 227 } 228 return errs 229 } 230 231 func (e Expressions) Validate() error { 232 errs := e.Errors() 233 if len(errs) == 0 { 234 return nil 235 } 236 var err error 237 return multierror.Append(err, e.Errors()...) 238 }