go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/pbutil/artifact.go (about) 1 // Copyright 2020 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 "fmt" 19 "net/url" 20 "regexp" 21 22 "go.chromium.org/luci/common/errors" 23 ) 24 25 const ( 26 // Unicode character classes L, M, N, P, S, Zs are the "graphic" type code points, 27 // so a good approximation of all the printable characters. 28 // Also accept '.' as the first character to enable upload of files starting with . 29 artifactIDPattern = `(?:[[:word:]]|\.)([\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]{0,254}[[:word:]])?` 30 ) 31 32 var ( 33 artifactIDRe = regexpf("^%s$", artifactIDPattern) 34 invocationArtifactNamePattern = fmt.Sprintf("invocations/(%s)/artifacts/(.+)", invocationIDPattern) 35 testResultArtifactNamePattern = fmt.Sprintf("invocations/(%s)/tests/([^/]+)/results/(%s)/artifacts/(.+)", invocationIDPattern, resultIDPattern) 36 invocationArtifactNameRe = regexpf("^%s$", invocationArtifactNamePattern) 37 testResultArtifactNameRe = regexpf("^%s$", testResultArtifactNamePattern) 38 artifactNameRe = regexpf("^%s|%s$", testResultArtifactNamePattern, invocationArtifactNamePattern) 39 textArtifactContentTypeRe = regexpf("^text/*") 40 ) 41 42 // ValidateArtifactID returns a non-nil error if id is invalid. 43 func ValidateArtifactID(id string) error { 44 return validateWithRe(artifactIDRe, id) 45 } 46 47 // ValidateArtifactName returns a non-nil error if name is invalid. 48 func ValidateArtifactName(name string) error { 49 return validateWithRe(artifactNameRe, name) 50 } 51 52 // ParseArtifactName extracts the invocation ID, unescaped test id, result ID 53 // and artifact ID. 54 // The testID and resultID are empty if this is an invocation-level artifact. 55 func ParseArtifactName(name string) (invocationID, testID, resultID, artifactID string, err error) { 56 if name == "" { 57 err = unspecified() 58 return 59 } 60 61 unescape := func(escaped string, re *regexp.Regexp) (string, error) { 62 unescaped, err := url.PathUnescape(escaped) 63 if err != nil { 64 return "", errors.Annotate(err, "%q", escaped).Err() 65 } 66 67 if err := validateWithRe(re, unescaped); err != nil { 68 return "", errors.Annotate(err, "%q", unescaped).Err() 69 } 70 71 return unescaped, nil 72 } 73 74 unescapeTestID := func(escaped string) (string, error) { 75 unescaped, err := url.PathUnescape(escaped) 76 if err != nil { 77 return "", errors.Annotate(err, "%q", escaped).Err() 78 } 79 80 if err := ValidateTestID(unescaped); err != nil { 81 return "", errors.Annotate(err, "%q", unescaped).Err() 82 } 83 84 return unescaped, nil 85 } 86 87 if m := invocationArtifactNameRe.FindStringSubmatch(name); m != nil { 88 invocationID = m[1] 89 artifactID, err = unescape(m[2], artifactIDRe) 90 err = errors.Annotate(err, "artifact ID").Err() 91 return 92 } 93 94 if m := testResultArtifactNameRe.FindStringSubmatch(name); m != nil { 95 invocationID = m[1] 96 if testID, err = unescapeTestID(m[2]); err != nil { 97 err = errors.Annotate(err, "test ID").Err() 98 return 99 } 100 resultID = m[3] 101 artifactID, err = unescape(m[4], artifactIDRe) 102 err = errors.Annotate(err, "artifact ID").Err() 103 return 104 } 105 106 err = doesNotMatch(artifactNameRe) 107 return 108 } 109 110 // InvocationArtifactName synthesizes a name of an invocation-level artifact. 111 // Does not validate IDs, use ValidateInvocationID and ValidateArtifactID. 112 func InvocationArtifactName(invocationID, artifactID string) string { 113 return fmt.Sprintf("invocations/%s/artifacts/%s", invocationID, url.PathEscape(artifactID)) 114 } 115 116 // TestResultArtifactName synthesizes a name of an test-result-level artifact. 117 // Does not validate IDs, use ValidateInvocationID, ValidateTestID, 118 // ValidateResultID and ValidateArtifactID. 119 func TestResultArtifactName(invocationID, testID, resulID, artifactID string) string { 120 return fmt.Sprintf("invocations/%s/tests/%s/results/%s/artifacts/%s", invocationID, url.PathEscape(testID), resulID, url.PathEscape(artifactID)) 121 } 122 123 // IsTextArtifact returns true if the content type represents a text-based artifact. 124 // Note: As the artifact content type field is optional, it is possible that 125 // a text artifact was uploaded to ResultDB without a content type. In such case, 126 // this function will return false. 127 // 128 // We rather miss some text artifact than wrongly classify a non-text artifact as 129 // text artifact. 130 func IsTextArtifact(contentType string) bool { 131 return textArtifactContentTypeRe.MatchString(contentType) 132 }