// Copyright 2017 The OPA Authors. All rights reserved. // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. package topdown import ( "github.com/open-policy-agent/opa/v1/ast" ) func evalWalk(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { input := operands[0] if pathIsWildcard(operands) { // When the path assignment is a wildcard: walk(input, [_, value]) // we may skip the path construction entirely, and simply return // same pointer in each iteration. This is a *much* more efficient // path when only the values are needed. return walkNoPath(ast.ArrayTerm(ast.InternedEmptyArray, input), iter) } filter := getOutputPath(operands) return walk(filter, nil, input, iter) } func walk(filter, path *ast.Array, input *ast.Term, iter func(*ast.Term) error) error { if filter == nil || filter.Len() == 0 { if path == nil { if err := iter(ast.ArrayTerm(ast.NewTerm(ast.InternedEmptyArrayValue), input)); err != nil { return err } } else { // Shallow copy, as while the array is modified, the elements are not pathCopy := copyShallow(path) // TODO(ae): I'd *really* like these terms to be retrieved from a sync.Pool, and // returned after iter is called. However, all my atttempts to do this have failed // as there seems to be something holding on to these references after the call, // leading to modifications that entirely alter the results. Perhaps this is not // possible to do, but if it is,it would be a huge performance win. if err := iter(ast.ArrayTerm(ast.NewTerm(pathCopy), input)); err != nil { return err } } } if filter != nil && filter.Len() > 0 { key := filter.Elem(0) filter = filter.Slice(1, -1) if key.IsGround() { if term := input.Get(key); term != nil { return walk(filter, pathAppend(path, key), term, iter) } return nil } } switch v := input.Value.(type) { case *ast.Array: for i := range v.Len() { if err := walk(filter, pathAppend(path, ast.InternedTerm(i)), v.Elem(i), iter); err != nil { return err } } case ast.Object: for _, k := range v.Keys() { if err := walk(filter, pathAppend(path, k), v.Get(k), iter); err != nil { return err } } case ast.Set: for _, elem := range v.Slice() { if err := walk(filter, pathAppend(path, elem), elem, iter); err != nil { return err } } } return nil } func walkNoPath(input *ast.Term, iter func(*ast.Term) error) error { // Note: the path array is embedded in the input from the start here // in order to avoid an extra allocation per iteration. This leads to // a little convoluted code below in order to extract and set the value, // but since walk is commonly used to traverse large data structures, // the performance gain is worth it. if err := iter(input); err != nil { return err } inputArray := input.Value.(*ast.Array) value := inputArray.Get(ast.InternedTerm(1)).Value switch v := value.(type) { case ast.Object: for _, k := range v.Keys() { inputArray.Set(1, v.Get(k)) if err := walkNoPath(input, iter); err != nil { return err } } case *ast.Array: for i := range v.Len() { inputArray.Set(1, v.Elem(i)) if err := walkNoPath(input, iter); err != nil { return err } } case ast.Set: for _, elem := range v.Slice() { inputArray.Set(1, elem) if err := walkNoPath(input, iter); err != nil { return err } } } return nil } func pathAppend(path *ast.Array, key *ast.Term) *ast.Array { if path == nil { return ast.NewArray(key) } return path.Append(key) } func getOutputPath(operands []*ast.Term) *ast.Array { if len(operands) == 2 { if arr, ok := operands[1].Value.(*ast.Array); ok && arr.Len() == 2 { if path, ok := arr.Elem(0).Value.(*ast.Array); ok { return path } } } return nil } func pathIsWildcard(operands []*ast.Term) bool { if len(operands) == 2 { if arr, ok := operands[1].Value.(*ast.Array); ok && arr.Len() == 2 { if v, ok := arr.Elem(0).Value.(ast.Var); ok { return v.IsWildcard() } } } return false } func copyShallow(arr *ast.Array) *ast.Array { cpy := make([]*ast.Term, 0, arr.Len()) arr.Foreach(func(elem *ast.Term) { cpy = append(cpy, elem) }) return ast.NewArray(cpy...) } func init() { RegisterBuiltinFunc(ast.WalkBuiltin.Name, evalWalk) }