wfc-server/serverbrowser/filter/eval.go
Palapeli f1741b0718
Some checks are pending
Build CI / build (push) Waiting to run
ServerBrowser/Filter: Return result of group node
2025-03-05 11:24:27 -05:00

399 lines
8.6 KiB
Go

// Modified from github.com/zdebeer99/goexpression
package filter
import (
"errors"
"regexp"
"strconv"
"strings"
)
type expression struct {
ast *TreeNode
context map[string]string
queryGame string
}
// Bug(zdebeer): functions is eval from right to left instead from left to right.
func Eval(basenode *TreeNode, context map[string]string, queryGame string) (value int64, err error) {
defer func() {
if str := recover(); str != nil {
value = 0
err = errors.New(str.(string))
}
}()
this := &expression{basenode, context, queryGame}
return this.eval(basenode), nil
}
func (this *expression) eval(basenode *TreeNode) int64 {
for _, node := range basenode.items {
switch node.Value.Category() {
case CatFunction:
return this.switchFunction(node)
case CatValue:
return this.getNumber(node)
case CatOther:
return this.switchOther(node)
}
}
panic("eval failed")
}
func (this *expression) switchOther(node *TreeNode) int64 {
switch v1 := node.Value.(type) {
case *GroupToken:
if v1.GroupType == "()" {
return this.eval(node)
}
}
panic("invalid node " + node.String())
}
func (this *expression) switchFunction(node *TreeNode) int64 {
val1 := node.Value.(*OperatorToken)
switch strings.ToLower(val1.Operator) {
case "=":
return this.evalEquals(node.Items())
case "==":
return this.evalEquals(node.Items())
case "!=":
return this.evalNotEquals(node.Items())
case ">":
return this.evalMathOperator(this.evalMathGreater, node.Items())
case "<":
return this.evalMathOperator(this.evalMathLess, node.Items())
case ">=":
return this.evalMathOperator(this.evalMathGreaterOrEqual, node.Items())
case "<=":
return this.evalMathOperator(this.evalMathLessOrEqual, node.Items())
case "+":
return this.evalMathOperator(this.evalMathPlus, node.Items())
case "-":
return this.evalMathOperator(this.evalMathMinus, node.Items())
case "&":
return this.evalMathOperator(this.evalMathAnd, node.Items())
case "|":
return this.evalMathOperator(this.evalMathOr, node.Items())
case "^":
return this.evalMathOperator(this.evalMathXor, node.Items())
case "<<":
return this.evalMathOperator(this.evalMathLShift, node.Items())
case ">>":
return this.evalMathOperator(this.evalMathRShift, node.Items())
case "and":
return this.evalAnd(node.Items())
case "or":
return this.evalOr(node.Items())
case "&&":
return this.evalAnd(node.Items())
case "||":
return this.evalOr(node.Items())
case "like":
return this.evalLike(node.Items())
default:
panic("function not supported: " + val1.Operator)
}
}
func (this *expression) getString(node *TreeNode) string {
switch v := node.Value.(type) {
case *NumberToken:
return strconv.FormatInt(v.Value, 10)
case *IdentityToken:
return this.getValue(v)
case *OperatorToken:
return strconv.FormatInt(this.switchFunction(node), 10)
case *GroupToken:
if v.GroupType == "()" {
return strconv.FormatInt(this.eval(node), 10)
}
panic("unexpected grouping type: " + node.String())
case *TextToken:
return node.Value.(*TextToken).Text
default:
panic("unexpected value: " + node.String())
}
}
func (this *expression) evalEquals(args []*TreeNode) int64 {
cnt := len(args)
switch {
case cnt < 2:
panic("operator missing arguments")
case cnt == 2:
if n, ok := args[0].Value.(*IdentityToken); ok {
if n.Name == "rk" && this.queryGame == "mariokartwii" {
return this.evalEqualsRK(this.getString(args[1]))
}
}
if this.getString(args[0]) == this.getString(args[1]) {
return 1
}
return 0
default:
arg := this.getString(args[0])
for i := 1; i < cnt; i++ {
if arg != this.getString(args[i]) {
return 0
}
}
return 1
}
}
// Operator override
func (this *expression) evalEqualsRK(value string) int64 {
rk := this.context["rk"]
// Check and remove regional searches due to the limited player count
// China (ID 6) gets a pass because it was never released
if len(rk) == 4 && (strings.HasPrefix(rk, "vs_") || strings.HasPrefix(rk, "bt_")) && rk[3] >= '0' && rk[3] < '6' {
rk = rk[:2]
}
if len(value) == 4 && (strings.HasPrefix(value, "vs_") || strings.HasPrefix(value, "bt_")) && value[3] >= '0' && value[3] < '6' {
value = value[:2]
}
if rk == value {
return 1
}
return 0
}
func (this *expression) evalNotEquals(args []*TreeNode) int64 {
cnt := len(args)
switch {
case cnt < 2:
panic("operator missing arguments")
case cnt == 2:
if this.getString(args[0]) != this.getString(args[1]) {
return 1
}
return 0
default:
arg := this.getString(args[0])
for i := 1; i < cnt; i++ {
if arg == this.getString(args[i]) {
return 0
}
}
return 1
}
}
func (this *expression) evalAnd(args []*TreeNode) int64 {
cnt := len(args)
if cnt < 2 {
panic("operator missing arguments")
}
for i := 0; i < cnt; i++ {
if this.getString(args[i]) == "0" {
return 0
}
}
return 1
}
func (this *expression) evalOr(args []*TreeNode) int64 {
cnt := len(args)
if cnt < 2 {
panic("operator missing arguments")
}
for i := 0; i < cnt; i++ {
if this.getString(args[i]) != "0" {
return 1
}
}
return 0
}
func (this *expression) getNumber(node *TreeNode) int64 {
switch v := node.Value.(type) {
case *NumberToken:
return v.Value
case *IdentityToken:
r1 := this.getValue(v)
return this.toInt64(r1)
case *OperatorToken:
return this.switchFunction(node)
case *GroupToken:
if v.GroupType == "()" {
return this.eval(node)
}
panic("unexpected grouping type: " + node.String())
case *TextToken:
return this.toInt64(node.Value.(*TextToken).Text)
default:
panic("unexpected value: " + node.String())
}
}
func (this *expression) evalMathOperator(fn func(int64, int64) int64, args []*TreeNode) int64 {
cnt := len(args)
switch {
case cnt < 2:
panic("operator missing arguments")
case cnt == 2:
if n, ok := args[0].Value.(*IdentityToken); ok {
// Remove VR search due to the limited player count
if (n.Name == "ev" || n.Name == "eb") && this.queryGame == "mariokartwii" {
return 1
}
}
return fn(this.getNumber(args[0]), this.getNumber(args[1]))
default:
answ := fn(this.getNumber(args[0]), this.getNumber(args[1]))
for i := 2; i < cnt; i++ {
answ = fn(answ, this.getNumber(args[i]))
}
return answ
}
}
func (this *expression) evalMathPlus(val1, val2 int64) int64 {
return val1 + val2
}
func (this *expression) evalMathMinus(val1, val2 int64) int64 {
return val1 - val2
}
func (this *expression) evalMathGreater(val1, val2 int64) int64 {
if val1 > val2 {
return 1
}
return 0
}
func (this *expression) evalMathLess(val1, val2 int64) int64 {
if val1 < val2 {
return 1
}
return 0
}
func (this *expression) evalMathGreaterOrEqual(val1, val2 int64) int64 {
if val1 >= val2 {
return 1
}
return 0
}
func (this *expression) evalMathLessOrEqual(val1, val2 int64) int64 {
if val1 <= val2 {
return 1
}
return 0
}
func (this *expression) evalMathAnd(val1, val2 int64) int64 {
return val1 & val2
}
func (this *expression) evalMathOr(val1, val2 int64) int64 {
return val1 | val2
}
func (this *expression) evalMathXor(val1, val2 int64) int64 {
return val1 ^ val2
}
func (this *expression) evalMathLShift(val1, val2 int64) int64 {
return val1 << val2
}
func (this *expression) evalMathRShift(val1, val2 int64) int64 {
return val1 >> val2
}
func (this *expression) evalLike(args []*TreeNode) int64 {
cnt := len(args)
switch {
case cnt < 2:
panic("operator missing arguments")
case cnt == 2:
return this.evalLikeSingle(args[0], args[1])
default:
panic("operator like does not support multiple arguments")
}
}
func (this *expression) evalLikeSingle(arg1, arg2 *TreeNode) int64 {
val1 := this.getString(arg1)
val2 := this.getString(arg2)
allowedCharacters := `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_%\`
regexString := "^"
// Convert SQL like pattern to regex
for i, c := range val2 {
if strings.IndexRune(allowedCharacters, c) < 0 {
panic("invalid character in like pattern: " + string(c))
}
if i != 0 && val2[i-1] == '\\' {
if c == '\\' {
regexString += "\\\\"
continue
}
regexString += string(c)
continue
}
switch c {
case '%':
regexString += ".*"
case '_':
regexString += "."
case '\\':
// Do nothing
default:
regexString += string(c)
}
}
regexString += "$"
if matched, err := regexp.MatchString(regexString, val1); err != nil {
panic(err)
} else if matched {
return 1
}
return 0
}
// Get a value from the context.
func (this *expression) getValue(token *IdentityToken) string {
return this.context[token.Name]
}
func (this *expression) toInt64(value string) int64 {
val, err := strconv.ParseInt(value, 10, 64)
if err != nil {
panic(err)
}
return val
}