Skip to content

Commit a339bac

Browse files
committed
fix(contrib/drivers/pgsql): preserve bytea data integrity on read and write
Fix two bytea data corruption issues in PostgreSQL driver: 1. READ path (gogf#4677): `CheckLocalTypeForField` and `ConvertValueForLocal` had no case for plain "bytea" type, causing it to fall through to Core layer which incorrectly mapped it to `LocalTypeString`. Binary data was converted to string via `gconv.String()`, corrupting the bytes. 2. WRITE path (gogf#4231): `ConvertValueForField` applied PostgreSQL array syntax conversion (`[`->`{`, `]`->`}`) to all slice types including `[]byte` for bytea columns, corrupting bytes 0x5B(`[`) and 0x5D(`]`). closes gogf#4677, closes gogf#4231
1 parent 6a3ea89 commit a339bac

3 files changed

Lines changed: 135 additions & 7 deletions

File tree

contrib/drivers/pgsql/pgsql_convert.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ import (
1111
"reflect"
1212
"strings"
1313

14-
"github.com/google/uuid"
15-
"github.com/lib/pq"
16-
1714
"github.com/gogf/gf/v2/database/gdb"
1815
"github.com/gogf/gf/v2/frame/g"
1916
"github.com/gogf/gf/v2/text/gregex"
2017
"github.com/gogf/gf/v2/text/gstr"
2118
"github.com/gogf/gf/v2/util/gconv"
19+
"github.com/google/uuid"
20+
"github.com/lib/pq"
2221
)
2322

2423
// ConvertValueForField converts value to database acceptable value.
@@ -30,6 +29,10 @@ func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fie
3029
var fieldValueKind = reflect.TypeOf(fieldValue).Kind()
3130

3231
if fieldValueKind == reflect.Slice {
32+
// For bytea type, pass []byte directly without any conversion.
33+
if _, ok := fieldValue.([]byte); ok && gstr.Contains(fieldType, "bytea") {
34+
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
35+
}
3336
// For pgsql, json or jsonb require '[]'
3437
if !gstr.Contains(fieldType, "json") {
3538
fieldValue = gstr.ReplaceByMap(gconv.String(fieldValue),
@@ -62,6 +65,7 @@ func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fie
6265
// | _varchar, _text | []string |
6366
// | _char, _bpchar | []string |
6467
// | _numeric, _decimal, _money | []float64 |
68+
// | bytea | []byte |
6569
// | _bytea | [][]byte |
6670
// | _uuid | []uuid.UUID |
6771
func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (gdb.LocalType, error) {
@@ -107,6 +111,9 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f
107111
case "_numeric", "_decimal", "_money":
108112
return gdb.LocalTypeFloat64Slice, nil
109113

114+
case "bytea":
115+
return gdb.LocalTypeBytes, nil
116+
110117
case "_bytea":
111118
return gdb.LocalTypeBytesSlice, nil
112119

@@ -141,6 +148,7 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f
141148
// | _numeric | numeric[] | pq.Float64Array | []float64 |
142149
// | _decimal | decimal[] | pq.Float64Array | []float64 |
143150
// | _money | money[] | pq.Float64Array | []float64 |
151+
// | bytea | bytea | - | []byte |
144152
// | _bytea | bytea[] | pq.ByteaArray | [][]byte |
145153
// | _uuid | uuid[] | pq.StringArray | []uuid.UUID |
146154
//
@@ -154,6 +162,13 @@ func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fie
154162
// Basic types are mostly handled by Core layer, only handle array types here
155163
switch typeName {
156164

165+
// []byte
166+
case "bytea":
167+
if v, ok := fieldValue.([]byte); ok {
168+
return v, nil
169+
}
170+
return fieldValue, nil
171+
157172
// []int32
158173
case "_int2", "_int4":
159174
var result pq.Int32Array

contrib/drivers/pgsql/pgsql_z_unit_convert_test.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ import (
1010
"context"
1111
"testing"
1212

13-
"github.com/google/uuid"
14-
13+
"github.com/gogf/gf/contrib/drivers/pgsql/v2"
1514
"github.com/gogf/gf/v2/database/gdb"
1615
"github.com/gogf/gf/v2/test/gtest"
17-
18-
"github.com/gogf/gf/contrib/drivers/pgsql/v2"
16+
"github.com/google/uuid"
1917
)
2018

2119
// Test_CheckLocalTypeForField tests the CheckLocalTypeForField method
@@ -108,6 +106,13 @@ func Test_CheckLocalTypeForField(t *testing.T) {
108106
t.Assert(localType, gdb.LocalTypeFloat64Slice)
109107
})
110108

109+
gtest.C(t, func(t *gtest.T) {
110+
// Test bytea type
111+
localType, err := driver.CheckLocalTypeForField(ctx, "bytea", nil)
112+
t.AssertNil(err)
113+
t.Assert(localType, gdb.LocalTypeBytes)
114+
})
115+
111116
gtest.C(t, func(t *gtest.T) {
112117
// Test bytea array type
113118
localType, err := driver.CheckLocalTypeForField(ctx, "_bytea", nil)
@@ -362,6 +367,17 @@ func Test_ConvertValueForLocal(t *testing.T) {
362367
_, err := driver.ConvertValueForLocal(ctx, "_bytea", "invalid")
363368
t.AssertNE(err, nil)
364369
})
370+
371+
gtest.C(t, func(t *gtest.T) {
372+
// Test bytea conversion - should preserve []byte as-is
373+
input := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x5D, 0x5B}
374+
result, err := driver.ConvertValueForLocal(ctx, "bytea", input)
375+
t.AssertNil(err)
376+
resultBytes, ok := result.([]byte)
377+
t.Assert(ok, true)
378+
t.Assert(len(resultBytes), len(input))
379+
t.Assert(resultBytes, input)
380+
})
365381
}
366382

367383
// Test_ConvertValueForField tests the ConvertValueForField method
@@ -406,4 +422,14 @@ func Test_ConvertValueForField(t *testing.T) {
406422
t.AssertNil(err)
407423
t.Assert(result, `["a","b"]`)
408424
})
425+
426+
gtest.C(t, func(t *gtest.T) {
427+
// Test []byte value for bytea type (should preserve raw bytes, not do []->{} replacement)
428+
input := []byte{0xDE, 0xAD, 0x5B, 0x5D, 0xBE, 0xEF}
429+
result, err := driver.ConvertValueForField(ctx, "bytea", input)
430+
t.AssertNil(err)
431+
resultBytes, ok := result.([]byte)
432+
t.Assert(ok, true)
433+
t.Assert(resultBytes, input)
434+
})
409435
}

contrib/drivers/pgsql/pgsql_z_unit_issue_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,93 @@ func Test_Issue4500(t *testing.T) {
290290
})
291291
}
292292

293+
// https://github.com/gogf/gf/issues/4677
294+
// record.Get().Bytes() corrupts bytea data on retrieval from PostgreSQL.
295+
func Test_Issue4677(t *testing.T) {
296+
table := fmt.Sprintf(`%s_%d`, TablePrefix+"issue4677", gtime.TimestampNano())
297+
if _, err := db.Exec(ctx, fmt.Sprintf(`
298+
CREATE TABLE %s (
299+
id bigserial PRIMARY KEY,
300+
bin_data bytea
301+
);`, table,
302+
)); err != nil {
303+
gtest.Fatal(err)
304+
}
305+
defer dropTable(table)
306+
307+
gtest.C(t, func(t *gtest.T) {
308+
// Test 1: Binary data with various byte values including 0x00, 0x5D(']'), 0x5B('[')
309+
originalBytes := []byte{
310+
0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01, 0x5B, 0x5D,
311+
0xFF, 0x7B, 0x7D, 0x80, 0xCA, 0xFE, 0xBA, 0xBE,
312+
}
313+
314+
_, err := db.Model(table).Data(g.Map{
315+
"bin_data": originalBytes,
316+
}).Insert()
317+
t.AssertNil(err)
318+
319+
record, err := db.Model(table).Where("id", 1).One()
320+
t.AssertNil(err)
321+
322+
retrievedBytes := record["bin_data"].Bytes()
323+
t.Assert(len(retrievedBytes), len(originalBytes))
324+
t.Assert(retrievedBytes, originalBytes)
325+
})
326+
327+
gtest.C(t, func(t *gtest.T) {
328+
// Test 2: Larger binary data (simulating gob/protobuf encoded payload)
329+
largeBytes := make([]byte, 1024)
330+
for i := range largeBytes {
331+
largeBytes[i] = byte(i % 256)
332+
}
333+
334+
_, err := db.Model(table).Data(g.Map{
335+
"bin_data": largeBytes,
336+
}).Insert()
337+
t.AssertNil(err)
338+
339+
record, err := db.Model(table).OrderDesc("id").One()
340+
t.AssertNil(err)
341+
342+
retrievedBytes := record["bin_data"].Bytes()
343+
t.Assert(len(retrievedBytes), len(largeBytes))
344+
t.Assert(retrievedBytes, largeBytes)
345+
})
346+
}
347+
348+
// https://github.com/gogf/gf/issues/4231
349+
// ConvertValueForField corrupts bytea data containing 0x5D on write.
350+
func Test_Issue4231(t *testing.T) {
351+
table := fmt.Sprintf(`%s_%d`, TablePrefix+"issue4231", gtime.TimestampNano())
352+
if _, err := db.Exec(ctx, fmt.Sprintf(`
353+
CREATE TABLE %s (
354+
id bigserial PRIMARY KEY,
355+
bin_data bytea
356+
);`, table,
357+
)); err != nil {
358+
gtest.Fatal(err)
359+
}
360+
defer dropTable(table)
361+
362+
gtest.C(t, func(t *gtest.T) {
363+
// Bytes containing 0x5D (ASCII ']') which was being converted to 0x7D ('}')
364+
originalBytes := []byte{0x01, 0x5D, 0x02, 0x5B, 0x03}
365+
366+
_, err := db.Model(table).Data(g.Map{
367+
"bin_data": originalBytes,
368+
}).Insert()
369+
t.AssertNil(err)
370+
371+
record, err := db.Model(table).Where("id", 1).One()
372+
t.AssertNil(err)
373+
374+
retrievedBytes := record["bin_data"].Bytes()
375+
t.Assert(len(retrievedBytes), len(originalBytes))
376+
t.Assert(retrievedBytes, originalBytes)
377+
})
378+
}
379+
293380
// https://github.com/gogf/gf/issues/4595
294381
// FieldsPrefix silently drops fields when using table alias before LeftJoin.
295382
func Test_Issue4595(t *testing.T) {

0 commit comments

Comments
 (0)