Skip to content
Snippets Groups Projects
path.go 6.12 KiB
Newer Older
  • Learn to ignore specific revisions
  • // Copyright (c) 2017 Arista Networks, Inc.
    // Use of this source code is governed by the Apache License 2.0
    // that can be found in the COPYING file.
    
    package gnmi
    
    import (
    	"fmt"
    	"sort"
    	"strings"
    
    	pb "github.com/openconfig/gnmi/proto/gnmi"
    )
    
    // nextTokenIndex returns the end index of the first token.
    func nextTokenIndex(path string) int {
    	var inBrackets bool
    	var escape bool
    	for i, c := range path {
    		switch c {
    		case '[':
    			inBrackets = true
    			escape = false
    		case ']':
    			if !escape {
    				inBrackets = false
    			}
    			escape = false
    		case '\\':
    			escape = !escape
    		case '/':
    			if !inBrackets && !escape {
    				return i
    			}
    			escape = false
    		default:
    			escape = false
    		}
    	}
    	return len(path)
    }
    
    // SplitPath splits a gnmi path according to the spec. See
    // https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-path-conventions.md
    // No validation is done. Behavior is undefined if path is an invalid
    // gnmi path. TODO: Do validation?
    func SplitPath(path string) []string {
    	var result []string
    	if len(path) > 0 && path[0] == '/' {
    		path = path[1:]
    	}
    	for len(path) > 0 {
    		i := nextTokenIndex(path)
    		result = append(result, path[:i])
    		path = path[i:]
    		if len(path) > 0 && path[0] == '/' {
    			path = path[1:]
    		}
    	}
    	return result
    }
    
    // SplitPaths splits multiple gnmi paths
    func SplitPaths(paths []string) [][]string {
    	out := make([][]string, len(paths))
    	for i, path := range paths {
    		out[i] = SplitPath(path)
    	}
    	return out
    }
    
    // StrPath builds a human-readable form of a gnmi path.
    // e.g. /a/b/c[e=f]
    func StrPath(path *pb.Path) string {
    	if path == nil {
    		return "/"
    	} else if len(path.Elem) != 0 {
    		return strPathV04(path)
    	} else if len(path.Element) != 0 {
    		return strPathV03(path)
    	}
    	return "/"
    }
    
    // strPathV04 handles the v0.4 gnmi and later path.Elem member.
    func strPathV04(path *pb.Path) string {
    	b := &strings.Builder{}
    	for _, elm := range path.Elem {
    		b.WriteRune('/')
    		writeSafeString(b, elm.Name, '/')
    		if len(elm.Key) > 0 {
    			// Sort the keys so that they print in a conistent
    			// order. We don't have the YANG AST information, so the
    			// best we can do is sort them alphabetically.
    			keys := make([]string, 0, len(elm.Key))
    			for k := range elm.Key {
    				keys = append(keys, k)
    			}
    			sort.Strings(keys)
    			for _, k := range keys {
    				b.WriteRune('[')
    				b.WriteString(k)
    				b.WriteRune('=')
    				writeSafeString(b, elm.Key[k], ']')
    				b.WriteRune(']')
    			}
    		}
    	}
    	return b.String()
    }
    
    // strPathV03 handles the v0.3 gnmi and earlier path.Element member.
    func strPathV03(path *pb.Path) string {
    	return "/" + strings.Join(path.Element, "/")
    }
    
    // upgradePath modernizes a Path by translating the contents of the Element field to Elem
    func upgradePath(path *pb.Path) *pb.Path {
    	if len(path.Elem) == 0 {
    		var elems []*pb.PathElem
    		for _, element := range path.Element {
    			n, keys, _ := parseElement(element)
    			elems = append(elems, &pb.PathElem{Name: n, Key: keys})
    		}
    		path.Elem = elems
    		path.Element = nil
    	}
    	return path
    }
    
    // JoinPaths joins multiple gnmi paths and returns a string representation
    func JoinPaths(paths ...*pb.Path) *pb.Path {
    	var elems []*pb.PathElem
    	for _, path := range paths {
    		path = upgradePath(path)
    		elems = append(elems, path.Elem...)
    	}
    	return &pb.Path{Elem: elems}
    }
    
    func writeSafeString(b *strings.Builder, s string, esc rune) {
    	for _, c := range s {
    		if c == esc || c == '\\' {
    			b.WriteRune('\\')
    		}
    		b.WriteRune(c)
    	}
    }
    
    // ParseGNMIElements builds up a gnmi path, from user-supplied text
    func ParseGNMIElements(elms []string) (*pb.Path, error) {
    	var parsed []*pb.PathElem
    	for _, e := range elms {
    		n, keys, err := parseElement(e)
    		if err != nil {
    			return nil, err
    		}
    		parsed = append(parsed, &pb.PathElem{Name: n, Key: keys})
    	}
    	return &pb.Path{
    		Element: elms, // Backwards compatibility with pre-v0.4 gnmi
    		Elem:    parsed,
    	}, nil
    }
    
    // parseElement parses a path element, according to the gNMI specification. See
    // https://github.com/openconfig/reference/blame/master/rpc/gnmi/gnmi-path-conventions.md
    //
    // It returns the first string (the current element name), and an optional map of key name
    // value pairs.
    func parseElement(pathElement string) (string, map[string]string, error) {
    	// First check if there are any keys, i.e. do we have at least one '[' in the element
    	name, keyStart := findUnescaped(pathElement, '[')
    	if keyStart < 0 {
    		return name, nil, nil
    	}
    
    	// Error if there is no element name or if the "[" is at the beginning of the path element
    	if len(name) == 0 {
    		return "", nil, fmt.Errorf("failed to find element name in %q", pathElement)
    	}
    
    	// Look at the keys now.
    	keys := make(map[string]string)
    	keyPart := pathElement[keyStart:]
    	for keyPart != "" {
    		k, v, nextKey, err := parseKey(keyPart)
    		if err != nil {
    			return "", nil, err
    		}
    		keys[k] = v
    		keyPart = nextKey
    	}
    	return name, keys, nil
    }
    
    // parseKey returns the key name, key value and the remaining string to be parsed,
    func parseKey(s string) (string, string, string, error) {
    	if s[0] != '[' {
    		return "", "", "", fmt.Errorf("failed to find opening '[' in %q", s)
    	}
    	k, iEq := findUnescaped(s[1:], '=')
    	if iEq < 0 {
    		return "", "", "", fmt.Errorf("failed to find '=' in %q", s)
    	}
    	if k == "" {
    		return "", "", "", fmt.Errorf("failed to find key name in %q", s)
    	}
    
    	rhs := s[1+iEq+1:]
    	v, iClosBr := findUnescaped(rhs, ']')
    	if iClosBr < 0 {
    		return "", "", "", fmt.Errorf("failed to find ']' in %q", s)
    	}
    	if v == "" {
    		return "", "", "", fmt.Errorf("failed to find key value in %q", s)
    	}
    
    	next := rhs[iClosBr+1:]
    	return k, v, next, nil
    }
    
    // findUnescaped will return the index of the first unescaped match of 'find', and the unescaped
    // string leading up to it.
    func findUnescaped(s string, find byte) (string, int) {
    	// Take a fast track if there are no escape sequences
    	if strings.IndexByte(s, '\\') == -1 {
    		i := strings.IndexByte(s, find)
    		if i < 0 {
    			return s, -1
    		}
    		return s[:i], i
    	}
    
    	// Find the first match, taking care of escaped chars.
    	var b strings.Builder
    	var i int
    	len := len(s)
    	for i = 0; i < len; {
    		ch := s[i]
    		if ch == find {
    			return b.String(), i
    		} else if ch == '\\' && i < len-1 {
    			i++
    			ch = s[i]
    		}
    		b.WriteByte(ch)
    		i++
    	}
    	return b.String(), -1
    }