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 }