mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-20 15:31:14 -06:00
037c247b73
Avoid wrapping expressions in comprehensions if they are known
to be defined. References that can't be undefined does not need
to be wrapped in a comprehension for safety, and as this incurs
a runtime cost, we should avoid it if we can! The goal is of course
to make interpolated strings the most performant option of all!
We're not there yet, as the recursive eval implementation remains
slower than e.g. sprintf or concat. We'll get there next! This PR
ensures that there's at least no runtime penalty passed from compilation
by not rewriting:
- A reference to a rule with default assignment
- A reference to a "constant" rule — single definition, ground value (`pi := 3.14`)
- A plain variable reference (`x`, not `x.y`)
In the case of constant rules, we could go one step further next,
and skip even a local assignment in favor of adding their value
directly to the interpolation, e.g:
```rego
x := 1
r := $"{x} + 2"
```
Would simply be rewritten like:
```rego
internal.template_string(["1" " + 2"], __local0__)
```
But saving that for another day.
Signed-off-by: Anders Eknert <anders.eknert@apple.com>
817 lines
20 KiB
Go
817 lines
20 KiB
Go
// Copyright 2016 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 (
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/tchap/go-patricia/v2/patricia"
|
|
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/topdown/builtins"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
func builtinAnyPrefixMatch(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
a, b := operands[0].Value, operands[1].Value
|
|
|
|
var strs []string
|
|
switch a := a.(type) {
|
|
case ast.String:
|
|
strs = []string{string(a)}
|
|
case *ast.Array, ast.Set:
|
|
var err error
|
|
strs, err = builtins.StringSliceOperand(a, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return builtins.NewOperandTypeErr(1, a, "string", "set", "array")
|
|
}
|
|
|
|
var prefixes []string
|
|
switch b := b.(type) {
|
|
case ast.String:
|
|
prefixes = []string{string(b)}
|
|
case *ast.Array, ast.Set:
|
|
var err error
|
|
prefixes, err = builtins.StringSliceOperand(b, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return builtins.NewOperandTypeErr(2, b, "string", "set", "array")
|
|
}
|
|
|
|
return iter(ast.InternedTerm(anyStartsWithAny(strs, prefixes)))
|
|
}
|
|
|
|
func builtinAnySuffixMatch(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
a, b := operands[0].Value, operands[1].Value
|
|
|
|
var strsReversed []string
|
|
switch a := a.(type) {
|
|
case ast.String:
|
|
strsReversed = []string{reverseString(string(a))}
|
|
case *ast.Array, ast.Set:
|
|
strs, err := builtins.StringSliceOperand(a, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
strsReversed = make([]string, len(strs))
|
|
for i := range strs {
|
|
strsReversed[i] = reverseString(strs[i])
|
|
}
|
|
default:
|
|
return builtins.NewOperandTypeErr(1, a, "string", "set", "array")
|
|
}
|
|
|
|
var suffixesReversed []string
|
|
switch b := b.(type) {
|
|
case ast.String:
|
|
suffixesReversed = []string{reverseString(string(b))}
|
|
case *ast.Array, ast.Set:
|
|
suffixes, err := builtins.StringSliceOperand(b, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
suffixesReversed = make([]string, len(suffixes))
|
|
for i := range suffixes {
|
|
suffixesReversed[i] = reverseString(suffixes[i])
|
|
}
|
|
default:
|
|
return builtins.NewOperandTypeErr(2, b, "string", "set", "array")
|
|
}
|
|
|
|
return iter(ast.InternedTerm(anyStartsWithAny(strsReversed, suffixesReversed)))
|
|
}
|
|
|
|
func anyStartsWithAny(strs []string, prefixes []string) bool {
|
|
if len(strs) == 0 || len(prefixes) == 0 {
|
|
return false
|
|
}
|
|
if len(strs) == 1 && len(prefixes) == 1 {
|
|
return strings.HasPrefix(strs[0], prefixes[0])
|
|
}
|
|
|
|
trie := patricia.NewTrie()
|
|
for i := range strs {
|
|
trie.Insert([]byte(strs[i]), true)
|
|
}
|
|
|
|
for i := range prefixes {
|
|
if trie.MatchSubtree([]byte(prefixes[i])) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func builtinFormatInt(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
|
|
input, err := builtins.NumberOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
base, err := builtins.NumberOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var format string
|
|
switch base {
|
|
case ast.Number("2"):
|
|
format = "%b"
|
|
case ast.Number("8"):
|
|
format = "%o"
|
|
case ast.Number("10"):
|
|
if i, ok := input.Int(); ok {
|
|
return iter(ast.InternedIntegerString(i))
|
|
}
|
|
format = "%d"
|
|
case ast.Number("16"):
|
|
format = "%x"
|
|
default:
|
|
return builtins.NewOperandEnumErr(2, "2", "8", "10", "16")
|
|
}
|
|
|
|
f := builtins.NumberToFloat(input)
|
|
i, _ := f.Int(nil)
|
|
|
|
return iter(ast.InternedTerm(fmt.Sprintf(format, i)))
|
|
}
|
|
|
|
func builtinConcat(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
join, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// fast path for empty or single string array/set, allocates no memory
|
|
if term, ok := zeroOrOneStringTerm(operands[1].Value); ok {
|
|
return iter(term)
|
|
}
|
|
|
|
sb := newSink(ast.Concat.Name, 0, bctx.Cancel)
|
|
|
|
// NOTE(anderseknert):
|
|
// More or less Go's strings.Join implementation, but where we avoid
|
|
// creating an intermediate []string slice to pass to that function,
|
|
// as that's expensive (3.5x more space allocated). Instead we build
|
|
// the string directly using the sink to concatenate the string
|
|
// values from the array/set with the separator.
|
|
n := 0
|
|
switch b := operands[1].Value.(type) {
|
|
case *ast.Array:
|
|
l := b.Len()
|
|
for i := range l {
|
|
s, ok := b.Elem(i).Value.(ast.String)
|
|
if !ok {
|
|
return builtins.NewOperandElementErr(2, b, b.Elem(i).Value, "string")
|
|
}
|
|
n += len(s)
|
|
}
|
|
sep := string(join)
|
|
n += len(sep) * (l - 1)
|
|
sb.Grow(n)
|
|
if _, err := sb.WriteString(string(b.Elem(0).Value.(ast.String))); err != nil {
|
|
return err
|
|
}
|
|
if sep == "" {
|
|
for i := 1; i < l; i++ {
|
|
if _, err := sb.WriteString(string(b.Elem(i).Value.(ast.String))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if len(sep) == 1 {
|
|
// when the separator is a single byte, sb.WriteByte is substantially faster
|
|
bsep := sep[0]
|
|
for i := 1; i < l; i++ {
|
|
if err := sb.WriteByte(bsep); err != nil {
|
|
return err
|
|
}
|
|
if _, err := sb.WriteString(string(b.Elem(i).Value.(ast.String))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
// for longer separators, there is no such difference between WriteString and Write
|
|
for i := 1; i < l; i++ {
|
|
if _, err := sb.WriteString(sep); err != nil {
|
|
return err
|
|
}
|
|
if _, err := sb.WriteString(string(b.Elem(i).Value.(ast.String))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return iter(ast.InternedTerm(sb.String()))
|
|
case ast.Set:
|
|
for _, v := range b.Slice() {
|
|
s, ok := v.Value.(ast.String)
|
|
if !ok {
|
|
return builtins.NewOperandElementErr(2, b, v.Value, "string")
|
|
}
|
|
n += len(s)
|
|
}
|
|
sep := string(join)
|
|
l := b.Len()
|
|
n += len(sep) * (l - 1)
|
|
sb.Grow(n)
|
|
for i, v := range b.Slice() {
|
|
if _, err := sb.WriteString(string(v.Value.(ast.String))); err != nil {
|
|
return err
|
|
}
|
|
if i < l-1 {
|
|
if _, err := sb.WriteString(sep); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return iter(ast.InternedTerm(sb.String()))
|
|
}
|
|
|
|
return builtins.NewOperandTypeErr(2, operands[1].Value, "set", "array")
|
|
}
|
|
|
|
func zeroOrOneStringTerm(a ast.Value) (*ast.Term, bool) {
|
|
switch b := a.(type) {
|
|
case *ast.Array:
|
|
if b.Len() == 0 {
|
|
return ast.InternedEmptyString, true
|
|
}
|
|
if b.Len() == 1 {
|
|
e := b.Elem(0)
|
|
if _, ok := e.Value.(ast.String); ok {
|
|
return e, true
|
|
}
|
|
}
|
|
case ast.Set:
|
|
if b.Len() == 0 {
|
|
return ast.InternedEmptyString, true
|
|
}
|
|
if b.Len() == 1 {
|
|
e := b.Slice()[0]
|
|
if _, ok := e.Value.(ast.String); ok {
|
|
return e, true
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func runesEqual(a, b []rune) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i, v := range a {
|
|
if v != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func builtinIndexOf(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
base, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
search, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(string(search)) == 0 {
|
|
return errors.New("empty search character")
|
|
}
|
|
|
|
if isASCII(string(base)) && isASCII(string(search)) {
|
|
// this is a false positive in the indexAlloc rule that thinks
|
|
// we're converting byte arrays to strings
|
|
//nolint:gocritic
|
|
return iter(ast.InternedTerm(strings.Index(string(base), string(search))))
|
|
}
|
|
|
|
baseRunes := []rune(string(base))
|
|
searchRunes := []rune(string(search))
|
|
searchLen := len(searchRunes)
|
|
|
|
for i, r := range baseRunes {
|
|
if len(baseRunes) >= i+searchLen {
|
|
if r == searchRunes[0] && runesEqual(baseRunes[i:i+searchLen], searchRunes) {
|
|
return iter(ast.InternedTerm(i))
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return iter(ast.InternedTerm(-1))
|
|
}
|
|
|
|
func builtinIndexOfN(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
base, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
search, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(string(search)) == 0 {
|
|
return errors.New("empty search character")
|
|
}
|
|
|
|
baseRunes := []rune(string(base))
|
|
searchRunes := []rune(string(search))
|
|
searchLen := len(searchRunes)
|
|
|
|
var arr []*ast.Term
|
|
for i, r := range baseRunes {
|
|
if len(baseRunes) >= i+searchLen {
|
|
if r == searchRunes[0] && runesEqual(baseRunes[i:i+searchLen], searchRunes) {
|
|
arr = append(arr, ast.InternedTerm(i))
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return iter(ast.ArrayTerm(arr...))
|
|
}
|
|
|
|
func builtinSubstring(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
|
|
base, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
startIndex, err := builtins.IntOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
length, err := builtins.IntOperand(operands[2].Value, 3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if startIndex < 0 {
|
|
return errors.New("negative offset")
|
|
}
|
|
|
|
sbase := string(base)
|
|
if sbase == "" {
|
|
return iter(ast.InternedEmptyString)
|
|
}
|
|
|
|
// Optimized path for the likely common case of ASCII strings.
|
|
// This allocates less memory and runs in about 1/3 the time.
|
|
if isASCII(sbase) {
|
|
if startIndex >= len(sbase) {
|
|
return iter(ast.InternedEmptyString)
|
|
}
|
|
|
|
if length < 0 {
|
|
return iter(ast.InternedTerm(sbase[startIndex:]))
|
|
}
|
|
|
|
if startIndex == 0 && length >= len(sbase) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
upto := min(len(sbase), startIndex+length)
|
|
return iter(ast.InternedTerm(sbase[startIndex:upto]))
|
|
}
|
|
|
|
if startIndex == 0 && length >= utf8.RuneCountInString(sbase) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
runes := []rune(base)
|
|
|
|
if startIndex >= len(runes) {
|
|
return iter(ast.InternedEmptyString)
|
|
}
|
|
|
|
var s string
|
|
if length < 0 {
|
|
s = string(runes[startIndex:])
|
|
} else {
|
|
upto := min(len(runes), startIndex+length)
|
|
s = string(runes[startIndex:upto])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(s))
|
|
}
|
|
|
|
func isASCII(s string) bool {
|
|
for i := range len(s) {
|
|
if s[i] > unicode.MaxASCII {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func builtinContains(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
substr, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return iter(ast.InternedTerm(strings.Contains(string(s), string(substr))))
|
|
}
|
|
|
|
func builtinStringCount(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
substr, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
baseTerm := string(s)
|
|
searchTerm := string(substr)
|
|
count := strings.Count(baseTerm, searchTerm)
|
|
|
|
return iter(ast.InternedTerm(count))
|
|
}
|
|
|
|
func builtinStartsWith(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
prefix, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return iter(ast.InternedTerm(strings.HasPrefix(string(s), string(prefix))))
|
|
}
|
|
|
|
func builtinEndsWith(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
suffix, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return iter(ast.InternedTerm(strings.HasSuffix(string(s), string(suffix))))
|
|
}
|
|
|
|
func builtinLower(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
arg := string(s)
|
|
low := strings.ToLower(arg)
|
|
|
|
if arg == low {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(low))
|
|
}
|
|
|
|
func builtinUpper(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
arg := string(s)
|
|
upp := strings.ToUpper(arg)
|
|
|
|
if arg == upp {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(upp))
|
|
}
|
|
|
|
func builtinSplit(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
text, delim := string(s), string(d)
|
|
if !strings.Contains(text, delim) {
|
|
return iter(ast.ArrayTerm(operands[0]))
|
|
}
|
|
|
|
return iter(ast.ArrayTerm(util.SplitMap(text, delim, ast.InternedTerm)...))
|
|
}
|
|
|
|
func builtinReplace(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
old, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n, err := builtins.StringOperand(operands[2].Value, 3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sink := newSink(ast.Replace.Name, len(s), bctx.Cancel)
|
|
replacer := strings.NewReplacer(string(old), string(n))
|
|
if _, err := replacer.WriteString(sink, string(s)); err != nil {
|
|
return err
|
|
}
|
|
replaced := sink.String()
|
|
if replaced == string(s) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(replaced))
|
|
}
|
|
|
|
func builtinReplaceN(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
patterns, err := builtins.ObjectOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keys := util.SortedFunc(patterns.Keys(), ast.TermValueCompare)
|
|
pairs := make([]string, 0, len(keys)*2)
|
|
|
|
for _, k := range keys {
|
|
keyVal, ok := k.Value.(ast.String)
|
|
if !ok {
|
|
return builtins.NewOperandErr(1, "non-string key found in pattern object")
|
|
}
|
|
strVal, ok := patterns.Get(k).Value.(ast.String)
|
|
if !ok {
|
|
return builtins.NewOperandErr(1, "non-string value found in pattern object")
|
|
}
|
|
pairs = append(pairs, string(keyVal), string(strVal))
|
|
}
|
|
|
|
sink := newSink(ast.ReplaceN.Name, len(s), bctx.Cancel)
|
|
replacer := strings.NewReplacer(pairs...)
|
|
if _, err := replacer.WriteString(sink, string(s)); err != nil {
|
|
return err
|
|
}
|
|
return iter(ast.InternedTerm(sink.String()))
|
|
}
|
|
|
|
func builtinTrim(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := string(s)
|
|
trimmed := strings.Trim(str, string(c))
|
|
if trimmed == str {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(trimmed))
|
|
}
|
|
|
|
func builtinTrimLeft(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trimmed := strings.TrimLeft(string(s), string(c))
|
|
if trimmed == string(s) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(trimmed))
|
|
}
|
|
|
|
func builtinTrimPrefix(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pre, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trimmed := strings.TrimPrefix(string(s), string(pre))
|
|
if trimmed == string(s) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(trimmed))
|
|
}
|
|
|
|
func builtinTrimRight(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trimmed := strings.TrimRight(string(s), string(c))
|
|
if trimmed == string(s) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(trimmed))
|
|
}
|
|
|
|
func builtinTrimSuffix(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
suf, err := builtins.StringOperand(operands[1].Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trimmed := strings.TrimSuffix(string(s), string(suf))
|
|
if trimmed == string(s) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(trimmed))
|
|
}
|
|
|
|
func builtinTrimSpace(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trimmed := strings.TrimSpace(string(s))
|
|
if trimmed == string(s) {
|
|
return iter(operands[0])
|
|
}
|
|
|
|
return iter(ast.InternedTerm(trimmed))
|
|
}
|
|
|
|
func builtinSprintf(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
astArr, ok := operands[1].Value.(*ast.Array)
|
|
if !ok {
|
|
return builtins.NewOperandTypeErr(2, operands[1].Value, "array")
|
|
}
|
|
|
|
// Optimized path for where sprintf is used as a "to_string" function for
|
|
// a single integer, i.e. sprintf("%d", [x]) where x is an integer.
|
|
if s == "%d" && astArr.Len() == 1 {
|
|
if n, ok := astArr.Elem(0).Value.(ast.Number); ok {
|
|
if i, ok := n.Int(); ok {
|
|
if interned := ast.InternedIntegerString(i); interned != nil {
|
|
return iter(interned)
|
|
}
|
|
return iter(ast.StringTerm(strconv.Itoa(i)))
|
|
}
|
|
}
|
|
}
|
|
|
|
args := make([]any, astArr.Len())
|
|
|
|
for i := range args {
|
|
switch v := astArr.Elem(i).Value.(type) {
|
|
case ast.Number:
|
|
if n, ok := v.Int(); ok {
|
|
args[i] = n
|
|
} else if b, ok := new(big.Int).SetString(v.String(), 10); ok {
|
|
args[i] = b
|
|
} else if f, ok := v.Float64(); ok {
|
|
args[i] = f
|
|
} else {
|
|
args[i] = v.String()
|
|
}
|
|
case ast.String:
|
|
args[i] = string(v)
|
|
default:
|
|
args[i] = astArr.Elem(i).String()
|
|
}
|
|
}
|
|
|
|
return iter(ast.InternedTerm(fmt.Sprintf(string(s), args...)))
|
|
}
|
|
|
|
func builtinReverse(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
s, err := builtins.StringOperand(operands[0].Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return iter(ast.InternedTerm(reverseString(string(s))))
|
|
}
|
|
|
|
func reverseString(str string) string {
|
|
var buf []byte
|
|
var arr [255]byte
|
|
size := len(str)
|
|
|
|
if size < 255 {
|
|
buf = arr[:size:size]
|
|
} else {
|
|
buf = make([]byte, size)
|
|
}
|
|
|
|
for start := 0; start < size; {
|
|
r, n := utf8.DecodeRuneInString(str[start:])
|
|
start += n
|
|
utf8.EncodeRune(buf[size-start:], r)
|
|
}
|
|
|
|
return string(buf)
|
|
}
|
|
|
|
func init() {
|
|
RegisterBuiltinFunc(ast.FormatInt.Name, builtinFormatInt)
|
|
RegisterBuiltinFunc(ast.Concat.Name, builtinConcat)
|
|
RegisterBuiltinFunc(ast.IndexOf.Name, builtinIndexOf)
|
|
RegisterBuiltinFunc(ast.IndexOfN.Name, builtinIndexOfN)
|
|
RegisterBuiltinFunc(ast.Substring.Name, builtinSubstring)
|
|
RegisterBuiltinFunc(ast.Contains.Name, builtinContains)
|
|
RegisterBuiltinFunc(ast.StringCount.Name, builtinStringCount)
|
|
RegisterBuiltinFunc(ast.StartsWith.Name, builtinStartsWith)
|
|
RegisterBuiltinFunc(ast.EndsWith.Name, builtinEndsWith)
|
|
RegisterBuiltinFunc(ast.Upper.Name, builtinUpper)
|
|
RegisterBuiltinFunc(ast.Lower.Name, builtinLower)
|
|
RegisterBuiltinFunc(ast.Split.Name, builtinSplit)
|
|
RegisterBuiltinFunc(ast.Replace.Name, builtinReplace)
|
|
RegisterBuiltinFunc(ast.ReplaceN.Name, builtinReplaceN)
|
|
RegisterBuiltinFunc(ast.Trim.Name, builtinTrim)
|
|
RegisterBuiltinFunc(ast.TrimLeft.Name, builtinTrimLeft)
|
|
RegisterBuiltinFunc(ast.TrimPrefix.Name, builtinTrimPrefix)
|
|
RegisterBuiltinFunc(ast.TrimRight.Name, builtinTrimRight)
|
|
RegisterBuiltinFunc(ast.TrimSuffix.Name, builtinTrimSuffix)
|
|
RegisterBuiltinFunc(ast.TrimSpace.Name, builtinTrimSpace)
|
|
RegisterBuiltinFunc(ast.Sprintf.Name, builtinSprintf)
|
|
RegisterBuiltinFunc(ast.AnyPrefixMatch.Name, builtinAnyPrefixMatch)
|
|
RegisterBuiltinFunc(ast.AnySuffixMatch.Name, builtinAnySuffixMatch)
|
|
RegisterBuiltinFunc(ast.StringReverse.Name, builtinReverse)
|
|
}
|