go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/proto/structmask/parse.go (about) 1 // Copyright 2021 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 structmask 16 17 import ( 18 "encoding/json" 19 "strconv" 20 "strings" 21 22 "go.chromium.org/luci/common/errors" 23 ) 24 25 // pathElement represents a single parsed StructMask path element. 26 type pathElement any 27 28 type starElement struct{} 29 type fieldElement struct{ field string } 30 type indexElement struct{ index int } 31 32 // parseMask parses a mask into a filtering tree. 33 func parseMask(mask []*StructMask) (root *node, err error) { 34 for _, m := range mask { 35 elements := make([]pathElement, len(m.Path)) 36 for idx, p := range m.Path { 37 if elements[idx], err = parseElement(p); err != nil { 38 return nil, errors.Annotate(err, "bad element %q in the mask %s", p, maskToStr(m)).Err() 39 } 40 } 41 if len(elements) == 0 { 42 return nil, errors.Reason("bad empty mask").Err() 43 } 44 if root, err = updateNode(root, elements); err != nil { 45 return nil, errors.Annotate(err, "unsupported mask %s", maskToStr(m)).Err() 46 } 47 } 48 return root, nil 49 } 50 51 // maskToStr converts a mask to a string for error messages. 52 func maskToStr(m *StructMask) string { 53 blob, err := json.Marshal(m.Path) 54 if err != nil { 55 panic(err) 56 } 57 return string(blob) 58 } 59 60 // parseElement interprets one path element in a StructMask. 61 func parseElement(p string) (pathElement, error) { 62 // If starts with `"` or `'` or ``` it must be a valid quoted string. 63 if strings.HasPrefix(p, `"`) || strings.HasPrefix(p, `'`) || strings.HasPrefix(p, "`") { 64 s, err := strconv.Unquote(p) 65 if err != nil { 66 return nil, errors.Annotate(err, "bad quoted string").Err() 67 } 68 return fieldElement{s}, nil 69 } 70 71 // Reserve `/.../` for regexps, if we ever allow them. 72 if len(p) >= 2 && strings.HasPrefix(p, `/`) && strings.HasSuffix(p, `/`) { 73 return nil, errors.Reason( 74 "regexp matches are not supported; "+ 75 "if you want to match a literal field /.../, wrap the value in quotes: %s", 76 strconv.Quote(p)).Err() 77 } 78 79 // If it contains `*`, we require it to be just `*` for now. That way we can 80 // later add prefix and suffix matches by allowing e.g. `something*`. 81 if p == "*" { 82 return starElement{}, nil 83 } 84 if strings.Contains(p, "*") { 85 return nil, errors.Reason( 86 "prefix and suffix matches are not supported; "+ 87 "if you want to match a field with literal `*` in it, wrap the value in quotes: %s", 88 strconv.Quote(p)).Err() 89 } 90 91 // If it looks like a number (even a float), it is a list index. We require it 92 // to be a non-negative integer though. 93 if _, err := strconv.ParseFloat(p, 32); err == nil { 94 val, err := strconv.ParseInt(p, 10, 32) 95 if err != nil || val < 0 { 96 return nil, errors.Reason("an index must be a non-negative integer").Err() 97 } 98 return indexElement{int(val)}, nil 99 } 100 101 // Otherwise assume it is a field name. 102 return fieldElement{p}, nil 103 } 104 105 // updateNode inserts the mask into the filtering tree. 106 // 107 // Empty `mask` here represent a leaf node ("take everything that's left"). 108 func updateNode(n *node, mask []pathElement) (*node, error) { 109 // If this is the very end of the mask, grab everything that's left. 110 if len(mask) == 0 { 111 return leafNode, nil 112 } 113 114 // Some mask path already requested this element and all its inner guts. Just 115 // ignore `mask`, it will not filter out anything. 116 if n == leafNode { 117 return leafNode, nil 118 } 119 120 // `n == nil` happens when we visit some path for the first time ever. If `n` 121 // is not nil, it means we've visited this path already in some previous 122 // StructMask and just need to update it (e.g. add more fields). 123 if n == nil { 124 n = &node{} 125 } 126 127 var err error 128 switch elem := mask[0].(type) { 129 case starElement: 130 n.star, err = updateNode(n.star, mask[1:]) 131 case fieldElement: 132 if n.fields == nil { 133 n.fields = make(map[string]*node, 1) 134 } 135 n.fields[elem.field], err = updateNode(n.fields[elem.field], mask[1:]) 136 case indexElement: 137 err = errors.Reason("individual index selectors are not supported").Err() 138 } 139 return n, err 140 }