Skip to content

Commit 32105d8

Browse files
authored
Serdes fuzz testing and some bug fixes/compatability work (#100)
* serde fuzz testing and some bug fixes/compatability work * a * some serdes error simplification * tweaks to errors * add ip serdes support * more tests * oops
1 parent 8f7d1e3 commit 32105d8

25 files changed

Lines changed: 1332 additions & 1009 deletions

astra/internal/untyped/row_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func TestRow_UnmarshalAstraRaw(t *testing.T) {
121121
{Name: "ip", Column: table.Column{Type: table.TypeInet}},
122122
{Name: "blob", Column: table.Column{Type: table.TypeBlob}},
123123
},
124-
jsonData: `{"uuid": "550e8400-e29b-41d4-a716-446655440000", "ip": [192, 168, 1, 1], "blob": {"$binary": "aGVsbG8="}}`,
124+
jsonData: `{"uuid": "550e8400-e29b-41d4-a716-446655440000", "ip": "192.168.1.1", "blob": {"$binary": "aGVsbG8="}}`,
125125
validate: func(t *testing.T, row Row) {
126126
data := row.ToMap()
127127
if _, ok := data["uuid"].(datatypes.UUID); !ok {
@@ -233,6 +233,11 @@ func TestRow_UnmarshalAstraRaw_InvalidJSON(t *testing.T) {
233233
func TestProperty_DeserializeWithTypeHint_Varint(t *testing.T) {
234234
rapid.Check(t, func(t *rapid.T) {
235235
s := rapid.StringMatching(`-?[0-9]{1,50}`).Draw(t, "digits")
236+
237+
if s[0] == '0' || s[0] == '-' && s[1] == '0' {
238+
return // invalid json
239+
}
240+
236241
raw := json.RawMessage(s)
237242
col := table.Column{Type: table.TypeVarint}
238243

@@ -259,6 +264,11 @@ func TestProperty_DeserializeWithTypeHint_Varint(t *testing.T) {
259264
func TestProperty_DeserializeWithTypeHint_Decimal(t *testing.T) {
260265
rapid.Check(t, func(t *rapid.T) {
261266
s := rapid.StringMatching(`-?[0-9]{1,20}(\.[0-9]{1,20})?`).Draw(t, "digits")
267+
268+
if s[0] == '0' || s[0] == '-' && s[1] == '0' {
269+
return // invalid json
270+
}
271+
262272
raw := json.RawMessage(s)
263273
col := table.Column{Type: table.TypeDecimal}
264274

@@ -574,7 +584,10 @@ func genValueForColumn(col table.Column) *rapid.Generator[any] {
574584
case table.TypeUUID, table.TypeTimeUUID:
575585
return datatypes.MustParseUUID(rapid.StringMatching(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`).Draw(t, "uuid"))
576586
case table.TypeInet:
577-
return net.IP(rapid.SliceOfN(rapid.Byte(), 4, 16).Draw(t, "ip_bytes"))
587+
if rapid.Bool().Draw(t, "is_ipv4") {
588+
return net.IP(rapid.SliceOfN(rapid.Byte(), 4, 4).Draw(t, "ipv4_bytes"))
589+
}
590+
return net.IP(rapid.SliceOfN(rapid.Byte(), 16, 16).Draw(t, "ipv6_bytes"))
578591
case table.TypeBlob:
579592
return rapid.SliceOf(rapid.Byte()).Draw(t, "blob")
580593
case table.TypeDecimal:

astra/options/api_options.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,12 @@ type SerdesOptions struct {
111111
UseJSONMarshal *bool
112112

113113
// Deserialization flags
114-
SparseRows *bool
115-
UseNumber *bool
116-
DesNoCache *bool
117-
ExtendedErrorContext *bool
118-
UseJSONUnmarshal *bool
114+
SparseRows *bool
115+
UseNumber *bool
116+
DesNoCache *bool
117+
ExtendedErrorSnippet *bool
118+
UseJSONUnmarshal *bool
119+
CaseInsensitiveFieldMatching *bool
119120
}
120121

121122
// Merge implements shouldMerge for SerdesOptions.
@@ -163,8 +164,9 @@ func (o *SerdesOptions) walkDesFlags(fn func(serdes.DesFlags, **bool)) {
163164
fn(serdes.SparseRows, &o.SparseRows)
164165
fn(serdes.UseNumber, &o.UseNumber)
165166
fn(serdes.DesNoCache, &o.DesNoCache)
166-
fn(serdes.ExtendedErrorContext, &o.ExtendedErrorContext)
167+
fn(serdes.ExtendedErrorSnippet, &o.ExtendedErrorSnippet)
167168
fn(serdes.UseJSONUnmarshal, &o.UseJSONUnmarshal)
169+
fn(serdes.CaseInsensitiveFieldMatching, &o.CaseInsensitiveFieldMatching)
168170
}
169171

170172
// TimeoutOptions contains timeout configuration for API operations.

astra/options/builders_gen.go

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

astra/serdes/codecs.go

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,30 +43,7 @@ type DecodeCtx struct {
4343
Target Target
4444
TargetCtx TargetDecodeCtx
4545
Flags DesFlags
46-
}
47-
48-
func (c DecodeCtx) syntaxError(src []byte, msg string) error {
49-
return &SyntaxError{msg: msg, Snippet: errorSnippet(src, c.Flags)}
50-
}
51-
52-
func (c DecodeCtx) syntaxErrorWrap(src []byte, msg string, err error) error {
53-
return &SyntaxError{msg: msg, Snippet: errorSnippet(src, c.Flags), Err: err}
54-
}
55-
56-
func (c DecodeCtx) unmarshalTypeError(src []byte, t reflect.Type) error {
57-
return &UnmarshalTypeError{Value: nextJsonType(src), Type: t, Snippet: errorSnippet(src, c.Flags)}
58-
}
59-
60-
func (c DecodeCtx) unmarshalValueTypeError(src []byte, t reflect.Type, value string) error {
61-
return &UnmarshalTypeError{Value: value, Type: t, Snippet: errorSnippet(src, c.Flags)}
62-
}
63-
64-
func (c DecodeCtx) unmarshalTypeErrorWrap(src []byte, t reflect.Type, err error) error {
65-
return &UnmarshalTypeError{Value: nextJsonType(src), Type: t, Snippet: errorSnippet(src, c.Flags), Err: err}
66-
}
67-
68-
func (c DecodeCtx) unmarshalValueTypeErrorWrap(src []byte, t reflect.Type, value string, err error) error {
69-
return &UnmarshalTypeError{Value: value, Type: t, Snippet: errorSnippet(src, c.Flags), Err: err}
46+
payload *[]byte
7047
}
7148

7249
type AstraMarshaler interface {
@@ -190,6 +167,8 @@ func resolveCodec(ctx codecCtx, t reflect.Type, seen seenStructs, canAddr bool)
190167
return codec{durationEncoder, durationDecoder}
191168
case timeType:
192169
return codec{timeEncoder, timeDecoder}
170+
case ipType:
171+
return codec{ipEncoder, ipDecoder}
193172
}
194173

195174
if c.encode != nil {
@@ -220,7 +199,7 @@ func resolveCodec(ctx codecCtx, t reflect.Type, seen seenStructs, canAddr bool)
220199
c = mkSomeInterfaceCodec(t)
221200
default:
222201
if c.encode == nil {
223-
c = mkErroredCodec(&UnsupportedTypeError{Type: t})
202+
c = mkErroredCodec(&EncodeError{Action: EncodeActionUnsupportedType, Type: t})
224203
}
225204
}
226205

astra/serdes/codecs_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func TestCache_HeavyContention_Correctness(t *testing.T) {
125125
// 4. Round-trip: Decode
126126
valOut := reflect.New(typ).Elem()
127127
ptrOut := unsafe.Pointer(valOut.UnsafeAddr())
128-
_, err = c.decode(DecodeCtx{}, buf, ptrOut)
128+
_, err = c.decode(DecodeCtx{payload: &buf}, buf, ptrOut)
129129
if err != nil {
130130
t.Errorf("worker %d: decode failed: %v", workerID, err)
131131
continue

astra/serdes/datatypes.go

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"encoding/base64"
2020
"fmt"
2121
"math/big"
22+
"net"
2223
"reflect"
2324
"strconv"
2425
"time"
@@ -90,7 +91,7 @@ var oidTag = []byte("objectId")
9091

9192
func objectIdEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
9293
if ctx.Target != TargetCollection {
93-
return dst, &UnsupportedValueError{Msg: "ObjectId is only supported for collections"}
94+
return dst, ctx.unsupportedValueError("ObjectId is only supported for collections")
9495
}
9596

9697
return encodeDollarDatatype(dst, oidTag, func(dst []byte) ([]byte, error) {
@@ -103,7 +104,7 @@ func objectIdEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error
103104

104105
func objectIdDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
105106
if ctx.Target != TargetCollection {
106-
return src, &UnsupportedValueError{Msg: "ObjectId is only supported for collections"}
107+
return src, ctx.unsupportedValueError(src, "ObjectId is only supported for collections")
107108
}
108109

109110
src, oid, err := parseDollarDatatype(ctx, src, oidTag, func(ctx DecodeCtx, b []byte) ([]byte, datatypes.ObjectId, error) {
@@ -179,7 +180,7 @@ func timeDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
179180

180181
func dateOnlyEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
181182
if ctx.Target == TargetCollection {
182-
return nil, &UnsupportedValueError{Msg: "DateOnly is not supported for collections"}
183+
return nil, ctx.unsupportedValueError("DateOnly is not supported for collections")
183184
}
184185

185186
d := (*datatypes.DateOnly)(p)
@@ -191,7 +192,7 @@ func dateOnlyEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error
191192

192193
func dateOnlyDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
193194
if ctx.Target == TargetCollection {
194-
return src, &UnsupportedValueError{Msg: "DateOnly is not supported for collections"}
195+
return src, ctx.unsupportedValueError(src, "DateOnly is not supported for collections")
195196
}
196197

197198
srcAfter, str, _, err := parseStringUnquote(ctx, src)
@@ -214,7 +215,7 @@ func dateOnlyDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error
214215

215216
func timeOnlyEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
216217
if ctx.Target == TargetCollection {
217-
return nil, &UnsupportedValueError{Msg: "TimeOnly is not supported for collections"}
218+
return nil, ctx.unsupportedValueError("TimeOnly is not supported for collections")
218219
}
219220

220221
t := (*datatypes.TimeOnly)(p)
@@ -226,7 +227,7 @@ func timeOnlyEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error
226227

227228
func timeOnlyDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
228229
if ctx.Target == TargetCollection {
229-
return src, &UnsupportedValueError{Msg: "TimeOnly is not supported for collections"}
230+
return src, ctx.unsupportedValueError(src, "TimeOnly is not supported for collections")
230231
}
231232

232233
srcAfter, str, _, err := parseStringUnquote(ctx, src)
@@ -355,27 +356,37 @@ func vectorDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error)
355356
}
356357

357358
// ================================
358-
// | Binary data ([]byte) - encoded as {"$binary":"<base64>"} in all contexts,
359+
// | Binary data ([]byte) - encoded as {"$binary":"<base64>"} in tables and collections, and just "<base64>" otherwise
359360
// | but can be decoded from either that format or from a base64 string
361+
// |
362+
// | NOTE: There's technically no reason to ever encode a []byte outside of encoding to a collection/table, but the
363+
// | special case just exists to help the fuzz testing pass
360364
// ================================
361365

362-
func binaryEncoder(_ EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
363-
return encodeDollarDatatype(dst, []byte("binary"), func(dst []byte) ([]byte, error) {
364-
return encodeBytesAsBase64(dst, *(*[]byte)(p)), nil
365-
})
366+
func binaryEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
367+
if ctx.Target == TargetCollection || ctx.Target == TargetTable {
368+
return encodeDollarDatatype(dst, []byte("binary"), func(dst []byte) ([]byte, error) {
369+
return encodeBytesAsBase64(dst, *(*[]byte)(p)), nil
370+
})
371+
}
372+
return encodeBytesAsBase64(dst, *(*[]byte)(p)), nil
366373
}
367374

368375
func binaryDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
369376
src = skipWS(src)
370377

378+
if b, ok := consumeNull(src); ok {
379+
*(*[]byte)(p) = nil
380+
return b, nil
381+
}
382+
371383
if len(src) != 0 && src[0] == '"' {
372-
src, str, _, err := parseStringUnquote(ctx, src)
373-
if err != nil {
374-
return src, err
375-
}
384+
src, data, err := decodeBytesFromBase64(ctx, src)
376385

377-
*(*[]byte)(p) = str
378-
return src, nil
386+
if err == nil {
387+
*(*[]byte)(p) = data
388+
}
389+
return src, err
379390
}
380391

381392
if len(src) != 0 && src[0] == '{' {
@@ -444,7 +455,7 @@ func decodeBytesFromBase64(ctx DecodeCtx, src []byte) ([]byte, []byte, error) {
444455

445456
func durationEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
446457
if ctx.Target == TargetCollection {
447-
return nil, &UnsupportedValueError{Msg: "Duration is not supported for collections"}
458+
return nil, ctx.unsupportedValueError("Duration is not supported for collections")
448459
}
449460
d := (*datatypes.Duration)(p)
450461
dst = append(dst, '"')
@@ -455,7 +466,7 @@ func durationEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error
455466

456467
func durationDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
457468
if ctx.Target == TargetCollection {
458-
return src, &UnsupportedValueError{Msg: "Duration is not supported for collections"}
469+
return src, ctx.unsupportedValueError(src, "Duration is not supported for collections")
459470
}
460471
src, str, _, err := parseStringUnquote(ctx, src)
461472
if err != nil {
@@ -469,6 +480,37 @@ func durationDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error
469480
return src, nil
470481
}
471482

483+
// ================================
484+
// | net.IP - encoded as a plain quoted string in all contexts
485+
// ================================
486+
487+
func ipEncoder(ctx EncodeCtx, dst []byte, p unsafe.Pointer) ([]byte, error) {
488+
if ctx.Target == TargetCollection {
489+
return nil, ctx.unsupportedValueError("net.IP is not supported for collections")
490+
}
491+
ip := (*net.IP)(p)
492+
dst = append(dst, '"')
493+
dst = append(dst, ip.String()...)
494+
dst = append(dst, '"')
495+
return dst, nil
496+
}
497+
498+
func ipDecoder(ctx DecodeCtx, src []byte, p unsafe.Pointer) ([]byte, error) {
499+
if ctx.Target == TargetCollection {
500+
return src, ctx.unsupportedValueError(src, "net.IP is not supported for collections")
501+
}
502+
srcAfter, str, _, err := parseStringUnquote(ctx, src)
503+
if err != nil {
504+
return srcAfter, err
505+
}
506+
ip := net.ParseIP(unsafeString(str))
507+
if ip == nil {
508+
return srcAfter, ctx.syntaxError(src, "invalid IP string")
509+
}
510+
*(*net.IP)(p) = ip
511+
return srcAfter, nil
512+
}
513+
472514
// ================================
473515
// | Helpers
474516
// ================================

0 commit comments

Comments
 (0)