From 24be4838143e399a16e98a34791c7da3fecf7f58 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 30 Jul 2026 21:37:46 +0200 Subject: [PATCH 1/2] fix(expr): reject trailing data after boolean expressions Signed-off-by: Minh Vu --- expr_json.go | 6 +++++- expr_json_test.go | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/expr_json.go b/expr_json.go index a81b5f4bd..d75ea2ddc 100644 --- a/expr_json.go +++ b/expr_json.go @@ -418,7 +418,11 @@ func decodeExpr(raw json.RawMessage, schema *Schema, caseSensitive bool) (Boolea if err != nil { return nil, fmt.Errorf("%w: cannot parse expression: %s", ErrInvalidArgument, err) } - if b, ok := tok.(bool); ok { + if _, ok := tok.(bool); ok { + var b bool + if err := json.Unmarshal(raw, &b); err != nil { + return nil, fmt.Errorf("%w: cannot parse expression: %s", ErrInvalidArgument, err) + } if b { return AlwaysTrue{}, nil } diff --git a/expr_json_test.go b/expr_json_test.go index b5841a4b8..1b9de31d6 100644 --- a/expr_json_test.go +++ b/expr_json_test.go @@ -393,6 +393,20 @@ func TestUnmarshalExpressionErrors(t *testing.T) { } } +func TestUnmarshalBooleanExpressionRejectsTrailingData(t *testing.T) { + for _, input := range []string{ + `true false`, + `false null`, + `true{}`, + `false garbage`, + } { + t.Run(input, func(t *testing.T) { + _, err := iceberg.ParseExpr([]byte(input), nil) + require.ErrorIs(t, err, iceberg.ErrInvalidArgument) + }) + } +} + // TestExpressionTransformTermRoundTrip covers a residual filter whose term is a // partition transform, e.g. a Java server's bucket[100](id) <= 50. func TestExpressionTransformTermRoundTrip(t *testing.T) { From a7a9d1c8d6d245fec98a5bef3a32e169ab5c21ee Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 30 Jul 2026 22:16:46 +0200 Subject: [PATCH 2/2] refactor(expr): validate boolean JSON in one pass --- expr_json.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/expr_json.go b/expr_json.go index d75ea2ddc..91cd8ed4e 100644 --- a/expr_json.go +++ b/expr_json.go @@ -21,7 +21,9 @@ import ( "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" + "io" "math" "sort" "strings" @@ -418,11 +420,7 @@ func decodeExpr(raw json.RawMessage, schema *Schema, caseSensitive bool) (Boolea if err != nil { return nil, fmt.Errorf("%w: cannot parse expression: %s", ErrInvalidArgument, err) } - if _, ok := tok.(bool); ok { - var b bool - if err := json.Unmarshal(raw, &b); err != nil { - return nil, fmt.Errorf("%w: cannot parse expression: %s", ErrInvalidArgument, err) - } + if b, ok := tok.(bool); ok { if b { return AlwaysTrue{}, nil } @@ -492,8 +490,21 @@ func decodeExpr(raw json.RawMessage, schema *Schema, caseSensitive bool) (Boolea // without inspecting bytes by hand. func firstToken(raw json.RawMessage) (json.Token, error) { dec := json.NewDecoder(bytes.NewReader(raw)) + tok, err := dec.Token() + if err != nil { + return nil, err + } + if _, ok := tok.(bool); ok { + if _, err := dec.Token(); !errors.Is(err, io.EOF) { + if err == nil { + return nil, errors.New("trailing data after boolean expression") + } + + return nil, err + } + } - return dec.Token() + return tok, nil } func decodePredicate(op Operation, node exprNode, schema *Schema, caseSensitive bool) (BooleanExpression, error) {