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 }