Reject NaN or infinite floating-point values in the CBOR serializer.

This commit is contained in:
Ben Luddy 2024-05-09 15:19:51 -04:00
parent 6346b9d132
commit 2a31354e26
No known key found for this signature in database
GPG Key ID: A6551E73A5974C30
4 changed files with 75 additions and 104 deletions

View File

@ -56,8 +56,6 @@ func TestAppendixA(t *testing.T) {
const ( const (
reasonArrayFixedLength = "indefinite-length arrays are re-encoded with fixed length" reasonArrayFixedLength = "indefinite-length arrays are re-encoded with fixed length"
reasonByteString = "strings are encoded as the byte string major type" reasonByteString = "strings are encoded as the byte string major type"
reasonFloatPacked = "floats are packed into the smallest value-preserving width"
reasonNaN = "all NaN values are represented with a single encoding"
reasonMapFixedLength = "indefinite-length maps are re-encoded with fixed length" reasonMapFixedLength = "indefinite-length maps are re-encoded with fixed length"
reasonMapSorted = "map entries are sorted" reasonMapSorted = "map entries are sorted"
reasonStringFixedLength = "indefinite-length strings are re-encoded with fixed length" reasonStringFixedLength = "indefinite-length strings are re-encoded with fixed length"
@ -202,68 +200,41 @@ func TestAppendixA(t *testing.T) {
example: hex("fbc010666666666666"), example: hex("fbc010666666666666"),
decoded: -4.1, decoded: -4.1,
}, },
// TODO: Should Inf/-Inf/NaN be supported? Current Protobuf will encode this, but
// JSON will produce an error. This is less than ideal -- we can't transcode
// everything to JSON.
{ {
example: hex("f97c00"), example: hex("f97c00"),
decoded: math.Inf(1), reject: "floating-point NaN and infinities are not accepted",
}, },
{ {
example: hex("f97e00"), example: hex("f97e00"),
decoded: math.Float64frombits(0x7ff8000000000000), reject: "floating-point NaN and infinities are not accepted",
}, },
{ {
example: hex("f9fc00"), example: hex("f9fc00"),
decoded: math.Inf(-1), reject: "floating-point NaN and infinities are not accepted",
}, },
{ {
example: hex("fa7f800000"), example: hex("fa7f800000"),
decoded: math.Inf(1), reject: "floating-point NaN and infinities are not accepted",
encoded: hex("f97c00"),
reasons: []string{
reasonFloatPacked,
},
}, },
{ {
example: hex("fa7fc00000"), example: hex("fa7fc00000"),
decoded: math.NaN(), reject: "floating-point NaN and infinities are not accepted",
encoded: hex("f97e00"),
reasons: []string{
reasonNaN,
},
}, },
{ {
example: hex("faff800000"), example: hex("faff800000"),
decoded: math.Inf(-1), reject: "floating-point NaN and infinities are not accepted",
encoded: hex("f9fc00"),
reasons: []string{
reasonFloatPacked,
},
}, },
{ {
example: hex("fb7ff0000000000000"), example: hex("fb7ff0000000000000"),
decoded: math.Inf(1), reject: "floating-point NaN and infinities are not accepted",
encoded: hex("f97c00"),
reasons: []string{
reasonFloatPacked,
},
}, },
{ {
example: hex("fb7ff8000000000000"), example: hex("fb7ff8000000000000"),
decoded: math.NaN(), reject: "floating-point NaN and infinities are not accepted",
encoded: hex("f97e00"),
reasons: []string{
reasonNaN,
},
}, },
{ {
example: hex("fbfff0000000000000"), example: hex("fbfff0000000000000"),
decoded: math.Inf(-1), reject: "floating-point NaN and infinities are not accepted",
encoded: hex("f9fc00"),
reasons: []string{
reasonFloatPacked,
},
}, },
{ {
example: hex("f4"), example: hex("f4"),

View File

@ -88,6 +88,11 @@ var Decode cbor.DecMode = func() cbor.DecMode {
// For parity with JSON, strings can be decoded into time.Time if they are RFC 3339 // For parity with JSON, strings can be decoded into time.Time if they are RFC 3339
// timestamps. // timestamps.
ByteStringToTime: cbor.ByteStringToTimeAllowed, ByteStringToTime: cbor.ByteStringToTimeAllowed,
// Reject NaN and infinite floating-point values since they don't have a JSON
// representation (RFC 8259 Section 6).
NaN: cbor.NaNDecodeForbidden,
Inf: cbor.InfDecodeForbidden,
}.DecMode() }.DecMode()
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -435,92 +435,83 @@ func TestDecode(t *testing.T) {
{ {
name: "half precision infinity", name: "half precision infinity",
in: hex("f97c00"), in: hex("f97c00"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "single precision infinity", name: "single precision infinity",
in: hex("fa7f800000"), in: hex("fa7f800000"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "double precision infinity", name: "double precision infinity",
in: hex("fb7ff0000000000000"), in: hex("fb7ff0000000000000"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "half precision negative infinity", name: "half precision negative infinity",
in: hex("f9fc00"), in: hex("f9fc00"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "single precision negative infinity", name: "single precision negative infinity",
in: hex("faff800000"), in: hex("faff800000"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "double precision negative infinity", name: "double precision negative infinity",
in: hex("fbfff0000000000000"), in: hex("fbfff0000000000000"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "half precision NaN", name: "half precision NaN",
in: hex("f97e00"), in: hex("f97e00"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point NaN"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "single precision NaN", name: "single precision NaN",
in: hex("fa7fc00000"), in: hex("fa7fc00000"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point NaN"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "double precision NaN", name: "double precision NaN",
in: hex("fb7ff8000000000000"), in: hex("fb7ff8000000000000"),
assertOnError: func(t *testing.T, e error) { assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if e == nil { if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point NaN"}, e); diff != "" {
t.Fatal("expected non-nil error") t.Errorf("unexpected error diff:\n%s", diff)
} }
}, }),
fixme: "NaN and positive/negative infinities should be rejected",
}, },
{ {
name: "smallest nonzero float64", name: "smallest nonzero float64",
@ -728,25 +719,31 @@ func TestDecode(t *testing.T) {
assertOnError: assertNilError, assertOnError: assertNilError,
}, },
{ {
name: "tag 1 with a positive infinity", name: "tag 1 with a positive infinity",
in: hex("c1f97c00"), // 1(Infinity) in: hex("c1f97c00"), // 1(Infinity)
want: "0001-01-01T00:00:00Z", assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
assertOnError: assertNilError, t.Errorf("unexpected error diff:\n%s", diff)
}
}),
}, },
{ {
name: "tag 1 with a negative infinity", name: "tag 1 with a negative infinity",
in: hex("c1f9fc00"), // 1(-Infinity) in: hex("c1f9fc00"), // 1(-Infinity)
want: "0001-01-01T00:00:00Z", assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point infinity"}, e); diff != "" {
assertOnError: assertNilError, t.Errorf("unexpected error diff:\n%s", diff)
}
}),
}, },
{ {
name: "tag 1 with NaN", name: "tag 1 with NaN",
in: hex("c1f9fc00"), // 1(NaN) in: hex("c1f97e00"), // 1(NaN)
want: "0001-01-01T00:00:00Z", assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
fixme: "decoding cbor data tagged with 1 produces time.Time instead of RFC3339 timestamp string", if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "primitives", Message: "floating-point NaN"}, e); diff != "" {
assertOnError: assertNilError, t.Errorf("unexpected error diff:\n%s", diff)
}
}),
}, },
}) })

View File

@ -32,12 +32,10 @@ var Encode cbor.EncMode = func() cbor.EncMode {
// encoding. Satisfies one of the "Core Deterministic Encoding Requirements". // encoding. Satisfies one of the "Core Deterministic Encoding Requirements".
ShortestFloat: cbor.ShortestFloat16, ShortestFloat: cbor.ShortestFloat16,
// ShortestFloat doesn't apply to NaN or Inf values. Inf values are losslessly // Error on attempt to encode NaN and infinite values. This is what the JSON
// encoded to float16. RFC 8949 recommends choosing a single representation of NaN // serializer does.
// in applications that do not smuggle additional information inside NaN values, we NaNConvert: cbor.NaNConvertReject,
// use 0x7e00. InfConvert: cbor.InfConvertReject,
NaNConvert: cbor.NaNConvert7e00,
InfConvert: cbor.InfConvertFloat16,
// Prefer encoding math/big.Int to one of the 64-bit integer types if it fits. When // Prefer encoding math/big.Int to one of the 64-bit integer types if it fits. When
// later decoded into Unstructured, the set of allowable concrete numeric types is // later decoded into Unstructured, the set of allowable concrete numeric types is