github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmi/path.go (about)

     1  // Copyright (c) 2017 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  package gnmi
     6  
     7  import (
     8  	"fmt"
     9  	"sort"
    10  	"strings"
    11  
    12  	pb "github.com/openconfig/gnmi/proto/gnmi"
    13  )
    14  
    15  // nextTokenIndex returns the end index of the first token.
    16  func nextTokenIndex(path string) int {
    17  	var inBrackets bool
    18  	var escape bool
    19  	for i, c := range path {
    20  		switch c {
    21  		case '[':
    22  			inBrackets = true
    23  			escape = false
    24  		case ']':
    25  			if !escape {
    26  				inBrackets = false
    27  			}
    28  			escape = false
    29  		case '\\':
    30  			escape = !escape
    31  		case '/':
    32  			if !inBrackets && !escape {
    33  				return i
    34  			}
    35  			escape = false
    36  		default:
    37  			escape = false
    38  		}
    39  	}
    40  	return len(path)
    41  }
    42  
    43  // SplitPath splits a gnmi path according to the spec. See
    44  // https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-path-conventions.md
    45  // No validation is done. Behavior is undefined if path is an invalid
    46  // gnmi path. TODO: Do validation?
    47  func SplitPath(path string) []string {
    48  	var result []string
    49  	if len(path) > 0 && path[0] == '/' {
    50  		path = path[1:]
    51  	}
    52  	for len(path) > 0 {
    53  		i := nextTokenIndex(path)
    54  		result = append(result, path[:i])
    55  		path = path[i:]
    56  		if len(path) > 0 && path[0] == '/' {
    57  			path = path[1:]
    58  		}
    59  	}
    60  	return result
    61  }
    62  
    63  // SplitPaths splits multiple gnmi paths
    64  func SplitPaths(paths []string) [][]string {
    65  	out := make([][]string, len(paths))
    66  	for i, path := range paths {
    67  		out[i] = SplitPath(path)
    68  	}
    69  	return out
    70  }
    71  
    72  // StrPath builds a human-readable form of a gnmi path.
    73  // e.g. /a/b/c[e=f]
    74  func StrPath(path *pb.Path) string {
    75  	if path == nil {
    76  		return "/"
    77  	} else if len(path.Elem) != 0 {
    78  		return strPathV04(path)
    79  	} else if len(path.Element) != 0 {
    80  		return strPathV03(path)
    81  	}
    82  	return "/"
    83  }
    84  
    85  // writeKey is used as a helper to contain the logic of writing keys as a string.
    86  func writeKey(b *strings.Builder, key map[string]string) {
    87  	// Sort the keys so that they print in a consistent
    88  	// order. We don't have the YANG AST information, so the
    89  	// best we can do is sort them alphabetically.
    90  	size := 0
    91  	keys := make([]string, 0, len(key))
    92  	for k, v := range key {
    93  		keys = append(keys, k)
    94  		size += len(k) + len(v) + 3 // [, =, ]
    95  	}
    96  	sort.Strings(keys)
    97  	b.Grow(size)
    98  	for _, k := range keys {
    99  		b.WriteByte('[')
   100  		b.WriteString(escapeKey(k))
   101  		b.WriteByte('=')
   102  		b.WriteString(escapeValue(key[k]))
   103  		b.WriteByte(']')
   104  	}
   105  }
   106  
   107  // KeyToString is used to get the string representation of the keys.
   108  func KeyToString(key map[string]string) string {
   109  	if len(key) == 1 {
   110  		for k, v := range key {
   111  			return "[" + escapeKey(k) + "=" + escapeValue(v) + "]"
   112  		}
   113  	}
   114  	var b strings.Builder
   115  	writeKey(&b, key)
   116  	return b.String()
   117  }
   118  
   119  func writeElem(b *strings.Builder, elm *pb.PathElem) {
   120  	b.WriteString(escapeName(elm.Name))
   121  	if len(elm.Key) > 0 {
   122  		writeKey(b, elm.Key)
   123  	}
   124  }
   125  
   126  func escapeKey(s string) string {
   127  	s = strings.ReplaceAll(s, `\`, `\\`)
   128  	s = strings.ReplaceAll(s, `=`, `\=`)
   129  	return s
   130  }
   131  
   132  func escapeValue(s string) string {
   133  	s = strings.ReplaceAll(s, `\`, `\\`)
   134  	s = strings.ReplaceAll(s, `]`, `\]`)
   135  	return s
   136  }
   137  
   138  func escapeName(s string) string {
   139  	s = strings.ReplaceAll(s, `\`, `\\`)
   140  	s = strings.ReplaceAll(s, `/`, `\/`)
   141  	s = strings.ReplaceAll(s, `[`, `\[`)
   142  	return s
   143  }
   144  
   145  // ElemToString is used to get the string representation of the Element.
   146  func ElemToString(elm *pb.PathElem) string {
   147  	b := &strings.Builder{}
   148  	writeElem(b, elm)
   149  	return b.String()
   150  }
   151  
   152  // strPathV04 handles the v0.4 gnmi and later path.Elem member.
   153  func strPathV04(path *pb.Path) string {
   154  	b := &strings.Builder{}
   155  	for _, elm := range path.Elem {
   156  		b.WriteRune('/')
   157  		writeElem(b, elm)
   158  	}
   159  	return b.String()
   160  }
   161  
   162  // strPathV03 handles the v0.3 gnmi and earlier path.Element member.
   163  func strPathV03(path *pb.Path) string {
   164  	return "/" + strings.Join(path.Element, "/")
   165  }
   166  
   167  // upgradePath modernizes a Path by translating the contents of the Element field to Elem
   168  func upgradePath(path *pb.Path) *pb.Path {
   169  	if path != nil && len(path.Elem) == 0 {
   170  		var elems []*pb.PathElem
   171  		for _, element := range path.Element {
   172  			n, keys, _ := parseElement(element)
   173  			elems = append(elems, &pb.PathElem{Name: n, Key: keys})
   174  		}
   175  		path.Elem = elems
   176  		path.Element = nil
   177  	}
   178  	return path
   179  }
   180  
   181  // JoinPaths joins multiple gnmi paths into a single path
   182  func JoinPaths(paths ...*pb.Path) *pb.Path {
   183  	var elems []*pb.PathElem
   184  	for _, path := range paths {
   185  		if path != nil {
   186  			path = upgradePath(path)
   187  			elems = append(elems, path.Elem...)
   188  		}
   189  	}
   190  	return &pb.Path{Elem: elems}
   191  }
   192  
   193  // ParseGNMIElements builds up a gnmi path, from user-supplied text
   194  func ParseGNMIElements(elms []string) (*pb.Path, error) {
   195  	var parsed []*pb.PathElem
   196  	for _, e := range elms {
   197  		n, keys, err := parseElement(e)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		parsed = append(parsed, &pb.PathElem{Name: n, Key: keys})
   202  	}
   203  	return &pb.Path{
   204  		Element: elms, // Backwards compatibility with pre-v0.4 gnmi
   205  		Elem:    parsed,
   206  	}, nil
   207  }
   208  
   209  // parseElement parses a path element, according to the gNMI specification. See
   210  // https://github.com/openconfig/reference/blame/master/rpc/gnmi/gnmi-path-conventions.md
   211  //
   212  // It returns the first string (the current element name), and an optional map of key name
   213  // value pairs.
   214  func parseElement(pathElement string) (string, map[string]string, error) {
   215  	// First check if there are any keys, i.e. do we have at least one '[' in the element
   216  	name, keyStart := findUnescaped(pathElement, '[')
   217  	if keyStart < 0 {
   218  		return name, nil, nil
   219  	}
   220  
   221  	// Error if there is no element name or if the "[" is at the beginning of the path element
   222  	if len(name) == 0 {
   223  		return "", nil, fmt.Errorf("failed to find element name in %q", pathElement)
   224  	}
   225  
   226  	keys, err := ParseKeys(pathElement[keyStart:])
   227  	if err != nil {
   228  		return "", nil, err
   229  	}
   230  	return name, keys, nil
   231  
   232  }
   233  
   234  // ParseKeys parses just the keys portion of the stringified elem and returns the map of stringified
   235  // keys.
   236  func ParseKeys(keyPart string) (map[string]string, error) {
   237  	// Look at the keys now.
   238  	keys := make(map[string]string)
   239  	for keyPart != "" {
   240  		k, v, nextKey, err := parseKey(keyPart)
   241  		if err != nil {
   242  			return nil, err
   243  		}
   244  		keys[k] = v
   245  		keyPart = nextKey
   246  	}
   247  	return keys, nil
   248  }
   249  
   250  // parseKey returns the key name, key value and the remaining string to be parsed,
   251  func parseKey(s string) (string, string, string, error) {
   252  	if s[0] != '[' {
   253  		return "", "", "", fmt.Errorf("failed to find opening '[' in %q", s)
   254  	}
   255  	k, iEq := findUnescaped(s[1:], '=')
   256  	if iEq < 0 {
   257  		return "", "", "", fmt.Errorf("failed to find '=' in %q", s)
   258  	}
   259  
   260  	rhs := s[1+iEq+1:]
   261  	v, iClosBr := findUnescaped(rhs, ']')
   262  	if iClosBr < 0 {
   263  		return "", "", "", fmt.Errorf("failed to find ']' in %q", s)
   264  	}
   265  
   266  	next := rhs[iClosBr+1:]
   267  	return k, v, next, nil
   268  }
   269  
   270  // findUnescaped will return the index of the first unescaped match of 'find', and the unescaped
   271  // string leading up to it.
   272  func findUnescaped(s string, find byte) (string, int) {
   273  	// Take a fast track if there are no escape sequences
   274  	if strings.IndexByte(s, '\\') == -1 {
   275  		i := strings.IndexByte(s, find)
   276  		if i < 0 {
   277  			return s, -1
   278  		}
   279  		return s[:i], i
   280  	}
   281  
   282  	// Find the first match, taking care of escaped chars.
   283  	var b strings.Builder
   284  	var i int
   285  	len := len(s)
   286  	for i = 0; i < len; {
   287  		ch := s[i]
   288  		if ch == find {
   289  			return b.String(), i
   290  		} else if ch == '\\' && i < len-1 {
   291  			i++
   292  			ch = s[i]
   293  		}
   294  		b.WriteByte(ch)
   295  		i++
   296  	}
   297  	return b.String(), -1
   298  }