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  }