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  }