Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions driver/postgres/decode_range_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package postgres

import (
"encoding/binary"
"math"
"strconv"
"strings"
"testing"

"github.com/goceleris/celeris/driver/postgres/protocol"
)

// Regression tests for celeris#502: decodeTextInto / decodeBinaryInto
// narrowed the parsed int64 into int / int32 / int16 / uint32 without a
// range check, so an out-of-range value wrapped silently (2147483648 scanned
// into *int32 became -2147483648) instead of failing the scan the way the
// encode side ("int4 overflow") and database/sql do.

func int8Codec() *protocol.TypeCodec { return &protocol.TypeCodec{OID: protocol.OIDInt8} }

func beInt8(n int64) []byte {
var b [8]byte
binary.BigEndian.PutUint64(b[:], uint64(n))
return b[:]
}

func wantRangeErr(t *testing.T, handled bool, err error, want string) {
t.Helper()
if !handled {
t.Fatalf("destination type not handled by the fast path")
}
if err == nil {
t.Fatalf("expected out-of-range error for %s, got nil", want)
}
if !strings.Contains(err.Error(), "out of range") || !strings.Contains(err.Error(), want) {
t.Fatalf("error %q does not mention out of range for %s", err, want)
}
}

func TestDecodeTextInto_Int32Overflow(t *testing.T) {
var d int32 = 7
handled, err := decodeTextInto(&d, []byte("2147483648"), nil)
wantRangeErr(t, handled, err, "int32")
if d != 7 {
t.Fatalf("destination was written on error: %d", d)
}

d = 7
handled, err = decodeTextInto(&d, []byte("-2147483649"), nil)
wantRangeErr(t, handled, err, "int32")
if d != 7 {
t.Fatalf("destination was written on error: %d", d)
}
}

func TestDecodeTextInto_Int32Boundaries(t *testing.T) {
for _, want := range []int64{math.MinInt32, -1, 0, 1, math.MaxInt32} {
var d int32
handled, err := decodeTextInto(&d, []byte(strconv.FormatInt(want, 10)), nil)
if !handled || err != nil {
t.Fatalf("%d: handled=%v err=%v", want, handled, err)
}
if int64(d) != want {
t.Fatalf("%d: got %d", want, d)
}
}
}

func TestDecodeTextInto_IntAndInt64Boundaries(t *testing.T) {
for _, want := range []int64{math.MinInt64, math.MinInt, -1, 0, 1, math.MaxInt, math.MaxInt64} {
var d64 int64
handled, err := decodeTextInto(&d64, []byte(strconv.FormatInt(want, 10)), nil)
if !handled || err != nil || d64 != want {
t.Fatalf("int64 %d: handled=%v err=%v got=%d", want, handled, err, d64)
}
if want < math.MinInt || want > math.MaxInt {
continue // only reachable on 32-bit builds
}
var d int
handled, err = decodeTextInto(&d, []byte(strconv.FormatInt(want, 10)), nil)
if !handled || err != nil {
t.Fatalf("int %d: handled=%v err=%v", want, handled, err)
}
if int64(d) != want {
t.Fatalf("int %d: got %d", want, d)
}
}
}

func TestDecodeBinaryInto_Int32Overflow(t *testing.T) {
var d int32 = 7
handled, err := decodeBinaryInto(&d, beInt8(2147483648), int8Codec())
wantRangeErr(t, handled, err, "int32")
if d != 7 {
t.Fatalf("destination was written on error: %d", d)
}
handled, err = decodeBinaryInto(&d, beInt8(math.MinInt32-1), int8Codec())
wantRangeErr(t, handled, err, "int32")
}

func TestDecodeBinaryInto_Int16Overflow(t *testing.T) {
var d int16
handled, err := decodeBinaryInto(&d, beInt8(math.MaxInt16+1), int8Codec())
wantRangeErr(t, handled, err, "int16")
handled, err = decodeBinaryInto(&d, beInt8(math.MinInt16-1), int8Codec())
wantRangeErr(t, handled, err, "int16")
}

func TestDecodeBinaryInto_Uint32Overflow(t *testing.T) {
var d uint32
handled, err := decodeBinaryInto(&d, beInt8(-1), int8Codec())
wantRangeErr(t, handled, err, "uint32")
handled, err = decodeBinaryInto(&d, beInt8(math.MaxUint32+1), int8Codec())
wantRangeErr(t, handled, err, "uint32")
}

// scanValue is the driver.Value fallback path (codec-less / non-fast-path
// destinations); it narrowed int64 the same way.
func TestScanValue_IntNarrowingRange(t *testing.T) {
var d32 int32 = 7
if err := scanValue(&d32, int64(2147483648)); err == nil || !strings.Contains(err.Error(), "out of range for int32") {
t.Fatalf("int32: err=%v", err)
}
if d32 != 7 {
t.Fatalf("int32 destination written on error: %d", d32)
}
var d16 int16
if err := scanValue(&d16, int64(math.MinInt16-1)); err == nil || !strings.Contains(err.Error(), "out of range for int16") {
t.Fatalf("int16: err=%v", err)
}
if err := scanValue(&d32, int64(math.MaxInt32)); err != nil || d32 != math.MaxInt32 {
t.Fatalf("int32 boundary: err=%v got=%d", err, d32)
}
if err := scanValue(&d16, int64(math.MinInt16)); err != nil || d16 != math.MinInt16 {
t.Fatalf("int16 boundary: err=%v got=%d", err, d16)
}
var di int
if err := scanValue(&di, int64(math.MaxInt)); err != nil || di != math.MaxInt {
t.Fatalf("int boundary: err=%v got=%d", err, di)
}
}

func TestDecodeBinaryInto_Boundaries(t *testing.T) {
codec := int8Codec()
for _, want := range []int64{math.MinInt32, -1, 0, 1, math.MaxInt32} {
var d int32
if handled, err := decodeBinaryInto(&d, beInt8(want), codec); !handled || err != nil || int64(d) != want {
t.Fatalf("int32 %d: handled=%v err=%v got=%d", want, handled, err, d)
}
}
for _, want := range []int64{math.MinInt16, -1, 0, 1, math.MaxInt16} {
var d int16
if handled, err := decodeBinaryInto(&d, beInt8(want), codec); !handled || err != nil || int64(d) != want {
t.Fatalf("int16 %d: handled=%v err=%v got=%d", want, handled, err, d)
}
}
for _, want := range []int64{0, 1, math.MaxUint32} {
var d uint32
if handled, err := decodeBinaryInto(&d, beInt8(want), codec); !handled || err != nil || int64(d) != want {
t.Fatalf("uint32 %d: handled=%v err=%v got=%d", want, handled, err, d)
}
}
for _, want := range []int64{math.MinInt64, -1, 0, 1, math.MaxInt64} {
var d int64
if handled, err := decodeBinaryInto(&d, beInt8(want), codec); !handled || err != nil || d != want {
t.Fatalf("int64 %d: handled=%v err=%v got=%d", want, handled, err, d)
}
if want < math.MinInt || want > math.MaxInt {
continue
}
var di int
if handled, err := decodeBinaryInto(&di, beInt8(want), codec); !handled || err != nil || int64(di) != want {
t.Fatalf("int %d: handled=%v err=%v got=%d", want, handled, err, di)
}
}
}
89 changes: 80 additions & 9 deletions driver/postgres/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
"math"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -704,6 +705,40 @@ func decodeToValue(raw []byte, codec *protocol.TypeCodec, col protocol.ColumnDes
return cp, nil
}

// The int64→narrower helpers below are the scan-side counterpart of the
// encoder's "int4 overflow" / "int2 overflow" checks: a value that does not
// fit the destination fails the scan instead of wrapping silently
// (celeris#502). Each returns the zero value with the error so callers can
// leave the destination untouched.

func int64ToInt(n int64) (int, error) {
if n < math.MinInt || n > math.MaxInt {
return 0, fmt.Errorf("celeris-postgres: scan: value %d out of range for int", n)
}
return int(n), nil
}

func int64ToInt32(n int64) (int32, error) {
if n < math.MinInt32 || n > math.MaxInt32 {
return 0, fmt.Errorf("celeris-postgres: scan: value %d out of range for int32", n)
}
return int32(n), nil
}

func int64ToInt16(n int64) (int16, error) {
if n < math.MinInt16 || n > math.MaxInt16 {
return 0, fmt.Errorf("celeris-postgres: scan: value %d out of range for int16", n)
}
return int16(n), nil
}

func int64ToUint32(n int64) (uint32, error) {
if n < 0 || n > math.MaxUint32 {
return 0, fmt.Errorf("celeris-postgres: scan: value %d out of range for uint32", n)
}
return uint32(n), nil
}

// decodeBinaryInto decodes raw binary-format bytes directly into dest
// for the common primitive types. Returns (true, err) when the type was
// handled (err may be non-nil for decode failures); (false, nil) when
Expand All @@ -715,7 +750,11 @@ func decodeBinaryInto(dest any, raw []byte, codec *protocol.TypeCodec) (bool, er
if err != nil {
return true, err
}
*d = int(n)
v, err := int64ToInt(n)
if err != nil {
return true, err
}
*d = v
return true, nil
case *int64:
n, err := protocol.DecodeIntBinary(raw, codec)
Expand All @@ -729,21 +768,33 @@ func decodeBinaryInto(dest any, raw []byte, codec *protocol.TypeCodec) (bool, er
if err != nil {
return true, err
}
*d = int32(n)
v, err := int64ToInt32(n)
if err != nil {
return true, err
}
*d = v
return true, nil
case *int16:
n, err := protocol.DecodeIntBinary(raw, codec)
if err != nil {
return true, err
}
*d = int16(n)
v, err := int64ToInt16(n)
if err != nil {
return true, err
}
*d = v
return true, nil
case *uint32:
n, err := protocol.DecodeIntBinary(raw, codec)
if err != nil {
return true, err
}
*d = uint32(n)
v, err := int64ToUint32(n)
if err != nil {
return true, err
}
*d = v
return true, nil
case *string:
// Fast path for text/varchar/etc.: no interface boxing, single
Expand Down Expand Up @@ -774,7 +825,11 @@ func decodeTextInto(dest any, raw []byte, _ *protocol.TypeCodec) (bool, error) {
if err != nil {
return true, err
}
*d = int(n)
v, err := int64ToInt(n)
if err != nil {
return true, err
}
*d = v
return true, nil
case *int64:
n, err := protocol.ParseIntTextASCII(raw)
Expand All @@ -788,7 +843,11 @@ func decodeTextInto(dest any, raw []byte, _ *protocol.TypeCodec) (bool, error) {
if err != nil {
return true, err
}
*d = int32(n)
v, err := int64ToInt32(n)
if err != nil {
return true, err
}
*d = v
return true, nil
case *string:
*d = string(raw)
Expand Down Expand Up @@ -868,7 +927,11 @@ func convertAssign(dest any, src any) error {
case *int:
switch s := src.(type) {
case int64:
*d = int(s)
v, err := int64ToInt(s)
if err != nil {
return err
}
*d = v
case float64:
*d = int(s)
case string:
Expand All @@ -888,14 +951,22 @@ func convertAssign(dest any, src any) error {
case *int32:
switch s := src.(type) {
case int64:
*d = int32(s)
v, err := int64ToInt32(s)
if err != nil {
return err
}
*d = v
default:
return fmt.Errorf("celeris-postgres: scan: cannot convert %T to int32", src)
}
case *int16:
switch s := src.(type) {
case int64:
*d = int16(s)
v, err := int64ToInt16(s)
if err != nil {
return err
}
*d = v
default:
return fmt.Errorf("celeris-postgres: scan: cannot convert %T to int16", src)
}
Expand Down