Repository: fxamacker/cbor
Pull request #513 (2024-04-02 20:36:43)

Allow rejection of NaN and Inf float values on encode and decode.
<!--
Thank you for your interest in contributing to fxamacker/cbor!
-->

### Description

Implements #512 to allow users to error rather than encode or decode NaN and Inf floating-point values.

<!-- 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 #512

#### 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.
```



benluddy: > Thoughts?

Sounds great. I had been thinking to myself, "what if a user needs to distinguish between ill-formed input and well-formed input that is rejected by the application's protocol." Now I realize that that case can be served just fine by a second call to `Wellformed` using a permissive decode mode on error cases.

I'll implement your recommendations today.
benluddy: > I'll implement your recommendations today.

Done. Please take another look when you have a chance.
benluddy: D'oh! Thank you. Added test cases to catch that and fixed.

Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 9eaa970..ca2ac33 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,14 +32,14 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
+      uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
       with:
         languages: ${{ matrix.language }}
 
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
+      uses: github/codeql-action/autobuild@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
+      uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
diff --git a/decode.go b/decode.go
index e37931f..6465191 100644
--- a/decode.go
+++ b/decode.go
@@ -539,6 +539,43 @@ func NewSimpleValueRegistryFromDefaults(fns ...func(*SimpleValueRegistry) error)
 	return &r, nil
 }
 
+// NaNMode specifies how to decode floating-point values (major type 7, additional information 25
+// through 27) representing NaN (not-a-number).
+type NaNMode int
+
+const (
+	// NaNDecodeAllowed will decode NaN values to Go float32 or float64.
+	NaNDecodeAllowed NaNMode = iota
+
+	// NaNDecodeForbidden will return an UnacceptableDataItemError on an attempt to decode a NaN value.
+	NaNDecodeForbidden
+
+	maxNaNDecode
+)
+
+func (ndm NaNMode) valid() bool {
+	return ndm >= 0 && ndm < maxNaNDecode
+}
+
+// InfMode specifies how to decode floating-point values (major type 7, additional information 25
+// through 27) representing positive or negative infinity.
+type InfMode int
+
+const (
+	// InfDecodeAllowed will decode infinite values to Go float32 or float64.
+	InfDecodeAllowed InfMode = iota
+
+	// InfDecodeForbidden will return an UnacceptableDataItemError on an attempt to decode an
+	// infinite value.
+	InfDecodeForbidden
+
+	maxInfDecode
+)
+
+func (idm InfMode) valid() bool {
+	return idm >= 0 && idm < maxInfDecode
+}
+
 // DecOptions specifies decoding options.
 type DecOptions struct {
 	// DupMapKey specifies whether to enforce duplicate map key.
@@ -645,6 +682,14 @@ type DecOptions struct {
 	// Users may provide a custom SimpleValueRegistry constructed via
 	// NewSimpleValueRegistryFromDefaults.
 	SimpleValues *SimpleValueRegistry
+
+	// NaN specifies how to decode floating-point values (major type 7, additional information
+	// 25 through 27) representing NaN (not-a-number).
+	NaN NaNMode
+
+	// Inf specifies how to decode floating-point values (major type 7, additional information
+	// 25 through 27) representing positive or negative infinity.
+	Inf InfMode
 }
 
 // DecMode returns DecMode with immutable options and no tags (safe for concurrency).
@@ -815,6 +860,14 @@ func (opts DecOptions) decMode() (*decMode, error) {
 		return nil, errors.New("cbor: invalid TimeTagToAny " + strconv.Itoa(int(opts.TimeTagToAny)))
 	}
 
+	if !opts.NaN.valid() {
+		return nil, errors.New("cbor: invalid NaNDec " + strconv.Itoa(int(opts.NaN)))
+	}
+
+	if !opts.Inf.valid() {
+		return nil, errors.New("cbor: invalid InfDec " + strconv.Itoa(int(opts.Inf)))
+	}
+
 	dm := decMode{
 		dupMapKey:             opts.DupMapKey,
 		timeTag:               opts.TimeTag,
@@ -836,6 +889,8 @@ func (opts DecOptions) decMode() (*decMode, error) {
 		unrecognizedTagToAny:  opts.UnrecognizedTagToAny,
 		timeTagToAny:          opts.TimeTagToAny,
 		simpleValues:          simpleValues,
+		nanDec:                opts.NaN,
+		infDec:                opts.Inf,
 	}
 
 	return &dm, nil
@@ -909,6 +964,8 @@ type decMode struct {
 	unrecognizedTagToAny  UnrecognizedTagToAnyMode
 	timeTagToAny          TimeTagToAnyMode
 	simpleValues          *SimpleValueRegistry
+	nanDec                NaNMode
+	infDec                InfMode
 }
 
 var defaultDecMode, _ = DecOptions{}.decMode()
@@ -943,6 +1000,8 @@ func (dm *decMode) DecOptions() DecOptions {
 		UnrecognizedTagToAny:  dm.unrecognizedTagToAny,
 		TimeTagToAny:          dm.timeTagToAny,
 		SimpleValues:          simpleValues,
+		NaN:                   dm.nanDec,
+		Inf:                   dm.infDec,
 	}
 }
 
diff --git a/encode.go b/encode.go
index 79f3298..7a7eb84 100644
--- a/encode.go
+++ b/encode.go
@@ -240,6 +240,9 @@ const (
 	// NaN payload.
 	NaNConvertQuiet
 
+	// NaNConvertReject returns UnsupportedValueError on attempts to encode a NaN value.
+	NaNConvertReject
+
 	maxNaNConvert
 )
 
@@ -258,6 +261,9 @@ const (
 	// InfConvertNone never converts (used by CTAP2 Canonical CBOR).
 	InfConvertNone
 
+	// InfConvertReject returns UnsupportedValueError on attempts to encode an infinite value.
+	InfConvertReject
+
 	maxInfConvert
 )
 
@@ -908,7 +914,10 @@ func encodeFloat(e *encoderBuffer, em *encMode, v reflect.Value) error {
 
 func encodeInf(e *encoderBuffer, em *encMode, v reflect.Value) error {
 	f64 := v.Float()
-	if em.infConvert == InfConvertFloat16 {
+	switch em.infConvert {
+	case InfConvertReject:
+		return &UnsupportedValueError{msg: "floating-point infinity"}
+	case InfConvertFloat16:
 		if f64 > 0 {
 			e.Write(cborPositiveInfinity)
 		} else {
@@ -935,6 +944,9 @@ func encodeNaN(e *encoderBuffer, em *encMode, v reflect.Value) error {
 		f32 := float32NaNFromReflectValue(v)
 		return encodeFloat32(e, f32)
 
+	case NaNConvertReject:
+		return &UnsupportedValueError{msg: "floating-point NaN"}
+
 	default: // NaNConvertPreserveSignal, NaNConvertQuiet
 		if v.Kind() == reflect.Float64 {
 			f64 := v.Float()
diff --git a/valid.go b/valid.go
index 11013fa..15ae19f 100644
--- a/valid.go
+++ b/valid.go
@@ -7,7 +7,10 @@ import (
 	"encoding/binary"
 	"errors"
 	"io"
+	"math"
 	"strconv"
+
+	"github.com/x448/float16"
 )
 
 // SyntaxError is a description of a CBOR syntax error.
@@ -297,6 +300,11 @@ func (d *decoder) wellformedHead() (t cborType, ai byte, val uint64, err error)
 		}
 		val = uint64(binary.BigEndian.Uint16(d.data[d.off : d.off+2]))
 		d.off += 2
+		if t == cborTypePrimitives {
+			if err := d.acceptableFloat(float64(float16.Frombits(uint16(val)).Float32())); err != nil {
+				return 0, 0, 0, err
+			}
+		}
 		return t, ai, val, nil
 	}
 	if ai == 26 {
@@ -305,6 +313,11 @@ func (d *decoder) wellformedHead() (t cborType, ai byte, val uint64, err error)
 		}
 		val = uint64(binary.BigEndian.Uint32(d.data[d.off : d.off+4]))
 		d.off += 4
+		if t == cborTypePrimitives {
+			if err := d.acceptableFloat(float64(math.Float32frombits(uint32(val)))); err != nil {
+				return 0, 0, 0, err
+			}
+		}
 		return t, ai, val, nil
 	}
 	if ai == 27 {
@@ -313,6 +326,11 @@ func (d *decoder) wellformedHead() (t cborType, ai byte, val uint64, err error)
 		}
 		val = binary.BigEndian.Uint64(d.data[d.off : d.off+8])
 		d.off += 8
+		if t == cborTypePrimitives {
+			if err := d.acceptableFloat(math.Float64frombits(val)); err != nil {
+				return 0, 0, 0, err
+			}
+		}
 		return t, ai, val, nil
 	}
 	if ai == 31 {
@@ -327,3 +345,19 @@ func (d *decoder) wellformedHead() (t cborType, ai byte, val uint64, err error)
 	// ai == 28, 29, 30
 	return 0, 0, 0, &SyntaxError{"cbor: invalid additional information " + strconv.Itoa(int(ai)) + " for type " + t.String()}
 }
+
+func (d *decoder) acceptableFloat(f float64) error {
+	switch {
+	case d.dm.nanDec == NaNDecodeForbidden && math.IsNaN(f):
+		return &UnacceptableDataItemError{
+			CBORType: cborTypePrimitives.String(),
+			Message:  "floating-point NaN",
+		}
+	case d.dm.infDec == InfDecodeForbidden && math.IsInf(f, 0):
+		return &UnacceptableDataItemError{
+			CBORType: cborTypePrimitives.String(),
+			Message:  "floating-point infinity",
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/decode_test.go b/decode_test.go
index afe287a..89e47d1 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4919,6 +4919,8 @@ func TestDecOptions(t *testing.T) {
 		UnrecognizedTagToAny:  UnrecognizedTagContentToAny,
 		TimeTagToAny:          TimeTagToRFC3339,
 		SimpleValues:          simpleValues,
+		NaN:                   NaNDecodeForbidden,
+		Inf:                   InfDecodeForbidden,
 	}
 	ov := reflect.ValueOf(opts1)
 	for i := 0; i < ov.NumField(); i++ {
@@ -8908,3 +8910,519 @@ func TestDecModeTimeTagToAny(t *testing.T) {
 		})
 	}
 }
+
+func TestDecModeInvalidNaNDec(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		opts         DecOptions
+		wantErrorMsg string
+	}{
+		{
+			name:         "below range of valid modes",
+			opts:         DecOptions{NaN: -1},
+			wantErrorMsg: "cbor: invalid NaNDec -1",
+		},
+		{
+			name:         "above range of valid modes",
+			opts:         DecOptions{NaN: 101},
+			wantErrorMsg: "cbor: invalid NaNDec 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)
+			}
+		})
+	}
+}
+
+func TestNaNDecMode(t *testing.T) {
+	for _, tc := range []struct {
+		opt    NaNMode
+		src    []byte
+		dst    interface{}
+		reject bool
+	}{
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("197e00"),
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("1a7fc00000"),
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("1b7ff8000000000000"),
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(float32),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(float64),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(time.Time),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(float32),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(float64),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(time.Time),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(float32),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(float64),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(time.Time),
+			reject: false,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f97e00"),
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f97e00"),
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f97e00"),
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("f97e00"),
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa7fc00000"),
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa7fc00000"),
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa7fc00000"),
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fa7fc00000"),
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb7ff8000000000000"),
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb7ff8000000000000"),
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb7ff8000000000000"),
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    NaNDecodeForbidden,
+			src:    hexDecode("fb7ff8000000000000"),
+			dst:    new(time.Time),
+			reject: true,
+		},
+	} {
+		t.Run(fmt.Sprintf("mode=%d/0x%x into %s", tc.opt, tc.src, reflect.TypeOf(tc.dst).String()), func(t *testing.T) {
+			dm, err := DecOptions{NaN: tc.opt}.DecMode()
+			if err != nil {
+				t.Fatal(err)
+			}
+			want := &UnacceptableDataItemError{
+				CBORType: cborTypePrimitives.String(),
+				Message:  "floating-point NaN",
+			}
+			if got := dm.Unmarshal(tc.src, tc.dst); got != nil {
+				if tc.reject {
+					if !reflect.DeepEqual(want, got) {
+						t.Errorf("want error: %v, got error: %v", want, got)
+					}
+				} else {
+					t.Errorf("unexpected error: %v", got)
+				}
+			} else if tc.reject {
+				t.Error("unexpected nil error")
+			}
+		})
+	}
+}
+
+func TestDecModeInvalidInfDec(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		opts         DecOptions
+		wantErrorMsg string
+	}{
+		{
+			name:         "below range of valid modes",
+			opts:         DecOptions{Inf: -1},
+			wantErrorMsg: "cbor: invalid InfDec -1",
+		},
+		{
+			name:         "above range of valid modes",
+			opts:         DecOptions{Inf: 101},
+			wantErrorMsg: "cbor: invalid InfDec 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)
+			}
+		})
+	}
+}
+
+func TestInfDecMode(t *testing.T) {
+	for _, tc := range []struct {
+		opt    InfMode
+		src    []byte
+		dst    interface{}
+		reject bool
+	}{
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("197c00"),
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("1a7f800000"), // Infinity
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("1b7ff0000000000000"), // Infinity
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(float32),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(float64),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f90000"), // 0.0
+			dst:    new(time.Time),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(float32),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(float64),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa47c35000"), // 100000.0
+			dst:    new(time.Time),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(interface{}),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(float32),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(float64),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb3ff199999999999a"), // 1.1
+			dst:    new(time.Time),
+			reject: false,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f97c00"), // Infinity
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f97c00"), // Infinity
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f97c00"), // Infinity
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f97c00"), // Infinity
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f9fc00"), // -Infinity
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f9fc00"), // -Infinity
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f9fc00"), // -Infinity
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("f9fc00"), // -Infinity
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa7f800000"), // Infinity
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa7f800000"), // Infinity
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa7f800000"), // Infinity
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fa7f800000"), // Infinity
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("faff800000"), // -Infinity
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("faff800000"), // -Infinity
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("faff800000"), // -Infinity
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("faff800000"), // -Infinity
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb7ff0000000000000"), // Infinity
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb7ff0000000000000"), // Infinity
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb7ff0000000000000"), // Infinity
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fb7ff0000000000000"), // Infinity
+			dst:    new(time.Time),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fbfff0000000000000"), // -Infinity
+			dst:    new(interface{}),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fbfff0000000000000"), // -Infinity
+			dst:    new(float32),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fbfff0000000000000"), // -Infinity
+			dst:    new(float64),
+			reject: true,
+		},
+		{
+			opt:    InfDecodeForbidden,
+			src:    hexDecode("fbfff0000000000000"), // -Infinity
+			dst:    new(time.Time),
+			reject: true,
+		},
+	} {
+		t.Run(fmt.Sprintf("mode=%d/0x%x into %s", tc.opt, tc.src, tc.dst), func(t *testing.T) {
+			dm, err := DecOptions{Inf: tc.opt}.DecMode()
+			if err != nil {
+				t.Fatal(err)
+			}
+			want := &UnacceptableDataItemError{
+				CBORType: cborTypePrimitives.String(),
+				Message:  "floating-point infinity",
+			}
+			if got := dm.Unmarshal(tc.src, tc.dst); got != nil {
+				if tc.reject {
+					if !reflect.DeepEqual(want, got) {
+						t.Errorf("want error: %v, got error: %v", want, got)
+					}
+				} else {
+					t.Errorf("unexpected error: %v", got)
+				}
+			} else if tc.reject {
+				t.Error("unexpected nil error")
+			}
+		})
+	}
+}
diff --git a/encode_test.go b/encode_test.go
index 2ed416b..5eb4f6e 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -3004,7 +3004,7 @@ func TestInfConvert(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			em, err := tc.opts.EncMode()
 			if err != nil {
-				t.Errorf("EncMode() returned an error %v", err)
+				t.Fatalf("EncMode() returned an error %v", err)
 			}
 			b, err := em.Marshal(tc.v)
 			if err != nil {
@@ -3013,6 +3013,23 @@ func TestInfConvert(t *testing.T) {
 				t.Errorf("Marshal(%v) = 0x%x, want 0x%x", tc.v, b, tc.wantCborData)
 			}
 		})
+		var vName string
+		switch v := tc.v.(type) {
+		case float32:
+			vName = fmt.Sprintf("0x%x", math.Float32bits(v))
+		case float64:
+			vName = fmt.Sprintf("0x%x", math.Float64bits(v))
+		}
+		t.Run("reject inf "+vName, func(t *testing.T) {
+			em, err := EncOptions{InfConvert: InfConvertReject}.EncMode()
+			if err != nil {
+				t.Fatalf("EncMode() returned an error %v", err)
+			}
+			want := &UnsupportedValueError{msg: "floating-point infinity"}
+			if _, got := em.Marshal(tc.v); !reflect.DeepEqual(want, got) {
+				t.Errorf("expected Marshal(%v) to return error: %v, got: %v", tc.v, want, got)
+			}
+		})
 	}
 }
 
@@ -3318,6 +3335,13 @@ func TestNaNConvert(t *testing.T) {
 		}},
 	}
 	for _, tc := range testCases {
+		var vName string
+		switch v := tc.v.(type) {
+		case float32:
+			vName = fmt.Sprintf("0x%x", math.Float32bits(v))
+		case float64:
+			vName = fmt.Sprintf("0x%x", math.Float64bits(v))
+		}
 		for _, convert := range tc.convert {
 			var convertName string
 			switch convert.opt.NaNConvert {
@@ -3330,18 +3354,11 @@ func TestNaNConvert(t *testing.T) {
 			case NaNConvertQuiet:
 				convertName = "ConvertQuiet"
 			}
-			var vName string
-			switch v := tc.v.(type) {
-			case float32:
-				vName = fmt.Sprintf("0x%x", math.Float32bits(v))
-			case float64:
-				vName = fmt.Sprintf("0x%x", math.Float64bits(v))
-			}
 			name := convertName + "_" + vName
 			t.Run(name, func(t *testing.T) {
 				em, err := convert.opt.EncMode()
 				if err != nil {
-					t.Errorf("EncMode() returned an error %v", err)
+					t.Fatalf("EncMode() returned an error %v", err)
 				}
 				b, err := em.Marshal(tc.v)
 				if err != nil {
@@ -3351,6 +3368,17 @@ func TestNaNConvert(t *testing.T) {
 				}
 			})
 		}
+
+		t.Run("ConvertReject_"+vName, func(t *testing.T) {
+			em, err := EncOptions{NaNConvert: NaNConvertReject}.EncMode()
+			if err != nil {
+				t.Fatalf("EncMode() returned an error %v", err)
+			}
+			want := &UnsupportedValueError{msg: "floating-point NaN"}
+			if _, got := em.Marshal(tc.v); !reflect.DeepEqual(want, got) {
+				t.Errorf("expected Marshal(%v) to return error: %v, got: %v", tc.v, want, got)
+			}
+		})
 	}
 }