Repository: fxamacker/cbor
Pull request #526 (2024-04-25 20:49:29)

Add options to disable BinaryMarshaler/BinaryUnmarshaler support.
<!--
Thank you for your interest in contributing to fxamacker/cbor!
-->

### Description

By default, values whose type implements BinaryMarshaler encode to a byte string whose contents are the result of calling MarshalBinary, and decoding a byte string into a BinaryUnmarshaler calls UnmarshalBinary on the contents of the byte string. These options make it possible to disable both behaviors.

<!-- For code contributions, please complete all the items below this line. -->
<!-- For documentation-only contributions, please delete everything below this line. -->

#### PR Was Proposed and Welcomed in Currently Open Issue

- [ ] This PR was proposed and welcomed by maintainer(s) in issue #___
- [x] Closes or Updates Issue #525

#### Checklist (for code PR only, ignore for docs PR)

- [x] Include unit tests that cover the new code
- [x] Pass all unit tests 
- [x] Pass all lint checks in CI (goimports, gosec, staticcheck, etc.)
- [x] Sign each commit with your real name and email.  
      Last line of each commit message should be in this format:  
      Signed-off-by: Firstname Lastname <firstname.lastname@example.com>
- [x] Certify the Developer's Certificate of Origin 1.1
      (see next section).

#### Certify the Developer's Certificate of Origin 1.1

- [x] By marking this item as completed, I certify 
      the Developer Certificate of Origin 1.1.

```
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
```




Patch:
diff --git a/decode.go b/decode.go
index 48c8f6f..91aa5d1 100644
--- a/decode.go
+++ b/decode.go
@@ -630,6 +630,25 @@ func (bseem ByteSliceExpectedEncodingMode) valid() bool {
 	return bseem >= 0 && bseem < maxByteSliceExpectedEncodingMode
 }
 
+// BinaryUnmarshalerMode specifies how to decode into types that implement
+// encoding.BinaryUnmarshaler.
+type BinaryUnmarshalerMode int
+
+const (
+	// BinaryUnmarshalerByteString will invoke UnmarshalBinary on the contents of a CBOR byte
+	// string when decoding into a value that implements BinaryUnmarshaler.
+	BinaryUnmarshalerByteString BinaryUnmarshalerMode = iota
+
+	// BinaryUnmarshalerNone does not recognize BinaryUnmarshaler implementations during decode.
+	BinaryUnmarshalerNone
+
+	maxBinaryUnmarshalerMode
+)
+
+func (bum BinaryUnmarshalerMode) valid() bool {
+	return bum >= 0 && bum < maxBinaryUnmarshalerMode
+}
+
 // DecOptions specifies decoding options.
 type DecOptions struct {
 	// DupMapKey specifies whether to enforce duplicate map key.
@@ -751,6 +770,10 @@ type DecOptions struct {
 	// ByteSliceExpectedEncodingMode specifies how to decode a byte string NOT enclosed in an
 	// "expected later encoding" tag (RFC 8949 Section 3.4.5.2) into a Go byte slice.
 	ByteSliceExpectedEncoding ByteSliceExpectedEncodingMode
+
+	// BinaryUnmarshaler specifies how to decode into types that implement
+	// encoding.BinaryUnmarshaler.
+	BinaryUnmarshaler BinaryUnmarshalerMode
 }
 
 // DecMode returns DecMode with immutable options and no tags (safe for concurrency).
@@ -954,6 +977,10 @@ func (opts DecOptions) decMode() (*decMode, error) {
 		return nil, errors.New("cbor: invalid ByteSliceExpectedEncoding " + strconv.Itoa(int(opts.ByteSliceExpectedEncoding)))
 	}
 
+	if !opts.BinaryUnmarshaler.valid() {
+		return nil, errors.New("cbor: invalid BinaryUnmarshaler " + strconv.Itoa(int(opts.BinaryUnmarshaler)))
+	}
+
 	dm := decMode{
 		dupMapKey:                 opts.DupMapKey,
 		timeTag:                   opts.TimeTag,
@@ -979,6 +1006,7 @@ func (opts DecOptions) decMode() (*decMode, error) {
 		infDec:                    opts.Inf,
 		byteStringToTime:          opts.ByteStringToTime,
 		byteSliceExpectedEncoding: opts.ByteSliceExpectedEncoding,
+		binaryUnmarshaler:         opts.BinaryUnmarshaler,
 	}
 
 	return &dm, nil
@@ -1056,6 +1084,7 @@ type decMode struct {
 	infDec                    InfMode
 	byteStringToTime          ByteStringToTimeMode
 	byteSliceExpectedEncoding ByteSliceExpectedEncodingMode
+	binaryUnmarshaler         BinaryUnmarshalerMode
 }
 
 var defaultDecMode, _ = DecOptions{}.decMode()
@@ -1094,6 +1123,7 @@ func (dm *decMode) DecOptions() DecOptions {
 		Inf:                       dm.infDec,
 		ByteStringToTime:          dm.byteStringToTime,
 		ByteSliceExpectedEncoding: dm.byteSliceExpectedEncoding,
+		BinaryUnmarshaler:         dm.binaryUnmarshaler,
 	}
 }
 
@@ -1413,7 +1443,7 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin
 			return err
 		}
 		copied = copied || converted
-		return fillByteString(t, b, !copied, v, d.dm.byteStringToString)
+		return fillByteString(t, b, !copied, v, d.dm.byteStringToString, d.dm.binaryUnmarshaler)
 
 	case cborTypeTextString:
 		b, err := d.parseTextString()
@@ -1465,7 +1495,7 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin
 				return nil
 			}
 			if tInfo.nonPtrKind == reflect.Slice || tInfo.nonPtrKind == reflect.Array {
-				return fillByteString(t, b, !copied, v, ByteStringToStringForbidden)
+				return fillByteString(t, b, !copied, v, ByteStringToStringForbidden, d.dm.binaryUnmarshaler)
 			}
 			if bi.IsUint64() {
 				return fillPositiveInt(t, bi.Uint64(), v)
@@ -1487,7 +1517,7 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin
 				return nil
 			}
 			if tInfo.nonPtrKind == reflect.Slice || tInfo.nonPtrKind == reflect.Array {
-				return fillByteString(t, b, !copied, v, ByteStringToStringForbidden)
+				return fillByteString(t, b, !copied, v, ByteStringToStringForbidden, d.dm.binaryUnmarshaler)
 			}
 			if bi.IsInt64() {
 				return fillNegativeInt(t, bi.Int64(), v)
@@ -2885,8 +2915,8 @@ func fillFloat(t cborType, val float64, v reflect.Value) error {
 	return &UnmarshalTypeError{CBORType: t.String(), GoType: v.Type().String()}
 }
 
-func fillByteString(t cborType, val []byte, shared bool, v reflect.Value, bsts ByteStringToStringMode) error {
-	if reflect.PtrTo(v.Type()).Implements(typeBinaryUnmarshaler) {
+func fillByteString(t cborType, val []byte, shared bool, v reflect.Value, bsts ByteStringToStringMode, bum BinaryUnmarshalerMode) error {
+	if bum == BinaryUnmarshalerByteString && reflect.PtrTo(v.Type()).Implements(typeBinaryUnmarshaler) {
 		if v.CanAddr() {
 			v = v.Addr()
 			if u, ok := v.Interface().(encoding.BinaryUnmarshaler); ok {
diff --git a/encode.go b/encode.go
index 9859a8f..40f79da 100644
--- a/encode.go
+++ b/encode.go
@@ -443,6 +443,23 @@ func (bam ByteArrayMode) valid() bool {
 	return bam >= 0 && bam < maxByteArrayMode
 }
 
+// BinaryMarshalerMode specifies how to encode types that implement encoding.BinaryMarshaler.
+type BinaryMarshalerMode int
+
+const (
+	// BinaryMarshalerByteString encodes the output of MarshalBinary to a CBOR byte string.
+	BinaryMarshalerByteString BinaryMarshalerMode = iota
+
+	// BinaryMarshalerNone does not recognize BinaryMarshaler implementations during encode.
+	BinaryMarshalerNone
+
+	maxBinaryMarshalerMode
+)
+
+func (bmm BinaryMarshalerMode) valid() bool {
+	return bmm >= 0 && bmm < maxBinaryMarshalerMode
+}
+
 // EncOptions specifies encoding options.
 type EncOptions struct {
 	// Sort specifies sorting order.
@@ -493,6 +510,9 @@ type EncOptions struct {
 
 	// ByteArray specifies how to encode byte arrays.
 	ByteArray ByteArrayMode
+
+	// BinaryMarshaler specifies how to encode types that implement encoding.BinaryMarshaler.
+	BinaryMarshaler BinaryMarshalerMode
 }
 
 // CanonicalEncOptions returns EncOptions for "Canonical CBOR" encoding,
@@ -685,6 +705,9 @@ func (opts EncOptions) encMode() (*encMode, error) {
 	if !opts.ByteArray.valid() {
 		return nil, errors.New("cbor: invalid ByteArray " + strconv.Itoa(int(opts.ByteArray)))
 	}
+	if !opts.BinaryMarshaler.valid() {
+		return nil, errors.New("cbor: invalid BinaryMarshaler " + strconv.Itoa(int(opts.BinaryMarshaler)))
+	}
 	em := encMode{
 		sort:                 opts.Sort,
 		shortestFloat:        opts.ShortestFloat,
@@ -703,6 +726,7 @@ func (opts EncOptions) encMode() (*encMode, error) {
 		byteSlice:            opts.ByteSlice,
 		byteSliceEncodingTag: byteSliceEncodingTag,
 		byteArray:            opts.ByteArray,
+		binaryMarshaler:      opts.BinaryMarshaler,
 	}
 	return &em, nil
 }
@@ -733,6 +757,7 @@ type encMode struct {
 	byteSlice            ByteSliceMode
 	byteSliceEncodingTag uint64
 	byteArray            ByteArrayMode
+	binaryMarshaler      BinaryMarshalerMode
 }
 
 var defaultEncMode, _ = EncOptions{}.encMode()
@@ -809,21 +834,22 @@ func getMarshalerDecMode(indefLength IndefLengthMode, tagsMd TagsMode) *decMode
 // EncOptions returns user specified options used to create this EncMode.
 func (em *encMode) EncOptions() EncOptions {
 	return EncOptions{
-		Sort:          em.sort,
-		ShortestFloat: em.shortestFloat,
-		NaNConvert:    em.nanConvert,
-		InfConvert:    em.infConvert,
-		BigIntConvert: em.bigIntConvert,
-		Time:          em.time,
-		TimeTag:       em.timeTag,
-		IndefLength:   em.indefLength,
-		NilContainers: em.nilContainers,
-		TagsMd:        em.tagsMd,
-		OmitEmpty:     em.omitEmpty,
-		String:        em.stringType,
-		FieldName:     em.fieldName,
-		ByteSlice:     em.byteSlice,
-		ByteArray:     em.byteArray,
+		Sort:            em.sort,
+		ShortestFloat:   em.shortestFloat,
+		NaNConvert:      em.nanConvert,
+		InfConvert:      em.infConvert,
+		BigIntConvert:   em.bigIntConvert,
+		Time:            em.time,
+		TimeTag:         em.timeTag,
+		IndefLength:     em.indefLength,
+		NilContainers:   em.nilContainers,
+		TagsMd:          em.tagsMd,
+		OmitEmpty:       em.omitEmpty,
+		String:          em.stringType,
+		FieldName:       em.fieldName,
+		ByteSlice:       em.byteSlice,
+		ByteArray:       em.byteArray,
+		BinaryMarshaler: em.binaryMarshaler,
 	}
 }
 
@@ -1513,7 +1539,16 @@ func encodeBigInt(e *bytes.Buffer, em *encMode, v reflect.Value) error {
 	return nil
 }
 
-func encodeBinaryMarshalerType(e *bytes.Buffer, em *encMode, v reflect.Value) error {
+type binaryMarshalerEncoder struct {
+	alternateEncode  encodeFunc
+	alternateIsEmpty isEmptyFunc
+}
+
+func (bme binaryMarshalerEncoder) encode(e *bytes.Buffer, em *encMode, v reflect.Value) error {
+	if em.binaryMarshaler != BinaryMarshalerByteString {
+		return bme.alternateEncode(e, em, v)
+	}
+
 	vt := v.Type()
 	m, ok := v.Interface().(encoding.BinaryMarshaler)
 	if !ok {
@@ -1533,6 +1568,24 @@ func encodeBinaryMarshalerType(e *bytes.Buffer, em *encMode, v reflect.Value) er
 	return nil
 }
 
+func (bme binaryMarshalerEncoder) isEmpty(em *encMode, v reflect.Value) (bool, error) {
+	if em.binaryMarshaler != BinaryMarshalerByteString {
+		return bme.alternateIsEmpty(em, v)
+	}
+
+	m, ok := v.Interface().(encoding.BinaryMarshaler)
+	if !ok {
+		pv := reflect.New(v.Type())
+		pv.Elem().Set(v)
+		m = pv.Interface().(encoding.BinaryMarshaler)
+	}
+	data, err := m.MarshalBinary()
+	if err != nil {
+		return false, err
+	}
+	return len(data) == 0, nil
+}
+
 func encodeMarshalerType(e *bytes.Buffer, em *encMode, v reflect.Value) error {
 	if em.tagsMd == TagsForbidden && v.Type() == typeRawTag {
 		return errors.New("cbor: cannot encode cbor.RawTag when TagsMd is TagsForbidden")
@@ -1618,7 +1671,7 @@ var (
 	typeByteString      = reflect.TypeOf(ByteString(""))
 )
 
-func getEncodeFuncInternal(t reflect.Type) (encodeFunc, isEmptyFunc) {
+func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc) {
 	k := t.Kind()
 	if k == reflect.Ptr {
 		return getEncodeIndirectValueFunc(t), isEmptyPtr
@@ -1641,7 +1694,15 @@ func getEncodeFuncInternal(t reflect.Type) (encodeFunc, isEmptyFunc) {
 		return encodeMarshalerType, alwaysNotEmpty
 	}
 	if reflect.PtrTo(t).Implements(typeBinaryMarshaler) {
-		return encodeBinaryMarshalerType, isEmptyBinaryMarshaler
+		defer func() {
+			// capture encoding method used for modes that disable BinaryMarshaler
+			bme := binaryMarshalerEncoder{
+				alternateEncode:  ef,
+				alternateIsEmpty: ief,
+			}
+			ef = bme.encode
+			ief = bme.isEmpty
+		}()
 	}
 	switch k {
 	case reflect.Bool:
@@ -1795,20 +1856,6 @@ func isEmptyStruct(em *encMode, v reflect.Value) (bool, error) {
 	return true, nil
 }
 
-func isEmptyBinaryMarshaler(_ *encMode, v reflect.Value) (bool, error) {
-	m, ok := v.Interface().(encoding.BinaryMarshaler)
-	if !ok {
-		pv := reflect.New(v.Type())
-		pv.Elem().Set(v)
-		m = pv.Interface().(encoding.BinaryMarshaler)
-	}
-	data, err := m.MarshalBinary()
-	if err != nil {
-		return false, err
-	}
-	return len(data) == 0, nil
-}
-
 func cannotFitFloat32(f64 float64) bool {
 	f32 := float32(f64)
 	return float64(f32) != f64


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 7a9c901..2e7fc77 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4923,6 +4923,7 @@ func TestDecOptions(t *testing.T) {
 		Inf:                       InfDecodeForbidden,
 		ByteStringToTime:          ByteStringToTimeAllowed,
 		ByteSliceExpectedEncoding: ByteSliceToByteStringWithExpectedConversionToBase64,
+		BinaryUnmarshaler:         BinaryUnmarshalerNone,
 	}
 	ov := reflect.ValueOf(opts1)
 	for i := 0; i < ov.NumField(); i++ {
@@ -9926,3 +9927,83 @@ func TestUnmarshalByteStringTextConversion(t *testing.T) {
 		})
 	}
 }
+
+func TestDecModeInvalidBinaryUnmarshaler(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		opts         DecOptions
+		wantErrorMsg string
+	}{
+		{
+			name:         "below range of valid modes",
+			opts:         DecOptions{BinaryUnmarshaler: -1},
+			wantErrorMsg: "cbor: invalid BinaryUnmarshaler -1",
+		},
+		{
+			name:         "above range of valid modes",
+			opts:         DecOptions{BinaryUnmarshaler: 101},
+			wantErrorMsg: "cbor: invalid BinaryUnmarshaler 101",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := tc.opts.DecMode()
+			if err == nil {
+				t.Errorf("DecMode() didn't return an error")
+			} else if err.Error() != tc.wantErrorMsg {
+				t.Errorf("DecMode() returned error %q, want %q", err.Error(), tc.wantErrorMsg)
+			}
+		})
+	}
+}
+
+type testBinaryUnmarshaler []byte
+
+func (bu *testBinaryUnmarshaler) UnmarshalBinary(_ []byte) error {
+	*bu = []byte("UnmarshalBinary")
+	return nil
+}
+
+func TestBinaryUnmarshalerMode(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		opts DecOptions
+		in   []byte
+		want interface{}
+	}{
+		{
+			name: "UnmarshalBinary is called by default",
+			opts: DecOptions{},
+			in:   []byte("\x45hello"), // 'hello'
+			want: testBinaryUnmarshaler("UnmarshalBinary"),
+		},
+		{
+			name: "UnmarshalBinary is called with BinaryUnmarshalerByteString",
+			opts: DecOptions{BinaryUnmarshaler: BinaryUnmarshalerByteString},
+			in:   []byte("\x45hello"), // 'hello'
+			want: testBinaryUnmarshaler("UnmarshalBinary"),
+		},
+		{
+			name: "default byte slice unmarshaling behavior is used with BinaryUnmarshalerNone",
+			opts: DecOptions{BinaryUnmarshaler: BinaryUnmarshalerNone},
+			in:   []byte("\x45hello"), // 'hello'
+			want: testBinaryUnmarshaler("hello"),
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			dm, err := tc.opts.DecMode()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			gotrv := reflect.New(reflect.TypeOf(tc.want))
+			if err := dm.Unmarshal(tc.in, gotrv.Interface()); err != nil {
+				t.Fatal(err)
+			}
+
+			got := gotrv.Elem().Interface()
+			if !reflect.DeepEqual(tc.want, got) {
+				t.Errorf("want: %v, got: %v", tc.want, got)
+			}
+		})
+	}
+}
diff --git a/encode_test.go b/encode_test.go
index bbca54f..835c534 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -3722,21 +3722,22 @@ func TestEncOptionsTagsForbidden(t *testing.T) {
 
 func TestEncOptions(t *testing.T) {
 	opts1 := EncOptions{
-		Sort:          SortBytewiseLexical,
-		ShortestFloat: ShortestFloat16,
-		NaNConvert:    NaNConvertPreserveSignal,
-		InfConvert:    InfConvertNone,
-		BigIntConvert: BigIntConvertNone,
-		Time:          TimeRFC3339Nano,
-		TimeTag:       EncTagRequired,
-		IndefLength:   IndefLengthForbidden,
-		NilContainers: NilContainerAsEmpty,
-		TagsMd:        TagsAllowed,
-		OmitEmpty:     OmitEmptyGoValue,
-		String:        StringToByteString,
-		FieldName:     FieldNameToByteString,
-		ByteSlice:     ByteSliceToByteStringWithExpectedConversionToBase16,
-		ByteArray:     ByteArrayToArray,
+		Sort:            SortBytewiseLexical,
+		ShortestFloat:   ShortestFloat16,
+		NaNConvert:      NaNConvertPreserveSignal,
+		InfConvert:      InfConvertNone,
+		BigIntConvert:   BigIntConvertNone,
+		Time:            TimeRFC3339Nano,
+		TimeTag:         EncTagRequired,
+		IndefLength:     IndefLengthForbidden,
+		NilContainers:   NilContainerAsEmpty,
+		TagsMd:          TagsAllowed,
+		OmitEmpty:       OmitEmptyGoValue,
+		String:          StringToByteString,
+		FieldName:       FieldNameToByteString,
+		ByteSlice:       ByteSliceToByteStringWithExpectedConversionToBase16,
+		ByteArray:       ByteArrayToArray,
+		BinaryMarshaler: BinaryMarshalerNone,
 	}
 	ov := reflect.ValueOf(opts1)
 	for i := 0; i < ov.NumField(); i++ {
@@ -4674,3 +4675,102 @@ func TestMarshalByteSliceMode(t *testing.T) {
 		})
 	}
 }
+
+func TestEncModeInvalidBinaryMarshalerMode(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		opts         EncOptions
+		wantErrorMsg string
+	}{
+		{
+			name:         "",
+			opts:         EncOptions{BinaryMarshaler: -1},
+			wantErrorMsg: "cbor: invalid BinaryMarshaler -1",
+		},
+		{
+			name:         "",
+			opts:         EncOptions{BinaryMarshaler: 101},
+			wantErrorMsg: "cbor: invalid BinaryMarshaler 101",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := tc.opts.EncMode()
+			if err == nil {
+				t.Errorf("EncMode() didn't return an error")
+			} else if err.Error() != tc.wantErrorMsg {
+				t.Errorf("EncMode() returned error %q, want %q", err.Error(), tc.wantErrorMsg)
+			}
+		})
+	}
+}
+
+type testBinaryMarshaler struct {
+	StringField  string `cbor:"s"`
+	IntegerField int64  `cbor:"i"`
+}
+
+func (testBinaryMarshaler) MarshalBinary() ([]byte, error) {
+	return []byte("MarshalBinary"), nil
+}
+
+func TestBinaryMarshalerMode(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		opts EncOptions
+		in   interface{}
+		want []byte
+	}{
+		{
+			name: "struct implementing BinaryMarshaler is encoded as MarshalBinary's output in a byte string by default",
+			opts: EncOptions{},
+			in: testBinaryMarshaler{
+				StringField:  "z",
+				IntegerField: 3,
+			},
+			want: []byte("\x4dMarshalBinary"), // 'MarshalBinary'
+		},
+		{
+			name: "struct implementing BinaryMarshaler is encoded as MarshalBinary's output in a byte string with BinaryMarshalerByteString",
+			opts: EncOptions{BinaryMarshaler: BinaryMarshalerByteString},
+			in: testBinaryMarshaler{
+				StringField:  "z",
+				IntegerField: 3,
+			},
+			want: []byte("\x4dMarshalBinary"), // 'MarshalBinary'
+		},
+		{
+			name: "struct implementing BinaryMarshaler is encoded to map with BinaryMarshalerNone",
+			opts: EncOptions{BinaryMarshaler: BinaryMarshalerNone},
+			in: testBinaryMarshaler{
+				StringField:  "z",
+				IntegerField: 3,
+			},
+			want: hexDecode("a26173617a616903"), // {"s": "z", "i": 3}
+		},
+		{
+			name: "struct implementing BinaryMarshaler is encoded to map with BinaryMarshalerNone",
+			opts: EncOptions{BinaryMarshaler: BinaryMarshalerNone},
+			in: testBinaryMarshaler{
+				StringField:  "z",
+				IntegerField: 3,
+			},
+			want: hexDecode("a26173617a616903"), // {"s": "z", "i": 3}
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			em, err := tc.opts.EncMode()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			got, err := em.Marshal(tc.in)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if !bytes.Equal(tc.want, got) {
+				t.Errorf("unexpected output, want: 0x%x, got 0x%x", tc.want, got)
+			}
+		})
+	}
+}