go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/pbutil/predicate.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package pbutil contains methods for manipulating LUCI Analysis protos.
    16  package pbutil
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"regexp"
    22  	"regexp/syntax"
    23  	"strings"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  
    27  	pb "go.chromium.org/luci/analysis/proto/v1"
    28  )
    29  
    30  var (
    31  	// Unspecified is the error to be used when something is unspecified when it's
    32  	// supposed to.
    33  	Unspecified = errors.Reason("unspecified").Err()
    34  
    35  	// DoesNotMatch is the error to be used when a string does not match a regex.
    36  	DoesNotMatch = errors.Reason("does not match").Err()
    37  )
    38  
    39  // validateRegexp returns a non-nil error if re is an invalid regular
    40  // expression.
    41  func validateRegexp(re string) error {
    42  	// Note: regexp.Compile uses syntax.Perl.
    43  	if _, err := syntax.Parse(re, syntax.Perl); err != nil {
    44  		return err
    45  	}
    46  
    47  	// Do not allow ^ and $ in the regexp, because we need to be able to prepend
    48  	// a pattern to the user-supplied pattern.
    49  	if strings.HasPrefix(re, "^") {
    50  		return errors.Reason("must not start with ^; it is prepended automatically").Err()
    51  	}
    52  	if strings.HasSuffix(re, "$") {
    53  		return errors.Reason("must not end with $; it is appended automatically").Err()
    54  	}
    55  
    56  	return nil
    57  }
    58  
    59  // ValidateWithRe validates a value matches the given re.
    60  func ValidateWithRe(re *regexp.Regexp, value string) error {
    61  	if value == "" {
    62  		return Unspecified
    63  	}
    64  	if !re.MatchString(value) {
    65  		return DoesNotMatch
    66  	}
    67  	return nil
    68  }
    69  
    70  // ValidateStringPair returns an error if p is invalid.
    71  func ValidateStringPair(p *pb.StringPair) error {
    72  	if err := ValidateWithRe(stringPairKeyRe, p.Key); err != nil {
    73  		return errors.Annotate(err, "key").Err()
    74  	}
    75  	if len(p.Key) > maxStringPairKeyLength {
    76  		return errors.Reason("key length must be less or equal to %d", maxStringPairKeyLength).Err()
    77  	}
    78  	if len(p.Value) > maxStringPairValueLength {
    79  		return errors.Reason("value length must be less or equal to %d", maxStringPairValueLength).Err()
    80  	}
    81  	return nil
    82  }
    83  
    84  // ValidateVariant returns an error if vr is invalid.
    85  func ValidateVariant(vr *pb.Variant) error {
    86  	for k, v := range vr.GetDef() {
    87  		p := pb.StringPair{Key: k, Value: v}
    88  		if err := ValidateStringPair(&p); err != nil {
    89  			return errors.Annotate(err, "%q:%q", k, v).Err()
    90  		}
    91  	}
    92  	return nil
    93  }
    94  
    95  // ValidateVariantPredicate returns a non-nil error if p is determined to be
    96  // invalid.
    97  func ValidateVariantPredicate(p *pb.VariantPredicate) error {
    98  	switch pr := p.Predicate.(type) {
    99  	case *pb.VariantPredicate_Equals:
   100  		return errors.Annotate(ValidateVariant(pr.Equals), "equals").Err()
   101  	case *pb.VariantPredicate_Contains:
   102  		return errors.Annotate(ValidateVariant(pr.Contains), "contains").Err()
   103  	case *pb.VariantPredicate_HashEquals:
   104  		return errors.Annotate(ValidateWithRe(variantHashRe, pr.HashEquals), "hash_equals").Err()
   105  	case nil:
   106  		return Unspecified
   107  	default:
   108  		panic("impossible")
   109  	}
   110  }
   111  
   112  // ValidateTestVerdictPredicate returns a non-nil error if p is determined to be
   113  // invalid.
   114  func ValidateTestVerdictPredicate(predicate *pb.TestVerdictPredicate) error {
   115  	if predicate == nil {
   116  		return Unspecified
   117  	}
   118  
   119  	if predicate.GetVariantPredicate() != nil {
   120  		if err := ValidateVariantPredicate(predicate.GetVariantPredicate()); err != nil {
   121  			return err
   122  		}
   123  	}
   124  	return ValidateEnum(int32(predicate.GetSubmittedFilter()), pb.SubmittedFilter_name)
   125  }
   126  
   127  // ValidateEnum returns a non-nil error if the value is not among valid values.
   128  func ValidateEnum(value int32, validValues map[int32]string) error {
   129  	if _, ok := validValues[value]; !ok {
   130  		return errors.Reason("invalid value %d", value).Err()
   131  	}
   132  	return nil
   133  }
   134  
   135  // ValidateTimeRange returns a non-nil error if tr is determined to be invalid.
   136  // To be valid, a TimeRange must have both Earliest and Latest fields specified,
   137  // and the Earliest time must be chronologically before the Latest time.
   138  func ValidateTimeRange(ctx context.Context, tr *pb.TimeRange) error {
   139  	if tr == nil {
   140  		return Unspecified
   141  	}
   142  
   143  	earliest, err := AsTime(tr.Earliest)
   144  	if err != nil {
   145  		return errors.Annotate(err, "earliest").Err()
   146  	}
   147  
   148  	latest, err := AsTime(tr.Latest)
   149  	if err != nil {
   150  		return errors.Annotate(err, "latest").Err()
   151  	}
   152  
   153  	if !earliest.Before(latest) {
   154  		return fmt.Errorf("earliest must be before latest")
   155  	}
   156  
   157  	return nil
   158  }