Skip to content

Commit 700df14

Browse files
authored
rlp: add support for optional struct fields (#22832)
This adds support for a new struct tag "optional". Using this tag, structs used for RLP encoding/decoding can be extended in a backwards-compatible way, by adding new fields at the end.
1 parent 8a070e8 commit 700df14

File tree

6 files changed

+330
-45
lines changed

6 files changed

+330
-45
lines changed

rlp/decode.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func decodeBigInt(s *Stream, val reflect.Value) error {
229229
i = new(big.Int)
230230
val.Set(reflect.ValueOf(i))
231231
}
232-
// Reject leading zero bytes
232+
// Reject leading zero bytes.
233233
if len(b) > 0 && b[0] == 0 {
234234
return wrapStreamError(ErrCanonInt, val.Type())
235235
}
@@ -394,9 +394,16 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) {
394394
if _, err := s.List(); err != nil {
395395
return wrapStreamError(err, typ)
396396
}
397-
for _, f := range fields {
397+
for i, f := range fields {
398398
err := f.info.decoder(s, val.Field(f.index))
399399
if err == EOL {
400+
if f.optional {
401+
// The field is optional, so reaching the end of the list before
402+
// reaching the last field is acceptable. All remaining undecoded
403+
// fields are zeroed.
404+
zeroFields(val, fields[i:])
405+
break
406+
}
400407
return &decodeError{msg: "too few elements", typ: typ}
401408
} else if err != nil {
402409
return addErrorContext(err, "."+typ.Field(f.index).Name)
@@ -407,6 +414,13 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) {
407414
return dec, nil
408415
}
409416

417+
func zeroFields(structval reflect.Value, fields []field) {
418+
for _, f := range fields {
419+
fv := structval.Field(f.index)
420+
fv.Set(reflect.Zero(fv.Type()))
421+
}
422+
}
423+
410424
// makePtrDecoder creates a decoder that decodes into the pointer's element type.
411425
func makePtrDecoder(typ reflect.Type, tag tags) (decoder, error) {
412426
etype := typ.Elem()

rlp/decode_test.go

Lines changed: 173 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -369,19 +369,46 @@ type intField struct {
369369
X int
370370
}
371371

372+
type optionalFields struct {
373+
A uint
374+
B uint `rlp:"optional"`
375+
C uint `rlp:"optional"`
376+
}
377+
378+
type optionalAndTailField struct {
379+
A uint
380+
B uint `rlp:"optional"`
381+
Tail []uint `rlp:"tail"`
382+
}
383+
384+
type optionalBigIntField struct {
385+
A uint
386+
B *big.Int `rlp:"optional"`
387+
}
388+
389+
type optionalPtrField struct {
390+
A uint
391+
B *[3]byte `rlp:"optional"`
392+
}
393+
394+
type optionalPtrFieldNil struct {
395+
A uint
396+
B *[3]byte `rlp:"optional,nil"`
397+
}
398+
399+
type ignoredField struct {
400+
A uint
401+
B uint `rlp:"-"`
402+
C uint
403+
}
404+
372405
var (
373406
veryBigInt = big.NewInt(0).Add(
374407
big.NewInt(0).Lsh(big.NewInt(0xFFFFFFFFFFFFFF), 16),
375408
big.NewInt(0xFFFF),
376409
)
377410
)
378411

379-
type hasIgnoredField struct {
380-
A uint
381-
B uint `rlp:"-"`
382-
C uint
383-
}
384-
385412
var decodeTests = []decodeTest{
386413
// booleans
387414
{input: "01", ptr: new(bool), value: true},
@@ -551,8 +578,8 @@ var decodeTests = []decodeTest{
551578
// struct tag "-"
552579
{
553580
input: "C20102",
554-
ptr: new(hasIgnoredField),
555-
value: hasIgnoredField{A: 1, C: 2},
581+
ptr: new(ignoredField),
582+
value: ignoredField{A: 1, C: 2},
556583
},
557584

558585
// struct tag "nilList"
@@ -592,6 +619,110 @@ var decodeTests = []decodeTest{
592619
value: nilStringSlice{X: &[]uint{3}},
593620
},
594621

622+
// struct tag "optional"
623+
{
624+
input: "C101",
625+
ptr: new(optionalFields),
626+
value: optionalFields{1, 0, 0},
627+
},
628+
{
629+
input: "C20102",
630+
ptr: new(optionalFields),
631+
value: optionalFields{1, 2, 0},
632+
},
633+
{
634+
input: "C3010203",
635+
ptr: new(optionalFields),
636+
value: optionalFields{1, 2, 3},
637+
},
638+
{
639+
input: "C401020304",
640+
ptr: new(optionalFields),
641+
error: "rlp: input list has too many elements for rlp.optionalFields",
642+
},
643+
{
644+
input: "C101",
645+
ptr: new(optionalAndTailField),
646+
value: optionalAndTailField{A: 1},
647+
},
648+
{
649+
input: "C20102",
650+
ptr: new(optionalAndTailField),
651+
value: optionalAndTailField{A: 1, B: 2, Tail: []uint{}},
652+
},
653+
{
654+
input: "C401020304",
655+
ptr: new(optionalAndTailField),
656+
value: optionalAndTailField{A: 1, B: 2, Tail: []uint{3, 4}},
657+
},
658+
{
659+
input: "C101",
660+
ptr: new(optionalBigIntField),
661+
value: optionalBigIntField{A: 1, B: nil},
662+
},
663+
{
664+
input: "C20102",
665+
ptr: new(optionalBigIntField),
666+
value: optionalBigIntField{A: 1, B: big.NewInt(2)},
667+
},
668+
{
669+
input: "C101",
670+
ptr: new(optionalPtrField),
671+
value: optionalPtrField{A: 1},
672+
},
673+
{
674+
input: "C20180", // not accepted because "optional" doesn't enable "nil"
675+
ptr: new(optionalPtrField),
676+
error: "rlp: input string too short for [3]uint8, decoding into (rlp.optionalPtrField).B",
677+
},
678+
{
679+
input: "C20102",
680+
ptr: new(optionalPtrField),
681+
error: "rlp: input string too short for [3]uint8, decoding into (rlp.optionalPtrField).B",
682+
},
683+
{
684+
input: "C50183010203",
685+
ptr: new(optionalPtrField),
686+
value: optionalPtrField{A: 1, B: &[3]byte{1, 2, 3}},
687+
},
688+
{
689+
input: "C101",
690+
ptr: new(optionalPtrFieldNil),
691+
value: optionalPtrFieldNil{A: 1},
692+
},
693+
{
694+
input: "C20180", // accepted because "nil" tag allows empty input
695+
ptr: new(optionalPtrFieldNil),
696+
value: optionalPtrFieldNil{A: 1},
697+
},
698+
{
699+
input: "C20102",
700+
ptr: new(optionalPtrFieldNil),
701+
error: "rlp: input string too short for [3]uint8, decoding into (rlp.optionalPtrFieldNil).B",
702+
},
703+
704+
// struct tag "optional" field clearing
705+
{
706+
input: "C101",
707+
ptr: &optionalFields{A: 9, B: 8, C: 7},
708+
value: optionalFields{A: 1, B: 0, C: 0},
709+
},
710+
{
711+
input: "C20102",
712+
ptr: &optionalFields{A: 9, B: 8, C: 7},
713+
value: optionalFields{A: 1, B: 2, C: 0},
714+
},
715+
{
716+
input: "C20102",
717+
ptr: &optionalAndTailField{A: 9, B: 8, Tail: []uint{7, 6, 5}},
718+
value: optionalAndTailField{A: 1, B: 2, Tail: []uint{}},
719+
},
720+
{
721+
input: "C101",
722+
ptr: &optionalPtrField{A: 9, B: &[3]byte{8, 7, 6}},
723+
value: optionalPtrField{A: 1},
724+
},
725+
595726
// RawValue
596727
{input: "01", ptr: new(RawValue), value: RawValue(unhex("01"))},
597728
{input: "82FFFF", ptr: new(RawValue), value: RawValue(unhex("82FFFF"))},
@@ -822,6 +953,40 @@ func TestDecoderFunc(t *testing.T) {
822953
x()
823954
}
824955

956+
// This tests the validity checks for fields with struct tag "optional".
957+
func TestInvalidOptionalField(t *testing.T) {
958+
type (
959+
invalid1 struct {
960+
A uint `rlp:"optional"`
961+
B uint
962+
}
963+
invalid2 struct {
964+
T []uint `rlp:"tail,optional"`
965+
}
966+
invalid3 struct {
967+
T []uint `rlp:"optional,tail"`
968+
}
969+
)
970+
971+
tests := []struct {
972+
v interface{}
973+
err string
974+
}{
975+
{v: new(invalid1), err: `rlp: struct field rlp.invalid1.B needs "optional" tag`},
976+
{v: new(invalid2), err: `rlp: invalid struct tag "optional" for rlp.invalid2.T (also has "tail" tag)`},
977+
{v: new(invalid3), err: `rlp: invalid struct tag "tail" for rlp.invalid3.T (also has "optional" tag)`},
978+
}
979+
for _, test := range tests {
980+
err := DecodeBytes(unhex("C20102"), test.v)
981+
if err == nil {
982+
t.Errorf("no error for %T", test.v)
983+
} else if err.Error() != test.err {
984+
t.Errorf("wrong error for %T: %v", test.v, err.Error())
985+
}
986+
}
987+
988+
}
989+
825990
func ExampleDecode() {
826991
input, _ := hex.DecodeString("C90A1486666F6F626172")
827992

rlp/doc.go

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,29 +102,60 @@ Signed integers, floating point numbers, maps, channels and functions cannot be
102102
103103
Struct Tags
104104
105-
Package rlp honours certain struct tags: "-", "tail", "nil", "nilList" and "nilString".
105+
As with other encoding packages, the "-" tag ignores fields.
106106
107-
The "-" tag ignores fields.
107+
type StructWithIgnoredField struct{
108+
Ignored uint `rlp:"-"`
109+
Field uint
110+
}
111+
112+
Go struct values encode/decode as RLP lists. There are two ways of influencing the mapping
113+
of fields to list elements. The "tail" tag, which may only be used on the last exported
114+
struct field, allows slurping up any excess list elements into a slice.
115+
116+
type StructWithTail struct{
117+
Field uint
118+
Tail []string `rlp:"tail"`
119+
}
108120
109-
The "tail" tag, which may only be used on the last exported struct field, allows slurping
110-
up any excess list elements into a slice. See examples for more details.
121+
The "optional" tag says that the field may be omitted if it is zero-valued. If this tag is
122+
used on a struct field, all subsequent public fields must also be declared optional.
111123
112-
The "nil" tag applies to pointer-typed fields and changes the decoding rules for the field
113-
such that input values of size zero decode as a nil pointer. This tag can be useful when
114-
decoding recursive types.
124+
When encoding a struct with optional fields, the output RLP list contains all values up to
125+
the last non-zero optional field.
115126
116-
type StructWithOptionalFoo struct {
117-
Foo *[20]byte `rlp:"nil"`
127+
When decoding into a struct, optional fields may be omitted from the end of the input
128+
list. For the example below, this means input lists of one, two, or three elements are
129+
accepted.
130+
131+
type StructWithOptionalFields struct{
132+
Required uint
133+
Optional1 uint `rlp:"optional"`
134+
Optional2 uint `rlp:"optional"`
135+
}
136+
137+
The "nil", "nilList" and "nilString" tags apply to pointer-typed fields only, and change
138+
the decoding rules for the field type. For regular pointer fields without the "nil" tag,
139+
input values must always match the required input length exactly and the decoder does not
140+
produce nil values. When the "nil" tag is set, input values of size zero decode as a nil
141+
pointer. This is especially useful for recursive types.
142+
143+
type StructWithNilField struct {
144+
Field *[3]byte `rlp:"nil"`
118145
}
119146
147+
In the example above, Field allows two possible input sizes. For input 0xC180 (a list
148+
containing an empty string) Field is set to nil after decoding. For input 0xC483000000 (a
149+
list containing a 3-byte string), Field is set to a non-nil array pointer.
150+
120151
RLP supports two kinds of empty values: empty lists and empty strings. When using the
121-
"nil" tag, the kind of empty value allowed for a type is chosen automatically. A struct
122-
field whose Go type is a pointer to an unsigned integer, string, boolean or byte
123-
array/slice expects an empty RLP string. Any other pointer field type encodes/decodes as
124-
an empty RLP list.
152+
"nil" tag, the kind of empty value allowed for a type is chosen automatically. A field
153+
whose Go type is a pointer to an unsigned integer, string, boolean or byte array/slice
154+
expects an empty RLP string. Any other pointer field type encodes/decodes as an empty RLP
155+
list.
125156
126157
The choice of null value can be made explicit with the "nilList" and "nilString" struct
127-
tags. Using these tags encodes/decodes a Go nil pointer value as the kind of empty
128-
RLP value defined by the tag.
158+
tags. Using these tags encodes/decodes a Go nil pointer value as the empty RLP value kind
159+
defined by the tag.
129160
*/
130161
package rlp

rlp/encode.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -546,15 +546,40 @@ func makeStructWriter(typ reflect.Type) (writer, error) {
546546
return nil, structFieldError{typ, f.index, f.info.writerErr}
547547
}
548548
}
549-
writer := func(val reflect.Value, w *encbuf) error {
550-
lh := w.list()
551-
for _, f := range fields {
552-
if err := f.info.writer(val.Field(f.index), w); err != nil {
553-
return err
549+
550+
var writer writer
551+
firstOptionalField := firstOptionalField(fields)
552+
if firstOptionalField == len(fields) {
553+
// This is the writer function for structs without any optional fields.
554+
writer = func(val reflect.Value, w *encbuf) error {
555+
lh := w.list()
556+
for _, f := range fields {
557+
if err := f.info.writer(val.Field(f.index), w); err != nil {
558+
return err
559+
}
554560
}
561+
w.listEnd(lh)
562+
return nil
563+
}
564+
} else {
565+
// If there are any "optional" fields, the writer needs to perform additional
566+
// checks to determine the output list length.
567+
writer = func(val reflect.Value, w *encbuf) error {
568+
lastField := len(fields) - 1
569+
for ; lastField >= firstOptionalField; lastField-- {
570+
if !val.Field(fields[lastField].index).IsZero() {
571+
break
572+
}
573+
}
574+
lh := w.list()
575+
for i := 0; i <= lastField; i++ {
576+
if err := fields[i].info.writer(val.Field(fields[i].index), w); err != nil {
577+
return err
578+
}
579+
}
580+
w.listEnd(lh)
581+
return nil
555582
}
556-
w.listEnd(lh)
557-
return nil
558583
}
559584
return writer, nil
560585
}

0 commit comments

Comments
 (0)