path.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. package gnmi
  5. import (
  6. "bytes"
  7. "fmt"
  8. "sort"
  9. "strings"
  10. pb "github.com/openconfig/gnmi/proto/gnmi"
  11. )
  12. // nextTokenIndex returns the end index of the first token.
  13. func nextTokenIndex(path string) int {
  14. var inBrackets bool
  15. var escape bool
  16. for i, c := range path {
  17. switch c {
  18. case '[':
  19. inBrackets = true
  20. escape = false
  21. case ']':
  22. if !escape {
  23. inBrackets = false
  24. }
  25. escape = false
  26. case '\\':
  27. escape = !escape
  28. case '/':
  29. if !inBrackets && !escape {
  30. return i
  31. }
  32. escape = false
  33. default:
  34. escape = false
  35. }
  36. }
  37. return len(path)
  38. }
  39. // SplitPath splits a gnmi path according to the spec. See
  40. // https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-path-conventions.md
  41. // No validation is done. Behavior is undefined if path is an invalid
  42. // gnmi path. TODO: Do validation?
  43. func SplitPath(path string) []string {
  44. var result []string
  45. if len(path) > 0 && path[0] == '/' {
  46. path = path[1:]
  47. }
  48. for len(path) > 0 {
  49. i := nextTokenIndex(path)
  50. result = append(result, path[:i])
  51. path = path[i:]
  52. if len(path) > 0 && path[0] == '/' {
  53. path = path[1:]
  54. }
  55. }
  56. return result
  57. }
  58. // SplitPaths splits multiple gnmi paths
  59. func SplitPaths(paths []string) [][]string {
  60. out := make([][]string, len(paths))
  61. for i, path := range paths {
  62. out[i] = SplitPath(path)
  63. }
  64. return out
  65. }
  66. // StrPath builds a human-readable form of a gnmi path.
  67. // e.g. /a/b/c[e=f]
  68. func StrPath(path *pb.Path) string {
  69. if path == nil {
  70. return "/"
  71. } else if len(path.Elem) != 0 {
  72. return strPathV04(path)
  73. } else if len(path.Element) != 0 {
  74. return strPathV03(path)
  75. }
  76. return "/"
  77. }
  78. // strPathV04 handles the v0.4 gnmi and later path.Elem member.
  79. func strPathV04(path *pb.Path) string {
  80. buf := &bytes.Buffer{}
  81. for _, elm := range path.Elem {
  82. buf.WriteRune('/')
  83. writeSafeString(buf, elm.Name, '/')
  84. if len(elm.Key) > 0 {
  85. // Sort the keys so that they print in a conistent
  86. // order. We don't have the YANG AST information, so the
  87. // best we can do is sort them alphabetically.
  88. keys := make([]string, 0, len(elm.Key))
  89. for k := range elm.Key {
  90. keys = append(keys, k)
  91. }
  92. sort.Strings(keys)
  93. for _, k := range keys {
  94. buf.WriteRune('[')
  95. buf.WriteString(k)
  96. buf.WriteRune('=')
  97. writeSafeString(buf, elm.Key[k], ']')
  98. buf.WriteRune(']')
  99. }
  100. }
  101. }
  102. return buf.String()
  103. }
  104. // strPathV03 handles the v0.3 gnmi and earlier path.Element member.
  105. func strPathV03(path *pb.Path) string {
  106. return "/" + strings.Join(path.Element, "/")
  107. }
  108. func writeSafeString(buf *bytes.Buffer, s string, esc rune) {
  109. for _, c := range s {
  110. if c == esc || c == '\\' {
  111. buf.WriteRune('\\')
  112. }
  113. buf.WriteRune(c)
  114. }
  115. }
  116. // ParseGNMIElements builds up a gnmi path, from user-supplied text
  117. func ParseGNMIElements(elms []string) (*pb.Path, error) {
  118. if len(elms) == 1 && elms[0] == "cli" {
  119. return &pb.Path{
  120. Origin: "cli",
  121. }, nil
  122. }
  123. var parsed []*pb.PathElem
  124. for _, e := range elms {
  125. n, keys, err := parseElement(e)
  126. if err != nil {
  127. return nil, err
  128. }
  129. parsed = append(parsed, &pb.PathElem{Name: n, Key: keys})
  130. }
  131. return &pb.Path{
  132. Element: elms, // Backwards compatibility with pre-v0.4 gnmi
  133. Elem: parsed,
  134. }, nil
  135. }
  136. // parseElement parses a path element, according to the gNMI specification. See
  137. // https://github.com/openconfig/reference/blame/master/rpc/gnmi/gnmi-path-conventions.md
  138. //
  139. // It returns the first string (the current element name), and an optional map of key name
  140. // value pairs.
  141. func parseElement(pathElement string) (string, map[string]string, error) {
  142. // First check if there are any keys, i.e. do we have at least one '[' in the element
  143. name, keyStart := findUnescaped(pathElement, '[')
  144. if keyStart < 0 {
  145. return name, nil, nil
  146. }
  147. // Error if there is no element name or if the "[" is at the beginning of the path element
  148. if len(name) == 0 {
  149. return "", nil, fmt.Errorf("failed to find element name in %q", pathElement)
  150. }
  151. // Look at the keys now.
  152. keys := make(map[string]string)
  153. keyPart := pathElement[keyStart:]
  154. for keyPart != "" {
  155. k, v, nextKey, err := parseKey(keyPart)
  156. if err != nil {
  157. return "", nil, err
  158. }
  159. keys[k] = v
  160. keyPart = nextKey
  161. }
  162. return name, keys, nil
  163. }
  164. // parseKey returns the key name, key value and the remaining string to be parsed,
  165. func parseKey(s string) (string, string, string, error) {
  166. if s[0] != '[' {
  167. return "", "", "", fmt.Errorf("failed to find opening '[' in %q", s)
  168. }
  169. k, iEq := findUnescaped(s[1:], '=')
  170. if iEq < 0 {
  171. return "", "", "", fmt.Errorf("failed to find '=' in %q", s)
  172. }
  173. if k == "" {
  174. return "", "", "", fmt.Errorf("failed to find key name in %q", s)
  175. }
  176. rhs := s[1+iEq+1:]
  177. v, iClosBr := findUnescaped(rhs, ']')
  178. if iClosBr < 0 {
  179. return "", "", "", fmt.Errorf("failed to find ']' in %q", s)
  180. }
  181. if v == "" {
  182. return "", "", "", fmt.Errorf("failed to find key value in %q", s)
  183. }
  184. next := rhs[iClosBr+1:]
  185. return k, v, next, nil
  186. }
  187. // findUnescaped will return the index of the first unescaped match of 'find', and the unescaped
  188. // string leading up to it.
  189. func findUnescaped(s string, find byte) (string, int) {
  190. // Take a fast track if there are no escape sequences
  191. if strings.IndexByte(s, '\\') == -1 {
  192. i := strings.IndexByte(s, find)
  193. if i < 0 {
  194. return s, -1
  195. }
  196. return s[:i], i
  197. }
  198. // Find the first match, taking care of escaped chars.
  199. buf := &bytes.Buffer{}
  200. var i int
  201. len := len(s)
  202. for i = 0; i < len; {
  203. ch := s[i]
  204. if ch == find {
  205. return buf.String(), i
  206. } else if ch == '\\' && i < len-1 {
  207. i++
  208. ch = s[i]
  209. }
  210. buf.WriteByte(ch)
  211. i++
  212. }
  213. return buf.String(), -1
  214. }