Skip to content

Commit

Permalink
Merge pull request #352 from dedefer/add_nil_containers_encode_options
Browse files Browse the repository at this point in the history
Add option to make nil containers encode as empty containers.

The newly added encoding option is likely to be renamed before next release.
  • Loading branch information
fxamacker authored Dec 30, 2022
2 parents 7c3a599 + 64ed7b7 commit 75d0384
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 3 deletions.
17 changes: 17 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,23 @@ func (m IndefLengthMode) valid() bool {
return m < maxIndefLengthMode
}

// NilContainersMode specifies how to encode []Type(nil) and map[Key]Type(nil).
type NilContainersMode int

const (
// NullForNil enforces null for []Type(nil)/map[Key]Type(nil).
NullForNil NilContainersMode = iota

// EmptyForNil enforces empty map/list for []Type(nil)/map[Key]Type(nil).
EmptyForNil

maxNilContainersMode
)

func (m NilContainersMode) valid() bool {
return m < maxNilContainersMode
}

// TagsMode specifies whether to allow CBOR tags.
type TagsMode int

Expand Down
14 changes: 11 additions & 3 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ type EncOptions struct {
// IndefLength specifies whether to allow indefinite length CBOR items.
IndefLength IndefLengthMode

// NilContainers specifies how to encode map[Key]Type(nil)/[]Type(nil)
NilContainers NilContainersMode

// TagsMd specifies whether to allow CBOR tags (major type 6).
TagsMd TagsMode
}
Expand Down Expand Up @@ -464,6 +467,9 @@ func (opts EncOptions) encMode() (*encMode, error) {
if !opts.IndefLength.valid() {
return nil, errors.New("cbor: invalid IndefLength " + strconv.Itoa(int(opts.IndefLength)))
}
if !opts.NilContainers.valid() {
return nil, errors.New("cbor: invalid NilContainers " + strconv.Itoa(int(opts.NilContainers)))
}
if !opts.TagsMd.valid() {
return nil, errors.New("cbor: invalid TagsMd " + strconv.Itoa(int(opts.TagsMd)))
}
Expand All @@ -479,6 +485,7 @@ func (opts EncOptions) encMode() (*encMode, error) {
time: opts.Time,
timeTag: opts.TimeTag,
indefLength: opts.IndefLength,
nilContainers: opts.NilContainers,
tagsMd: opts.TagsMd,
}
return &em, nil
Expand All @@ -501,6 +508,7 @@ type encMode struct {
time TimeMode
timeTag EncTagMode
indefLength IndefLengthMode
nilContainers NilContainersMode
tagsMd TagsMode
}

Expand Down Expand Up @@ -787,7 +795,7 @@ func encodeFloat64(e *encoderBuffer, f64 float64) error {

func encodeByteString(e *encoderBuffer, em *encMode, v reflect.Value) error {
vk := v.Kind()
if vk == reflect.Slice && v.IsNil() {
if vk == reflect.Slice && v.IsNil() && em.nilContainers == NullForNil {
e.Write(cborNil)
return nil
}
Expand Down Expand Up @@ -824,7 +832,7 @@ type arrayEncodeFunc struct {
}

func (ae arrayEncodeFunc) encode(e *encoderBuffer, em *encMode, v reflect.Value) error {
if v.Kind() == reflect.Slice && v.IsNil() {
if v.Kind() == reflect.Slice && v.IsNil() && em.nilContainers == NullForNil {
e.Write(cborNil)
return nil
}
Expand All @@ -849,7 +857,7 @@ type mapEncodeFunc struct {
}

func (me mapEncodeFunc) encode(e *encoderBuffer, em *encMode, v reflect.Value) error {
if v.IsNil() {
if v.IsNil() && em.nilContainers == NullForNil {
e.Write(cborNil)
return nil
}
Expand Down
45 changes: 45 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2857,6 +2857,50 @@ func TestInvalidInfConvert(t *testing.T) {
}
}

func TestNilContainers(t *testing.T) {
nilContainersNull := EncOptions{NilContainers: NullForNil}
nilContainersEmpty := EncOptions{NilContainers: EmptyForNil}
testCases := []struct {
name string
v interface{}
opts EncOptions
wantCborData []byte
}{
{"map(nil) as null", map[string]string(nil), nilContainersNull, hexDecode("f6")},
{"map(nil) as empty map", map[string]string(nil), nilContainersEmpty, hexDecode("a0")},

{"slice(nil) as null", []int(nil), nilContainersNull, hexDecode("f6")},
{"slice(nil) as empty list", []int(nil), nilContainersEmpty, hexDecode("80")},

{"[]byte(nil) as null", []byte(nil), nilContainersNull, hexDecode("f6")},
{"[]byte(nil) as empty bytestring", []byte(nil), nilContainersEmpty, hexDecode("40")},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
em, err := tc.opts.EncMode()
if err != nil {
t.Errorf("EncMode() returned an error %v", err)
}
b, err := em.Marshal(tc.v)
if err != nil {
t.Errorf("Marshal(%v) returned error %v", tc.v, err)
} else if !bytes.Equal(b, tc.wantCborData) {
t.Errorf("Marshal(%v) = 0x%x, want 0x%x", tc.v, b, tc.wantCborData)
}
})
}
}

func TestInvalidNilContainers(t *testing.T) {
wantErrorMsg := "cbor: invalid NilContainers 100"
_, err := EncOptions{NilContainers: NilContainersMode(100)}.EncMode()
if err == nil {
t.Errorf("EncMode() didn't return an error")
} else if err.Error() != wantErrorMsg {
t.Errorf("EncMode() returned error %q, want %q", err.Error(), wantErrorMsg)
}
}

// Keith Randall's workaround for constant propagation issue https://github.com/golang/go/issues/36400
const (
// qnan 32 bits constants
Expand Down Expand Up @@ -3311,6 +3355,7 @@ func TestEncOptions(t *testing.T) {
Time: TimeRFC3339Nano,
TimeTag: EncTagRequired,
IndefLength: IndefLengthForbidden,
NilContainers: NullForNil,
TagsMd: TagsAllowed,
}
em, err := opts1.EncMode()
Expand Down

0 comments on commit 75d0384

Please sign in to comment.