laskoviymishka commented on code in PR #1404:
URL: https://github.com/apache/iceberg-go/pull/1404#discussion_r3536300881


##########
types.go:
##########
@@ -558,7 +564,21 @@ func (m *MapType) UnmarshalJSON(b []byte) error {
        return nil
 }
 
-func FixedTypeOf(n int) FixedType { return FixedType{len: n} }
+func validateFixedLength(length int) error {
+       if length <= 0 {
+               return fmt.Errorf("invalid fixed length %d: must be greater 
than 0", length)
+       }
+
+       return nil
+}
+
+func FixedTypeOf(n int) FixedType {
+       if err := validateFixedLength(n); err != nil {
+               panic(fmt.Errorf("%w: %w", ErrInvalidArgument, err))

Review Comment:
   I think there's a real crash hiding here. `FixedLiteral.Type()` at 
literals.go:1124 is `return FixedTypeOf(len(f))`, and `FixedLiteral` is an 
exported `[]byte` whose `UnmarshalBinary` happily accepts empty input. So a 
zero-length literal — the zero value, `FixedLiteral(nil)`, or anything from 
`UnmarshalBinary([]byte{})` — now panics inside `.Type()`, which has no error 
return, so callers can't catch it.
   
   This is the same failure #1405 fixed for `DecimalLiteral`. I'd mirror it: 
either change literals.go:1124 to `return FixedType{len: len(f)}` to bypass the 
validator, or make `UnmarshalBinary` reject empty data with an error so 
corruption gets caught through the normal channel (I'd lean toward the latter). 
Either way this needs handling before merge.



##########
types.go:
##########
@@ -558,7 +564,21 @@ func (m *MapType) UnmarshalJSON(b []byte) error {
        return nil
 }
 
-func FixedTypeOf(n int) FixedType { return FixedType{len: n} }
+func validateFixedLength(length int) error {
+       if length <= 0 {
+               return fmt.Errorf("invalid fixed length %d: must be greater 
than 0", length)
+       }
+
+       return nil
+}
+
+func FixedTypeOf(n int) FixedType {

Review Comment:
   Making a public constructor panic is a real API choice, and I'm not sure 
it's the one we want here. `table/arrow_utils.go:374` calls 
`iceberg.FixedTypeOf(dt.ByteWidth)` — `ByteWidth` can be `0` for a degenerate 
Arrow schema, so that path goes from silently producing a zero-width type to 
crashing, and it's not updated in this PR.
   
   I'd lean toward `FixedTypeOf(n int) (FixedType, error)`, or a 
`MustFixedTypeOf` alongside it à la `regexp.MustCompile`, so callers deriving 
`n` from external data have a channel to handle it. wdyt?



##########
types.go:
##########
@@ -189,7 +189,13 @@ func (t *typeIFace) UnmarshalJSON(b []byte) error {
                                        return fmt.Errorf("%w: %s", 
ErrInvalidTypeString, typename)
                                }
 
-                               n, _ := strconv.Atoi(matches[1])
+                               n, err := strconv.Atoi(matches[1])

Review Comment:
   Worth noting the regex `(\d+)` already guarantees digits, so this `err` 
branch is only reachable via integer overflow (e.g. 
`fixed[99999999999999999999]`) — which is real, and better than the pre-PR 
behavior of silently mapping to `0`, but it's untested and surfaces a raw Go 
parse error rather than a domain message.
   
   I'd add a `fixed[99999999999999999999]` case and wrap the error a bit more 
descriptively.



##########
types.go:
##########
@@ -189,7 +189,13 @@ func (t *typeIFace) UnmarshalJSON(b []byte) error {
                                        return fmt.Errorf("%w: %s", 
ErrInvalidTypeString, typename)
                                }
 
-                               n, _ := strconv.Atoi(matches[1])
+                               n, err := strconv.Atoi(matches[1])
+                               if err != nil {
+                                       return fmt.Errorf("%w: %s", 
ErrInvalidTypeString, err)
+                               }
+                               if err := validateFixedLength(n); err != nil {

Review Comment:
   I'm a little wary of gating this on the read path. The spec doesn't require 
`fixed` length > 0 — Java's `FixedType.ofLength` + `FIXED` regex, PyIceberg's 
`FixedType.__init__`, and iceberg-rust's `Fixed(u64)` all accept `fixed[0]` 
with no check. So after this, any table metadata written by one of those 
clients with a `fixed[0]` column fails to unmarshal here with 
`ErrInvalidTypeString`, i.e. an unreadable table.
   
   I'd keep the validation on the construction/write path but drop it from 
`UnmarshalJSON`, unless we've confirmed via the community that the spec intends 
to prohibit `fixed[0]`. Do we know either way?



##########
types.go:
##########
@@ -558,7 +564,21 @@ func (m *MapType) UnmarshalJSON(b []byte) error {
        return nil
 }
 
-func FixedTypeOf(n int) FixedType { return FixedType{len: n} }
+func validateFixedLength(length int) error {
+       if length <= 0 {
+               return fmt.Errorf("invalid fixed length %d: must be greater 
than 0", length)

Review Comment:
   Small thing while we're here: this message has its own colon, so wrapped it 
reads `invalid argument: invalid fixed length 0: must be greater than 0` — 
three colon segments. I'd flatten it to `fixed length must be greater than 0, 
got %d`.
   
   Also the `%w: %w` at line 577 is the only multi-`%w` in the package 
(everything in exprs.go uses a single `%w` sentinel + string detail), and since 
the inner error isn't a sentinel the second `%w` buys nothing for `errors.Is`. 
`panic(fmt.Errorf("%w: fixed length must be greater than 0, got %d", 
ErrInvalidArgument, n))` would keep it consistent.



##########
types_test.go:
##########
@@ -77,6 +77,39 @@ func TestFixedType(t *testing.T) {
        assert.Equal(t, "fixed[5]", typ.String())
        assert.True(t, typ.Equals(iceberg.FixedTypeOf(5)))
        assert.False(t, typ.Equals(iceberg.FixedTypeOf(6)))
+
+       t.Run("FixedTypeOf validates length", func(t *testing.T) {
+               assert.PanicsWithError(t, "invalid argument: invalid fixed 
length 0: must be greater than 0", func() {
+                       iceberg.FixedTypeOf(0)
+               })
+               assert.PanicsWithError(t, "invalid argument: invalid fixed 
length -1: must be greater than 0", func() {
+                       iceberg.FixedTypeOf(-1)
+               })
+       })
+}
+
+func TestFixedTypeInvalidParse(t *testing.T) {
+       tests := []struct {
+               name string
+               data string
+       }{
+               {
+                       name: "zero length",
+                       data: `{"id": 1, "name": "f", "type": "fixed[0]", 
"required": true}`,
+               },
+               {
+                       name: "invalid syntax",

Review Comment:
   `fixed[0]junk` fails the bracket regex and returns before ever reaching 
`validateFixedLength`, so this exercises the pre-existing path, not the new 
code, and the name is a bit misleading. I'd either rename it (it's really 
"malformed suffix") or swap in the overflow case from the comment above so 
we're actually covering the new branches.



##########
types_test.go:
##########
@@ -77,6 +77,39 @@ func TestFixedType(t *testing.T) {
        assert.Equal(t, "fixed[5]", typ.String())
        assert.True(t, typ.Equals(iceberg.FixedTypeOf(5)))
        assert.False(t, typ.Equals(iceberg.FixedTypeOf(6)))
+
+       t.Run("FixedTypeOf validates length", func(t *testing.T) {
+               assert.PanicsWithError(t, "invalid argument: invalid fixed 
length 0: must be greater than 0", func() {

Review Comment:
   Matching the exact panic string makes this brittle — any wording tweak (like 
the one above) breaks it. I'd assert on the sentinel instead: recover and 
`assert.ErrorIs(recovered, iceberg.ErrInvalidArgument)`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to