package jsonpath import ( "strconv" "strings" json "github.com/nspcc-dev/go-ordered-json" ) type ( // pathTokenType represents a single JSONPath token. pathTokenType byte // pathParser combines a JSONPath and a position to start parsing from. pathParser struct { s string i int depth int } ) const ( pathInvalid pathTokenType = iota pathRoot pathDot pathLeftBracket pathRightBracket pathAsterisk pathComma pathColon pathIdentifier pathString pathNumber ) const ( maxNestingDepth = 6 maxObjects = 1024 ) // Get returns substructures of value selected by path. // The result is always non-nil unless the path is invalid. func Get(path string, value any) ([]any, bool) { if path == "" { return []any{value}, true } p := pathParser{ depth: maxNestingDepth, s: path, } typ, _ := p.nextToken() if typ != pathRoot { return nil, false } objs := []any{value} for p.i < len(p.s) { var ok bool switch typ, _ := p.nextToken(); typ { case pathDot: objs, ok = p.processDot(objs) case pathLeftBracket: objs, ok = p.processLeftBracket(objs) } if !ok || maxObjects < len(objs) { return nil, false } } if objs == nil { objs = []any{} } return objs, true } func (p *pathParser) nextToken() (pathTokenType, string) { var ( typ pathTokenType value string ok = true numRead = 1 ) if p.i >= len(p.s) { return pathInvalid, "" } switch c := p.s[p.i]; c { case '$': typ = pathRoot case '.': typ = pathDot case '[': typ = pathLeftBracket case ']': typ = pathRightBracket case '*': typ = pathAsterisk case ',': typ = pathComma case ':': typ = pathColon case '\'': typ = pathString value, numRead, ok = p.parseString() default: switch { case c == '_' || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'): typ = pathIdentifier value, numRead, ok = p.parseIdent() case c == '-' || ('0' <= c && c <= '9'): typ = pathNumber value, numRead, ok = p.parseNumber() default: return pathInvalid, "" } } if !ok { return pathInvalid, "" } p.i += numRead return typ, value } // parseString parses a JSON string surrounded by single quotes. // It returns the number of characters consumed and true on success. func (p *pathParser) parseString() (string, int, bool) { var end int for end = p.i + 1; end < len(p.s); end++ { if p.s[end] == '\'' { return p.s[p.i : end+1], end + 1 - p.i, true } } return "", 0, false } // parseIdent parses an alphanumeric identifier. // It returns the number of characters consumed and true on success. func (p *pathParser) parseIdent() (string, int, bool) { var end int for end = p.i + 1; end < len(p.s); end++ { c := p.s[end] if c != '_' && !('a' <= c && c <= 'z') && !('A' <= c && c <= 'Z') && !('0' <= c && c <= '9') { break } } return p.s[p.i:end], end - p.i, true } // parseNumber parses an integer number. // Only string representation is returned, size-checking is done on the first use. // It also returns the number of characters consumed and true on success. func (p *pathParser) parseNumber() (string, int, bool) { var end int for end = p.i + 1; end < len(p.s); end++ { c := p.s[end] if c < '0' || '9' < c { break } } return p.s[p.i:end], end - p.i, true } // processDot handles `.` operator. // It either descends 1 level down or performs recursive descent. func (p *pathParser) processDot(objs []any) ([]any, bool) { typ, value := p.nextToken() switch typ { case pathAsterisk: return p.descend(objs) case pathDot: return p.descendRecursive(objs) case pathIdentifier: return p.descendByIdent(objs, value) default: return nil, false } } // descend descends 1 level down. // It flattens arrays and returns map values for maps. func (p *pathParser) descend(objs []any) ([]any, bool) { if p.depth <= 0 { return nil, false } p.depth-- var values []any for i := range objs { switch obj := objs[i].(type) { case []any: if maxObjects < len(values)+len(obj) { return nil, false } values = append(values, obj...) case json.OrderedObject: if maxObjects < len(values)+len(obj) { return nil, false } for i := range obj { values = append(values, obj[i].Value) } } } return values, true } // descendRecursive performs recursive descent. func (p *pathParser) descendRecursive(objs []any) ([]any, bool) { typ, val := p.nextToken() if typ != pathIdentifier { return nil, false } var values []any for len(objs) > 0 { newObjs, _ := p.descendByIdentAux(objs, false, val) if maxObjects < len(values)+len(newObjs) { return nil, false } values = append(values, newObjs...) objs, _ = p.descend(objs) } return values, true } // descendByIdent performs map's field access by name. func (p *pathParser) descendByIdent(objs []any, names ...string) ([]any, bool) { return p.descendByIdentAux(objs, true, names...) } func (p *pathParser) descendByIdentAux(objs []any, checkDepth bool, names ...string) ([]any, bool) { if checkDepth { if p.depth <= 0 { return nil, false } p.depth-- } var values []any for i := range objs { obj, ok := objs[i].(json.OrderedObject) if !ok { continue } for j := range names { for k := range obj { if obj[k].Key == names[j] { if maxObjects < len(values)+1 { return nil, false } values = append(values, obj[k].Value) break } } } } return values, true } // descendByIndex performs array access by index. func (p *pathParser) descendByIndex(objs []any, indices ...int) ([]any, bool) { if p.depth <= 0 { return nil, false } p.depth-- var values []any for i := range objs { obj, ok := objs[i].([]any) if !ok { continue } for _, j := range indices { if j < 0 { j += len(obj) } if 0 <= j && j < len(obj) { if maxObjects < len(values)+1 { return nil, false } values = append(values, obj[j]) } } } return values, true } // processLeftBracket processes index expressions which can be either // array/map access, array sub-slice or union of indices. func (p *pathParser) processLeftBracket(objs []any) ([]any, bool) { typ, value := p.nextToken() switch typ { case pathAsterisk: typ, _ := p.nextToken() if typ != pathRightBracket { return nil, false } return p.descend(objs) case pathColon: return p.processSlice(objs, 0) case pathNumber: subTyp, _ := p.nextToken() switch subTyp { case pathColon: index, err := strconv.ParseInt(value, 10, 32) if err != nil { return nil, false } return p.processSlice(objs, int(index)) case pathComma: return p.processUnion(objs, pathNumber, value) case pathRightBracket: index, err := strconv.ParseInt(value, 10, 32) if err != nil { return nil, false } return p.descendByIndex(objs, int(index)) default: return nil, false } case pathString: subTyp, _ := p.nextToken() switch subTyp { case pathComma: return p.processUnion(objs, pathString, value) case pathRightBracket: s := strings.Trim(value, "'") err := json.Unmarshal([]byte(`"`+s+`"`), &s) if err != nil { return nil, false } return p.descendByIdent(objs, s) default: return nil, false } default: return nil, false } } // processUnion processes union of multiple indices. // firstTyp is assumed to be either pathNumber or pathString. func (p *pathParser) processUnion(objs []any, firstTyp pathTokenType, firstVal string) ([]any, bool) { items := []string{firstVal} for { typ, val := p.nextToken() if typ != firstTyp { return nil, false } items = append(items, val) typ, _ = p.nextToken() if typ == pathRightBracket { break } else if typ != pathComma { return nil, false } } switch firstTyp { case pathNumber: values := make([]int, len(items)) for i := range items { index, err := strconv.ParseInt(items[i], 10, 32) if err != nil { return nil, false } values[i] = int(index) } return p.descendByIndex(objs, values...) case pathString: for i := range items { s := strings.Trim(items[i], "'") err := json.Unmarshal([]byte(`"`+s+`"`), &items[i]) if err != nil { return nil, false } } return p.descendByIdent(objs, items...) default: panic("token in union must be either number or string") } } // processSlice processes a slice with the specified start index. func (p *pathParser) processSlice(objs []any, start int) ([]any, bool) { typ, val := p.nextToken() switch typ { case pathNumber: typ, _ := p.nextToken() if typ != pathRightBracket { return nil, false } index, err := strconv.ParseInt(val, 10, 32) if err != nil { return nil, false } return p.descendByRange(objs, start, int(index)) case pathRightBracket: return p.descendByRange(objs, start, 0) default: return nil, false } } // descendByRange is similar to descend but skips maps and returns sub-slices for arrays. func (p *pathParser) descendByRange(objs []any, start, end int) ([]any, bool) { if p.depth <= 0 { return nil, false } p.depth-- var values []any for i := range objs { arr, ok := objs[i].([]any) if !ok { continue } subStart := start if subStart < 0 { subStart += len(arr) } subEnd := end if subEnd <= 0 { subEnd += len(arr) } if subEnd > len(arr) { subEnd = len(arr) } if subEnd <= subStart { continue } if maxObjects < len(values)+subEnd-subStart { return nil, false } values = append(values, arr[subStart:subEnd]...) } return values, true }