Skip to content

Commit 642d93f

Browse files
authored
Merge pull request #770 from tdnguyenND/feature/toindefarray-v2
feat: add toindefarray struct tag option for CBOR indefinite-length arrays. This can be useful for Cardano Plutus Data interoperability.
2 parents 555d252 + 7b123cf commit 642d93f

4 files changed

Lines changed: 429 additions & 15 deletions

File tree

cache.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,16 @@ func getDecodingStructType(t reflect.Type) (*decodingStructType, error) {
111111

112112
flds, structOptions := getFields(t)
113113

114-
toArray := hasToArrayOption(structOptions)
115-
116-
if toArray {
114+
hasToArray, hasToIndefArray, err := arrayStructOptions(t, structOptions)
115+
if err != nil {
116+
structType := &decodingStructType{err: err}
117+
decodingStructTypeCache.Store(t, structType)
118+
return nil, err
119+
}
120+
// Both options describe a struct laid out as a CBOR array. The decoder
121+
// accepts definite- and indefinite-length arrays interchangeably, so
122+
// the same code path serves both options.
123+
if hasToArray || hasToIndefArray {
117124
return getDecodingStructToArrayType(t, flds)
118125
}
119126

@@ -205,11 +212,12 @@ func getDecodingStructToArrayType(t reflect.Type, flds fields) (*decodingStructT
205212

206213
type encodingStructType struct {
207214
fields encodingFields
208-
bytewiseFields encodingFields // Only populated if toArray is false
209-
lengthFirstFields encodingFields // Only populated if toArray is false
210-
omitEmptyFieldsIdx []int // Only populated if toArray is false
215+
bytewiseFields encodingFields // Only populated if struct is not array-shaped
216+
lengthFirstFields encodingFields // Only populated if struct is not array-shaped
217+
omitEmptyFieldsIdx []int // Only populated if struct is not array-shaped
211218
err error
212-
toArray bool
219+
toArray bool // True iff the struct declares the `toarray` option
220+
toIndefArray bool // True iff the struct declares the `toindefarray` option
213221
}
214222

215223
func (st *encodingStructType) getFields(em *encMode) encodingFields {
@@ -245,8 +253,14 @@ func getEncodingStructType(t reflect.Type) (*encodingStructType, error) {
245253

246254
flds, structOptions := getFields(t)
247255

248-
if hasToArrayOption(structOptions) {
249-
return getEncodingStructToArrayType(t, flds)
256+
hasToArray, hasToIndefArray, err := arrayStructOptions(t, structOptions)
257+
if err != nil {
258+
structType := &encodingStructType{err: err}
259+
encodingStructTypeCache.Store(t, structType)
260+
return nil, err
261+
}
262+
if hasToArray || hasToIndefArray {
263+
return getEncodingStructToArrayType(t, flds, hasToArray, hasToIndefArray)
250264
}
251265

252266
var hasKeyAsInt bool
@@ -344,7 +358,7 @@ func getEncodingStructType(t reflect.Type) (*encodingStructType, error) {
344358
return structType, nil
345359
}
346360

347-
func getEncodingStructToArrayType(t reflect.Type, flds fields) (*encodingStructType, error) {
361+
func getEncodingStructToArrayType(t reflect.Type, flds fields, toArray, toIndefArray bool) (*encodingStructType, error) {
348362
encFlds := make(encodingFields, len(flds))
349363
for i, f := range flds {
350364
encFlds[i] = &encodingField{field: *f}
@@ -357,8 +371,9 @@ func getEncodingStructToArrayType(t reflect.Type, flds fields) (*encodingStructT
357371
}
358372

359373
structType := &encodingStructType{
360-
fields: encFlds,
361-
toArray: true,
374+
fields: encFlds,
375+
toArray: toArray,
376+
toIndefArray: toIndefArray,
362377
}
363378
encodingStructTypeCache.Store(t, structType)
364379
return structType, nil
@@ -388,3 +403,21 @@ func hasToArrayOption(tag string) bool {
388403
idx := strings.Index(tag, s)
389404
return idx >= 0 && (len(tag) == idx+len(s) || tag[idx+len(s)] == ',')
390405
}
406+
407+
func hasToIndefArrayOption(tag string) bool {
408+
s := ",toindefarray"
409+
idx := strings.Index(tag, s)
410+
return idx >= 0 && (len(tag) == idx+len(s) || tag[idx+len(s)] == ',')
411+
}
412+
413+
// arrayStructOptions reports whether the struct options request encoding as
414+
// a CBOR array (definite- or indefinite-length), and returns an error if the
415+
// two options are specified together (they are mutually exclusive).
416+
func arrayStructOptions(t reflect.Type, structOptions string) (hasToArray, hasToIndefArray bool, err error) {
417+
hasToArray = hasToArrayOption(structOptions)
418+
hasToIndefArray = hasToIndefArrayOption(structOptions)
419+
if hasToArray && hasToIndefArray {
420+
return false, false, fmt.Errorf("cbor: struct %v cannot have both \"toarray\" and \"toindefarray\" options", t)
421+
}
422+
return hasToArray, hasToIndefArray, nil
423+
}

encode.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,25 @@ func (tmm TextMarshalerMode) valid() bool {
546546
return tmm >= 0 && tmm < maxTextMarshalerMode
547547
}
548548

549+
// ToIndefArrayStructTagMode specifies whether to honor the `toindefarray`
550+
// struct tag option. Indefinite-length encoding via the streaming API is
551+
// governed separately by IndefLength.
552+
type ToIndefArrayStructTagMode int
553+
554+
const (
555+
// ToIndefArrayStructTagForbidden rejects encoding of structs tagged with `toindefarray`.
556+
ToIndefArrayStructTagForbidden ToIndefArrayStructTagMode = iota
557+
558+
// ToIndefArrayStructTagAllowed encodes structs tagged with `toindefarray` as indefinite-length arrays.
559+
ToIndefArrayStructTagAllowed
560+
561+
maxToIndefArrayStructTagMode
562+
)
563+
564+
func (m ToIndefArrayStructTagMode) valid() bool {
565+
return m >= 0 && m < maxToIndefArrayStructTagMode
566+
}
567+
549568
// EncOptions specifies encoding options.
550569
type EncOptions struct {
551570
// Sort specifies sorting order.
@@ -611,6 +630,9 @@ type EncOptions struct {
611630
// json.Marshaler but do not also implement cbor.Marshaler. If nil, encoding behavior is not
612631
// influenced by whether or not a type implements json.Marshaler.
613632
JSONMarshalerTranscoder Transcoder
633+
634+
// ToIndefArrayStructTag specifies whether the `toindefarray` struct tag option is honored.
635+
ToIndefArrayStructTag ToIndefArrayStructTagMode
614636
}
615637

616638
// CanonicalEncOptions returns EncOptions for "Canonical CBOR" encoding,
@@ -824,6 +846,12 @@ func (opts EncOptions) encMode() (*encMode, error) { //nolint:gocritic // ignore
824846
if !opts.TextMarshaler.valid() {
825847
return nil, errors.New("cbor: invalid TextMarshaler " + strconv.Itoa(int(opts.TextMarshaler)))
826848
}
849+
if !opts.ToIndefArrayStructTag.valid() {
850+
return nil, errors.New("cbor: invalid ToIndefArrayStructTag " + strconv.Itoa(int(opts.ToIndefArrayStructTag)))
851+
}
852+
if opts.IndefLength == IndefLengthForbidden && opts.ToIndefArrayStructTag == ToIndefArrayStructTagAllowed {
853+
return nil, errors.New("cbor: cannot set ToIndefArrayStructTag to ToIndefArrayStructTagAllowed when IndefLength is IndefLengthForbidden")
854+
}
827855
em := encMode{
828856
sort: opts.Sort,
829857
shortestFloat: opts.ShortestFloat,
@@ -845,6 +873,7 @@ func (opts EncOptions) encMode() (*encMode, error) { //nolint:gocritic // ignore
845873
binaryMarshaler: opts.BinaryMarshaler,
846874
textMarshaler: opts.TextMarshaler,
847875
jsonMarshalerTranscoder: opts.JSONMarshalerTranscoder,
876+
toIndefArrayStructTag: opts.ToIndefArrayStructTag,
848877
}
849878
return &em, nil
850879
}
@@ -892,6 +921,7 @@ type encMode struct {
892921
binaryMarshaler BinaryMarshalerMode
893922
textMarshaler TextMarshalerMode
894923
jsonMarshalerTranscoder Transcoder
924+
toIndefArrayStructTag ToIndefArrayStructTagMode
895925
}
896926

897927
var defaultEncMode, _ = EncOptions{}.encMode()
@@ -986,6 +1016,7 @@ func (em *encMode) EncOptions() EncOptions {
9861016
BinaryMarshaler: em.binaryMarshaler,
9871017
TextMarshaler: em.textMarshaler,
9881018
JSONMarshalerTranscoder: em.jsonMarshalerTranscoder,
1019+
ToIndefArrayStructTag: em.toIndefArrayStructTag,
9891020
}
9901021
}
9911022

@@ -1457,13 +1488,22 @@ func encodeStructToArray(e *bytes.Buffer, em *encMode, v reflect.Value) (err err
14571488
return err
14581489
}
14591490

1491+
if structType.toIndefArray && em.toIndefArrayStructTag != ToIndefArrayStructTagAllowed {
1492+
return errors.New("cbor: cannot encode struct " + v.Type().String() +
1493+
" with `toindefarray` when ToIndefArrayStructTag is not ToIndefArrayStructTagAllowed")
1494+
}
1495+
14601496
if b := em.encTagBytes(v.Type()); b != nil {
14611497
e.Write(b)
14621498
}
14631499

14641500
flds := structType.fields
14651501

1466-
encodeHead(e, byte(cborTypeArray), uint64(len(flds)))
1502+
if structType.toIndefArray {
1503+
e.WriteByte(cborArrayWithIndefiniteLengthHead)
1504+
} else {
1505+
encodeHead(e, byte(cborTypeArray), uint64(len(flds)))
1506+
}
14671507
for i := range flds {
14681508
f := flds[i]
14691509

@@ -1486,6 +1526,9 @@ func encodeStructToArray(e *bytes.Buffer, em *encMode, v reflect.Value) (err err
14861526
return err
14871527
}
14881528
}
1529+
if structType.toIndefArray {
1530+
e.WriteByte(cborBreakFlag)
1531+
}
14891532
return nil
14901533
}
14911534

@@ -2050,7 +2093,7 @@ func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc, izf
20502093
if f, ok := t.FieldByName("_"); ok {
20512094
tag := f.Tag.Get("cbor")
20522095
if tag != "-" {
2053-
if hasToArrayOption(tag) {
2096+
if hasToArrayOption(tag) || hasToIndefArrayOption(tag) {
20542097
return encodeStructToArray, isEmptyStruct, isZeroFieldStruct
20552098
}
20562099
}
@@ -2133,7 +2176,7 @@ func isEmptyStruct(em *encMode, v reflect.Value) (bool, error) {
21332176
return false, nil
21342177
}
21352178

2136-
if structType.toArray {
2179+
if structType.toArray || structType.toIndefArray {
21372180
return len(structType.fields) == 0, nil
21382181
}
21392182

encode_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5633,6 +5633,13 @@ func TestEncOptions(t *testing.T) {
56335633
// non-zero value for other options (e.g. TimeTag).
56345634
continue
56355635
}
5636+
if fn == "ToIndefArrayStructTag" {
5637+
// Roundtripping non-zero values for ToIndefArrayStructTag is tested
5638+
// separately since the non-zero value (ToIndefArrayStructTagAllowed)
5639+
// is incompatible with IndefLengthForbidden, which is the non-zero
5640+
// value used by IndefLength above.
5641+
continue
5642+
}
56365643
t.Errorf("options field %q is unset or set to the zero value for its type", fn)
56375644
}
56385645
}

0 commit comments

Comments
 (0)