// 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 ( "bytes" "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 { buf := &bytes.Buffer{} for _, elm := range path.Elem { buf.WriteRune('/') writeSafeString(buf, 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 { buf.WriteRune('[') buf.WriteString(k) buf.WriteRune('=') writeSafeString(buf, elm.Key[k], ']') buf.WriteRune(']') } } } return buf.String() } // strPathV03 handles the v0.3 gnmi and earlier path.Element member. func strPathV03(path *pb.Path) string { return "/" + strings.Join(path.Element, "/") } func writeSafeString(buf *bytes.Buffer, s string, esc rune) { for _, c := range s { if c == esc || c == '\\' { buf.WriteRune('\\') } buf.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. buf := &bytes.Buffer{} var i int len := len(s) for i = 0; i < len; { ch := s[i] if ch == find { return buf.String(), i } else if ch == '\\' && i < len-1 { i++ ch = s[i] } buf.WriteByte(ch) i++ } return buf.String(), -1 }