// 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 ( "fmt" "regexp" "regexp/syntax" "sync" gintersect "github.com/yashtewari/glob-intersection" "github.com/open-policy-agent/opa/v1/ast" "github.com/open-policy-agent/opa/v1/topdown/builtins" ) const ( regexCacheMaxSize = 100 regexInterQueryValueCacheHits = "rego_builtin_regex_interquery_value_cache_hits" ) var ( regexpCacheLock = sync.RWMutex{} regexpCache = make(map[string]*regexp.Regexp) ) func builtinRegexIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { if s, err := builtins.StringOperand(operands[0].Value, 1); err == nil { if _, err = syntax.Parse(string(s), syntax.Perl); err == nil { return iter(ast.InternedTerm(true)) } } return iter(ast.InternedTerm(false)) } func builtinRegexMatch(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { s1, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } s2, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } re, err := getRegexp(bctx, string(s1)) if err != nil { return err } return iter(ast.InternedTerm(re.MatchString(string(s2)))) } func builtinRegexMatchTemplate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { pattern, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } match, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } start, err := builtins.StringOperand(operands[2].Value, 3) if err != nil { return err } end, err := builtins.StringOperand(operands[3].Value, 4) if err != nil { return err } if len(start) != 1 { return fmt.Errorf("start delimiter has to be exactly one character long but is %d long", len(start)) } if len(end) != 1 { return fmt.Errorf("end delimiter has to be exactly one character long but is %d long", len(start)) } re, err := getRegexpTemplate(string(pattern), string(start)[0], string(end)[0]) if err != nil { return err } return iter(ast.InternedTerm(re.MatchString(string(match)))) } func builtinRegexSplit(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { s1, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } s2, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } re, err := getRegexp(bctx, string(s1)) if err != nil { return err } elems := re.Split(string(s2), -1) arr := make([]*ast.Term, len(elems)) for i := range elems { arr[i] = ast.StringTerm(elems[i]) } return iter(ast.ArrayTerm(arr...)) } func getRegexp(bctx BuiltinContext, pat string) (*regexp.Regexp, error) { if bctx.InterQueryBuiltinValueCache != nil { // TODO: Use named cache var key ast.Value = ast.String(pat) val, ok := bctx.InterQueryBuiltinValueCache.Get(key) if ok { res, valid := val.(*regexp.Regexp) if !valid { // The cache key may exist for a different value type (eg. glob). // In this case, we calculate the regex and return the result w/o updating the cache. return regexp.Compile(pat) } bctx.Metrics.Counter(regexInterQueryValueCacheHits).Incr() return res, nil } re, err := regexp.Compile(pat) if err != nil { return nil, err } bctx.InterQueryBuiltinValueCache.Insert(key, re) return re, nil } regexpCacheLock.RLock() re, ok := regexpCache[pat] numCached := len(regexpCache) regexpCacheLock.RUnlock() if !ok { var err error re, err = regexp.Compile(pat) if err != nil { return nil, err } regexpCacheLock.Lock() if numCached >= regexCacheMaxSize { // Delete a (semi-)random key to make room for the new one. for k := range regexpCache { delete(regexpCache, k) break } } regexpCache[pat] = re regexpCacheLock.Unlock() } return re, nil } func getRegexpTemplate(pat string, delimStart, delimEnd byte) (*regexp.Regexp, error) { regexpCacheLock.RLock() re, ok := regexpCache[pat] regexpCacheLock.RUnlock() if !ok { var err error re, err = compileRegexTemplate(pat, delimStart, delimEnd) if err != nil { return nil, err } regexpCacheLock.Lock() regexpCache[pat] = re regexpCacheLock.Unlock() } return re, nil } func builtinGlobsMatch(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { s1, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } s2, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } ne, err := gintersect.NonEmpty(string(s1), string(s2)) if err != nil { return err } return iter(ast.InternedTerm(ne)) } func builtinRegexFind(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { s1, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } s2, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } n, err := builtins.IntOperand(operands[2].Value, 3) if err != nil { return err } re, err := getRegexp(bctx, string(s1)) if err != nil { return err } elems := re.FindAllString(string(s2), n) arr := make([]*ast.Term, len(elems)) for i := range elems { arr[i] = ast.StringTerm(elems[i]) } return iter(ast.ArrayTerm(arr...)) } func builtinRegexFindAllStringSubmatch(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { s1, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } s2, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } n, err := builtins.IntOperand(operands[2].Value, 3) if err != nil { return err } re, err := getRegexp(bctx, string(s1)) if err != nil { return err } matches := re.FindAllStringSubmatch(string(s2), n) outer := make([]*ast.Term, len(matches)) for i := range matches { inner := make([]*ast.Term, len(matches[i])) for j := range matches[i] { inner[j] = ast.StringTerm(matches[i][j]) } outer[i] = ast.ArrayTerm(inner...) } return iter(ast.ArrayTerm(outer...)) } func builtinRegexReplace(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { base, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } pattern, err := builtins.StringOperand(operands[1].Value, 2) if err != nil { return err } value, err := builtins.StringOperand(operands[2].Value, 3) if err != nil { return err } re, err := getRegexp(bctx, string(pattern)) if err != nil { return err } // If no cancellation context, use the fast path if bctx.Cancel == nil { res := re.ReplaceAllString(string(base), string(value)) if res == string(base) { return iter(operands[0]) } return iter(ast.InternedTerm(res)) } // Use sink writer for cancellation-aware replacement sink := newSink(ast.RegexReplace.Name, len(base), bctx.Cancel) src := []byte(base) repl := []byte(value) // Find all matches at once to preserve anchor behavior: replace("foo", "^[a-z]", "F") => "Foo" allMatches := re.FindAllSubmatchIndex(src, -1) lastEnd := 0 for _, match := range allMatches { if _, err := sink.Write(src[lastEnd:match[0]]); err != nil { return err } if _, err := sink.Write(re.Expand(nil, repl, src, match)); err != nil { return err } lastEnd = match[1] } if _, err := sink.Write(src[lastEnd:]); err != nil { return err } res := sink.String() if res == string(base) { return iter(operands[0]) } return iter(ast.InternedTerm(res)) } func init() { RegisterBuiltinFunc(ast.RegexIsValid.Name, builtinRegexIsValid) RegisterBuiltinFunc(ast.RegexMatch.Name, builtinRegexMatch) RegisterBuiltinFunc(ast.RegexMatchDeprecated.Name, builtinRegexMatch) RegisterBuiltinFunc(ast.RegexSplit.Name, builtinRegexSplit) RegisterBuiltinFunc(ast.GlobsMatch.Name, builtinGlobsMatch) RegisterBuiltinFunc(ast.RegexTemplateMatch.Name, builtinRegexMatchTemplate) RegisterBuiltinFunc(ast.RegexFind.Name, builtinRegexFind) RegisterBuiltinFunc(ast.RegexFindAllStringSubmatch.Name, builtinRegexFindAllStringSubmatch) RegisterBuiltinFunc(ast.RegexReplace.Name, builtinRegexReplace) }