diff --git a/database/sake.go b/database/sake.go index c545369..b42ffaf 100644 --- a/database/sake.go +++ b/database/sake.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "wwfc/filter" "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" @@ -74,7 +75,7 @@ func parseSakeFieldsFromJson(fieldsJson []byte) (map[string]SakeField, error) { return fields, nil } -func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerIds []int32, tableId string, recordIds []int32, fields []string, filter string) ([]SakeRecord, error) { +func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerIds []int32, tableId string, recordIds []int32, fields []string, filterExpr string) ([]SakeRecord, error) { if fields == nil { fields = []string{} } @@ -85,7 +86,28 @@ func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerId recordIds = []int32{} } - rows, err := pool.Query(ctx, getSakeRecordsQuery, gameId, tableId, ownerIds, recordIds) + query := getSakeRecordsQuery + if filterExpr != "" { + tree, err := filter.Parse(filterExpr) + if err != nil { + return nil, err + } + + var filterQuery string + err = pool.AcquireFunc(ctx, func(conn *pgxpool.Conn) error { + filterQuery, err = createSqlFilter(conn.Conn().PgConn(), tree) + return err + }) + if err != nil { + return nil, err + } + + // This filter has been entirely rewritten by our filter code, + // based on the expression supplied by the user. This should be safe!!! + query += " AND (" + filterQuery + ")" + } + + rows, err := pool.Query(ctx, query, gameId, tableId, ownerIds, recordIds) if err != nil { return nil, err } diff --git a/database/sake_filter.go b/database/sake_filter.go new file mode 100644 index 0000000..bb210e4 --- /dev/null +++ b/database/sake_filter.go @@ -0,0 +1,189 @@ +package database + +import ( + "errors" + "strconv" + "strings" + "wwfc/filter" + + "github.com/jackc/pgconn" +) + +type expression struct { + ast *filter.TreeNode + query string + conn *pgconn.PgConn +} + +func createSqlFilter(conn *pgconn.PgConn, basenode *filter.TreeNode) (value string, err error) { + defer func() { + if str := recover(); str != nil { + value = "" + err = errors.New(str.(string)) + } + }() + + this := &expression{basenode, "", conn} + this.filterAppendRoot(basenode) + return "(" + this.query + ")", nil +} + +func (this *expression) filterAppendRoot(basenode *filter.TreeNode) { + for _, node := range basenode.Items() { + switch node.Value.Category() { + case filter.CatFunction: + this.filterSwitchFunction(node) + return + + case filter.CatValue: + this.filterAppendNode(node) + return + + case filter.CatOther: + this.filterSwitchOther(node) + return + } + } + panic("eval failed") +} + +func (this *expression) filterSwitchOther(node *filter.TreeNode) { + switch v1 := node.Value.(type) { + case *filter.GroupToken: + if v1.GroupType == "()" { + this.filterAppendRoot(node) + return + } + } + panic("invalid node " + node.String()) +} + +func (this *expression) filterSwitchFunction(node *filter.TreeNode) { + val1 := node.Value.(*filter.OperatorToken) + switch strings.ToLower(val1.Operator) { + case "=", "!=": + this.filterAppendOperator(strings.ToLower(val1.Operator), node.Items()) + + case ">", "<", ">=", "<=", "+", "-", "&", "|", "^", "<<", ">>": + this.filterAppendMathOperator(strings.ToLower(val1.Operator), node.Items()) + + case "and": + this.filterAppendAnd(node.Items()) + case "or": + this.filterAppendOr(node.Items()) + + default: + panic("function not supported: " + val1.Operator) + } + +} + +func (this *expression) filterAppendNode(node *filter.TreeNode) { + switch v := node.Value.(type) { + case *filter.NumberToken: + this.query += "'" + strconv.FormatInt(int64(int32(v.Value)), 10) + "'" + case *filter.IdentityToken: + this.filterAppendQueryValue(v) + case *filter.OperatorToken: + this.filterSwitchFunction(node) + case *filter.GroupToken: + if v.GroupType == "()" { + this.query += "(" + this.filterAppendRoot(node) + this.query += ")" + return + } + panic("unexpected grouping type '" + v.GroupType + "': " + node.String()) + case *filter.TextToken: + this.query += "(" + this.filterPushArg(v.Text) + ")::varchar" + + default: + panic("unexpected value: " + node.String()) + } +} + +func (this *expression) filterAppendAnd(args []*filter.TreeNode) { + cnt := len(args) + if cnt < 2 { + panic("operator missing arguments") + } + + this.query += "( " + this.filterAppendNode(args[0]) + this.query += " AND " + this.filterAppendNode(args[1]) + this.query += " )" +} + +func (this *expression) filterAppendOr(args []*filter.TreeNode) { + cnt := len(args) + if cnt < 2 { + panic("operator missing arguments") + } + + this.query += "( " + this.filterAppendNode(args[0]) + this.query += " OR " + this.filterAppendNode(args[1]) + this.query += " )" +} + +func (this *expression) filterAppendOperator(operator string, args []*filter.TreeNode) { + cnt := len(args) + if cnt != 2 { + panic("operator requires exactly 2 arguments") + } + this.query += "( " + this.filterAppendNode(args[0]) + this.query += " " + operator + " " + this.filterAppendNode(args[1]) + this.query += " )" +} + +func (this *expression) filterAppendMathOperator(operator string, args []*filter.TreeNode) { + cnt := len(args) + if cnt != 2 { + panic("operator requires exactly 2 arguments") + } + this.query += "( (" + this.filterAppendNode(args[0]) + this.query += ")::int " + operator + " (" + this.filterAppendNode(args[1]) + this.query += ")::int )" +} + +// Get a value from the record +func (this *expression) filterAppendQueryValue(token *filter.IdentityToken) { + if token.Name == "ownerid" { + this.query += "(owner_id)" + return + } + if token.Name == "recordid" { + this.query += "(record_id)" + return + } + if token.Name == "gameid" { + this.query += "(game_id)" + return + } + if token.Name == "tableid" { + this.query += "(table_id)" + return + } + + this.query += "(fields->" + this.filterPushArg(token.Name) + "->>'value')" + +} + +func (this *expression) filterPushArg(arg string) string { + // This is scary!!! + if this.conn == nil { + return `'` + strings.Replace(arg, "'", "''", -1) + `'` + } + + str, err := this.conn.EscapeString(arg) + if err != nil { + panic(err) + } + return `'` + str + `'` +} diff --git a/database/sake_filter_test.go b/database/sake_filter_test.go new file mode 100644 index 0000000..fa8cd68 --- /dev/null +++ b/database/sake_filter_test.go @@ -0,0 +1,35 @@ +package database + +import ( + "fmt" + "testing" + "wwfc/filter" +) + +func testGenerateFilter(t *testing.T, expression string) (string, error) { + tree, err := filter.Parse(expression) + if err != nil { + t.Error(err) + return "", err + } + + fmt.Printf("tree: %s\n", tree.String()) + + query, err := createSqlFilter(nil, tree) + if err != nil { + t.Error(err) + return "", err + } + + fmt.Printf("query: %s\n", query) + + return query, err +} + +func TestSakeFilter(t *testing.T) { + testGenerateFilter(t, "ownerid = 1") + testGenerateFilter(t, "course = 12 and gameid = 1687 and time < 195") + + // Random complex filter I made up + testGenerateFilter(t, "gameid = 1687 and (test = 'aaa' or (DROP = 100000) and ((((UPDATE != 4))))) or (1 = 2 + 7 & SELECT - 9)") +} diff --git a/serverbrowser/filter/eval.go b/filter/eval.go similarity index 100% rename from serverbrowser/filter/eval.go rename to filter/eval.go diff --git a/serverbrowser/filter/parse.go b/filter/parse.go similarity index 100% rename from serverbrowser/filter/parse.go rename to filter/parse.go diff --git a/serverbrowser/filter/parse_branches.go b/filter/parse_branches.go similarity index 100% rename from serverbrowser/filter/parse_branches.go rename to filter/parse_branches.go diff --git a/serverbrowser/filter/scanner.go b/filter/scanner.go similarity index 100% rename from serverbrowser/filter/scanner.go rename to filter/scanner.go diff --git a/serverbrowser/filter/scanner_helper.go b/filter/scanner_helper.go similarity index 100% rename from serverbrowser/filter/scanner_helper.go rename to filter/scanner_helper.go diff --git a/serverbrowser/filter/tokens.go b/filter/tokens.go similarity index 100% rename from serverbrowser/filter/tokens.go rename to filter/tokens.go diff --git a/serverbrowser/filter/treenode.go b/filter/treenode.go similarity index 100% rename from serverbrowser/filter/treenode.go rename to filter/treenode.go diff --git a/serverbrowser/filter.go b/serverbrowser/filter.go index e4b9b6d..90ac9c2 100644 --- a/serverbrowser/filter.go +++ b/serverbrowser/filter.go @@ -1,8 +1,8 @@ package serverbrowser import ( + "wwfc/filter" "wwfc/logging" - "wwfc/serverbrowser/filter" "github.com/logrusorgru/aurora/v3" ) diff --git a/serverbrowser/filter_test.go b/serverbrowser/filter_test.go index 90cc921..b0d2dc9 100644 --- a/serverbrowser/filter_test.go +++ b/serverbrowser/filter_test.go @@ -3,7 +3,7 @@ package serverbrowser import ( "fmt" "testing" - "wwfc/serverbrowser/filter" + "wwfc/filter" ) func parseFilter(t *testing.T, expression string) error {