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

     1  // Copyright 2019 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
    16  
    17  import (
    18  	"crypto/sha256"
    19  	"fmt"
    20  	"regexp"
    21  	"sort"
    22  	"strings"
    23  	"time"
    24  
    25  	structpb "github.com/golang/protobuf/ptypes/struct"
    26  	pb "go.chromium.org/luci/resultdb/proto/v1"
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/reflect/protoreflect"
    29  	"google.golang.org/protobuf/types/known/durationpb"
    30  	"google.golang.org/protobuf/types/known/timestamppb"
    31  
    32  	"go.chromium.org/luci/common/errors"
    33  )
    34  
    35  const MaxSizeProperties = 4 * 1024
    36  
    37  var requestIDRe = regexp.MustCompile(`^[[:ascii:]]{0,36}$`)
    38  
    39  // Allow hostnames permitted by
    40  // https://www.rfc-editor.org/rfc/rfc1123#page-13. (Note that
    41  // the 255 character limit must be seperately applied.)
    42  var hostnameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9-]+(\.[a-z0-9-]+)*$`)
    43  
    44  // The maximum hostname permitted by
    45  // https://www.rfc-editor.org/rfc/rfc1123#page-13.
    46  const hostnameMaxLength = 255
    47  
    48  var sha1Regex = regexp.MustCompile(`^[a-f0-9]{40}$`)
    49  
    50  func regexpf(patternFormat string, subpatterns ...any) *regexp.Regexp {
    51  	return regexp.MustCompile(fmt.Sprintf(patternFormat, subpatterns...))
    52  }
    53  
    54  func doesNotMatch(r *regexp.Regexp) error {
    55  	return errors.Reason("does not match %s", r).Err()
    56  }
    57  
    58  func unspecified() error {
    59  	return errors.Reason("unspecified").Err()
    60  }
    61  
    62  func validateWithRe(re *regexp.Regexp, value string) error {
    63  	if value == "" {
    64  		return unspecified()
    65  	}
    66  	if !re.MatchString(value) {
    67  		return doesNotMatch(re)
    68  	}
    69  	return nil
    70  }
    71  
    72  // MustTimestampProto converts a time.Time to a *timestamppb.Timestamp and panics
    73  // on failure.
    74  func MustTimestampProto(t time.Time) *timestamppb.Timestamp {
    75  	ts := timestamppb.New(t)
    76  	if err := ts.CheckValid(); err != nil {
    77  		panic(err)
    78  	}
    79  	return ts
    80  }
    81  
    82  // MustTimestamp converts a *timestamppb.Timestamp to a time.Time and panics
    83  // on failure.
    84  func MustTimestamp(ts *timestamppb.Timestamp) time.Time {
    85  	if err := ts.CheckValid(); err != nil {
    86  		panic(err)
    87  	}
    88  	t := ts.AsTime()
    89  	return t
    90  }
    91  
    92  // ValidateRequestID returns a non-nil error if requestID is invalid.
    93  // Returns nil if requestID is empty.
    94  func ValidateRequestID(requestID string) error {
    95  	if !requestIDRe.MatchString(requestID) {
    96  		return doesNotMatch(requestIDRe)
    97  	}
    98  	return nil
    99  }
   100  
   101  // ValidateBatchRequestCount validates the number of requests in a batch
   102  // request.
   103  func ValidateBatchRequestCount(count int) error {
   104  	const limit = 500
   105  	if count > limit {
   106  		return errors.Reason("the number of requests in the batch exceeds %d", limit).Err()
   107  	}
   108  	return nil
   109  }
   110  
   111  // ValidateEnum returns a non-nil error if the value is not among valid values.
   112  func ValidateEnum(value int32, validValues map[int32]string) error {
   113  	if _, ok := validValues[value]; !ok {
   114  		return errors.Reason("invalid value %d", value).Err()
   115  	}
   116  	return nil
   117  }
   118  
   119  // MustDuration converts a *durationpb.Duration to a time.Duration and panics
   120  // on failure.
   121  func MustDuration(du *durationpb.Duration) time.Duration {
   122  	if err := du.CheckValid(); err != nil {
   123  		panic(err)
   124  	}
   125  	d := du.AsDuration()
   126  	return d
   127  }
   128  
   129  // MustMarshal marshals a protobuf message and panics on failure.
   130  func MustMarshal(m protoreflect.ProtoMessage) []byte {
   131  	msg, err := proto.Marshal(m)
   132  	if err != nil {
   133  		panic(err)
   134  	}
   135  	return msg
   136  }
   137  
   138  // ValidateProperties returns a non-nil error if properties is invalid.
   139  func ValidateProperties(properties *structpb.Struct) error {
   140  	if proto.Size(properties) > MaxSizeProperties {
   141  		return errors.Reason("exceeds the maximum size of %d bytes", MaxSizeProperties).Err()
   142  	}
   143  	return nil
   144  }
   145  
   146  // ValidateGitilesCommit validates a gitiles commit.
   147  func ValidateGitilesCommit(commit *pb.GitilesCommit) error {
   148  	switch {
   149  	case commit == nil:
   150  		return errors.Reason("unspecified").Err()
   151  
   152  	case commit.Host == "":
   153  		return errors.Reason("host: unspecified").Err()
   154  	case len(commit.Host) > 255:
   155  		return errors.Reason("host: exceeds 255 characters").Err()
   156  	case !hostnameRE.MatchString(commit.Host):
   157  		return errors.Reason("host: does not match %q", hostnameRE).Err()
   158  
   159  	case commit.Project == "":
   160  		return errors.Reason("project: unspecified").Err()
   161  	case len(commit.Project) > hostnameMaxLength:
   162  		return errors.Reason("project: exceeds %v characters", hostnameMaxLength).Err()
   163  
   164  	case commit.Ref == "":
   165  		return errors.Reason("ref: unspecified").Err()
   166  
   167  	// The 255 character ref limit is arbitrary and not based on a known
   168  	// restriction in Git. It exists simply because there should be a limit
   169  	// to protect downstream clients.
   170  	case len(commit.Ref) > 255:
   171  		return errors.Reason("ref: exceeds 255 characters").Err()
   172  	case !strings.HasPrefix(commit.Ref, "refs/"):
   173  		return errors.Reason("ref: does not match refs/.*").Err()
   174  
   175  	case commit.CommitHash == "":
   176  		return errors.Reason("commit_hash: unspecified").Err()
   177  	case !sha1Regex.MatchString(commit.CommitHash):
   178  		return errors.Reason("commit_hash: does not match %q", sha1Regex).Err()
   179  
   180  	case commit.Position == 0:
   181  		return errors.Reason("position: unspecified").Err()
   182  	case commit.Position < 0:
   183  		return errors.Reason("position: cannot be negative").Err()
   184  	}
   185  	return nil
   186  }
   187  
   188  // ValidateGerritChange validates a gerrit change.
   189  func ValidateGerritChange(change *pb.GerritChange) error {
   190  	switch {
   191  	case change == nil:
   192  		return errors.Reason("unspecified").Err()
   193  
   194  	case change.Host == "":
   195  		return errors.Reason("host: unspecified").Err()
   196  	case len(change.Host) > hostnameMaxLength:
   197  		return errors.Reason("host: exceeds %v characters", hostnameMaxLength).Err()
   198  	case !hostnameRE.MatchString(change.Host):
   199  		return errors.Reason("host: does not match %q", hostnameRE).Err()
   200  
   201  	case change.Project == "":
   202  		return errors.Reason("project: unspecified").Err()
   203  	// The 255 character project limit is arbitrary and not based on a known
   204  	// restriction in Gerrit. It exists simply because there should be a limit
   205  	// to protect downstream clients.
   206  	case len(change.Project) > 255:
   207  		return errors.Reason("project: exceeds 255 characters").Err()
   208  
   209  	case change.Change == 0:
   210  		return errors.Reason("change: unspecified").Err()
   211  	case change.Change < 0:
   212  		return errors.Reason("change: cannot be negative").Err()
   213  
   214  	case change.Patchset == 0:
   215  		return errors.Reason("patchset: unspecified").Err()
   216  	case change.Patchset < 0:
   217  		return errors.Reason("patchset: cannot be negative").Err()
   218  	default:
   219  		return nil
   220  	}
   221  }
   222  
   223  // SortGerritChanges sorts in-place the gerrit changes lexicographically.
   224  func SortGerritChanges(changes []*pb.GerritChange) {
   225  	sort.Slice(changes, func(i, j int) bool {
   226  		if changes[i].Host != changes[j].Host {
   227  			return changes[i].Host < changes[j].Host
   228  		}
   229  		if changes[i].Project != changes[j].Project {
   230  			return changes[i].Project < changes[j].Project
   231  		}
   232  		if changes[i].Change != changes[j].Change {
   233  			return changes[i].Change < changes[j].Change
   234  		}
   235  		return changes[i].Patchset < changes[j].Patchset
   236  	})
   237  }
   238  
   239  // SourceRefFromSources extracts a SourceRef from given sources.
   240  func SourceRefFromSources(srcs *pb.Sources) *pb.SourceRef {
   241  	return &pb.SourceRef{
   242  		System: &pb.SourceRef_Gitiles{
   243  			Gitiles: &pb.GitilesRef{
   244  				Host:    srcs.GitilesCommit.Host,
   245  				Project: srcs.GitilesCommit.Project,
   246  				Ref:     srcs.GitilesCommit.Ref,
   247  			},
   248  		}}
   249  }
   250  
   251  // SourceRefHash returns a short hash of the sourceRef.
   252  func SourceRefHash(sr *pb.SourceRef) []byte {
   253  	var result [32]byte
   254  	switch sr.System.(type) {
   255  	case *pb.SourceRef_Gitiles:
   256  		gitiles := sr.GetGitiles()
   257  		result = sha256.Sum256([]byte("gitiles" + "\n" + gitiles.Host + "\n" + gitiles.Project + "\n" + gitiles.Ref))
   258  	default:
   259  		panic("invalid source ref")
   260  	}
   261  	return result[:8]
   262  }