encoding/json/jsontext: report errors for numeric Token accessors

A JSON number may not cleanly correspond with a Go int64, uint64, or float64.
For example:
* 1e1000 overflows any of the Go number types.
* -123 is a syntactically invalid uint64
* 123.456 or 1e13 is also syntactically invalid for a uint64 or int64

Previously, the Int, Uint, and Float accessors would report
the closest representable value and swallow any conversion errors.
This change makes it such that a sensible value is still returned,
but that an error is also report in such edge cases.

As a minor change, when a JSON number overflows a Go float,
we use +Inf or -Inf instead of +MaxFloat or -MaxFloat.

As another minor change, the v1 compatibility implementation
of Decoder.Token now reports an error when the JSON number
overflows a float64, just like how v1 behaves today.

Fixes #77666

Change-Id: Ibd22d68b865319db302dc284170fef2e4597622b
Reviewed-on: https://go-review.googlesource.com/c/go/+/772360
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Joe Tsai 2026-04-29 15:28:29 -07:00 committed by Joseph Tsai
parent f1bc06b98d
commit 464dc3f344
11 changed files with 281 additions and 224 deletions

View file

@ -604,26 +604,3 @@ func ParseUint(b []byte) (v uint64, ok bool) {
}
return v, true
}
// ParseFloat parses a floating point number according to the Go float grammar.
// Note that the JSON number grammar is a strict subset.
//
// If the number overflows the finite representation of a float,
// then we return MaxFloat since any finite value will always be infinitely
// more accurate at representing another finite value than an infinite value.
func ParseFloat(b []byte, bits int) (v float64, ok bool) {
fv, err := strconv.ParseFloat(string(b), bits)
if math.IsInf(fv, 0) {
switch {
case bits == 32 && math.IsInf(fv, +1):
fv = +math.MaxFloat32
case bits == 64 && math.IsInf(fv, +1):
fv = +math.MaxFloat64
case bits == 32 && math.IsInf(fv, -1):
fv = -math.MaxFloat32
case bits == 64 && math.IsInf(fv, -1):
fv = -math.MaxFloat64
}
}
return fv, err == nil
}

View file

@ -397,47 +397,3 @@ func TestParseUint(t *testing.T) {
})
}
}
func TestParseFloat(t *testing.T) {
tests := []struct {
in string
want32 float64
want64 float64
wantOk bool
}{
{"0", 0, 0, true},
{"-1", -1, -1, true},
{"1", 1, 1, true},
{"-16777215", -16777215, -16777215, true}, // -(1<<24 - 1)
{"16777215", 16777215, 16777215, true}, // +(1<<24 - 1)
{"-16777216", -16777216, -16777216, true}, // -(1<<24)
{"16777216", 16777216, 16777216, true}, // +(1<<24)
{"-16777217", -16777216, -16777217, true}, // -(1<<24 + 1)
{"16777217", 16777216, 16777217, true}, // +(1<<24 + 1)
{"-9007199254740991", -9007199254740992, -9007199254740991, true}, // -(1<<53 - 1)
{"9007199254740991", 9007199254740992, 9007199254740991, true}, // +(1<<53 - 1)
{"-9007199254740992", -9007199254740992, -9007199254740992, true}, // -(1<<53)
{"9007199254740992", 9007199254740992, 9007199254740992, true}, // +(1<<53)
{"-9007199254740993", -9007199254740992, -9007199254740992, true}, // -(1<<53 + 1)
{"9007199254740993", 9007199254740992, 9007199254740992, true}, // +(1<<53 + 1)
{"-1e1000", -math.MaxFloat32, -math.MaxFloat64, false},
{"1e1000", +math.MaxFloat32, +math.MaxFloat64, false},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
got32, gotOk32 := ParseFloat([]byte(tt.in), 32)
if got32 != tt.want32 || gotOk32 != tt.wantOk {
t.Errorf("ParseFloat(%q, 32) = (%v, %v), want (%v, %v)", tt.in, got32, gotOk32, tt.want32, tt.wantOk)
}
got64, gotOk64 := ParseFloat([]byte(tt.in), 64)
if got64 != tt.want64 || gotOk64 != tt.wantOk {
t.Errorf("ParseFloat(%q, 64) = (%v, %v), want (%v, %v)", tt.in, got64, gotOk64, tt.want64, tt.wantOk)
}
})
}
}

View file

@ -332,6 +332,16 @@ func (t Token) appendNumber(dst []byte, flags *jsonflags.Flags) ([]byte, error)
// Float32 returns the floating-point value for a JSON number
// parsed according to 32 bits of precision.
//
// If the JSON number is outside the representable range of a float32,
// it returns +Inf or -Inf along with an error
// that matches [strconv.ErrRange] according to [errors.Is].
//
// It returns a NaN, +Inf, or -Inf value for any JSON string
// with the values "NaN", "Infinity", or "-Infinity".
//
// It panics if the token kind is not a JSON number
// or a JSON string with the aforementioned values.
//
// Note that most JSON libraries and standards assume that JSON numbers
// are 64-bit floating-point numbers.
// This method should only be used if the caller knows
@ -339,46 +349,57 @@ func (t Token) appendNumber(dst []byte, flags *jsonflags.Flags) ([]byte, error)
// formatted only to 32 bits of precision (such as being encoded
// using the [Float32] constructor). For all other situations,
// prefer using the [Token.Float] accessor instead.
//
// It returns a NaN, +Inf, or -Inf value for any JSON string
// with the values "NaN", "Infinity", or "-Infinity".
// It panics for all other cases.
func (t Token) Float32() float32 {
return float32(t.float(32))
func (t Token) Float32() (float32, error) {
f, err := t.float(32)
return float32(f), err
}
// Float returns the floating-point value for a JSON number
// parsed according to 64 bits of precision.
//
// If the JSON number is outside the representable range of a float64,
// it returns +Inf or -Inf along with an error
// that matches [strconv.ErrRange] according to [errors.Is].
//
// It returns a NaN, +Inf, or -Inf value for any JSON string
// with the values "NaN", "Infinity", or "-Infinity".
// It panics for all other cases.
func (t Token) Float() float64 {
return float64(t.float(64))
//
// It panics if the token kind is not a JSON number
// or a JSON string with the aforementioned values.
func (t Token) Float() (float64, error) {
f, err := t.float(64)
return float64(f), err
}
func (t Token) float(bits int) float64 {
func (t Token) float(bits int) (float64, error) {
if raw := t.raw; raw != nil {
// Handle raw number value.
// Handle raw JSON number value.
if uint64(raw.previousOffsetStart()) != t.num {
panic(invalidTokenPanic)
}
buf := raw.previousBuffer()
if Kind(buf[0]).normalize() == '0' {
fv, _ := jsonwire.ParseFloat(buf, bits)
return fv
fv, err := strconv.ParseFloat(string(buf), bits)
if err != nil {
err = &SyntacticError{Err: errors.Unwrap(err)} // only ever ErrRange
}
return fv, err
}
} else if t.num != 0 {
// Handle exact number value.
// Handle typed Go number value.
switch t.str[0] {
case 'F':
return float64(math.Float32frombits(uint32(t.num)))
return float64(math.Float32frombits(uint32(t.num))), nil
case 'f':
return float64(math.Float64frombits(uint64(t.num)))
f64 := float64(math.Float64frombits(uint64(t.num)))
if bits == 32 && !math.IsInf(f64, 0) && math.IsInf(float64(float32(f64)), 0) {
return f64, &SyntacticError{Err: strconv.ErrRange}
}
return f64, nil
case 'i':
return float64(int64(t.num))
return float64(int64(t.num)), nil // NOTE: This may lead to loss of precision.
case 'u':
return float64(uint64(t.num))
return float64(uint64(t.num)), nil // NOTE: This may lead to loss of precision.
}
}
@ -386,128 +407,181 @@ func (t Token) float(bits int) float64 {
if t.Kind() == '"' {
switch t.String() {
case "NaN":
return math.NaN()
return math.NaN(), nil
case "Infinity":
return math.Inf(+1)
return math.Inf(+1), nil
case "-Infinity":
return math.Inf(-1)
return math.Inf(-1), nil
}
// TODO: Should this be a error instead of a panic?
// We can safely switch from a panic to an error in the future.
}
panic("invalid JSON token kind: " + t.Kind().String())
}
// Int returns the signed integer value for a JSON number.
//
// It reports an error that matches [strconv.ErrSyntax] according to [errors.Is]
// if the JSON number does not match the restricted grammar of just a signed integer.
// It reports an error that matches [strconv.ErrRange] according to [errors.Is]
// if the JSON number is a signed integer, but outside the range of an int64.
// Even if an error is reported, a reasonable value is still returned.
// The fractional component of any number is ignored (truncation toward zero).
// Any number beyond the representation of an int64 will be saturated
// to the closest representable value.
//
// It panics if the token kind is not a JSON number.
func (t Token) Int() int64 {
func (t Token) Int() (int64, error) {
if raw := t.raw; raw != nil {
// Handle raw integer value.
// Handle raw JSON number value.
if uint64(raw.previousOffsetStart()) != t.num {
panic(invalidTokenPanic)
}
neg := false
buf := raw.previousBuffer()
if len(buf) > 0 && buf[0] == '-' {
neg, buf = true, buf[1:]
}
if numAbs, ok := jsonwire.ParseUint(buf); ok {
if neg {
if numAbs > -minInt64 {
return minInt64
}
return -1 * int64(numAbs)
} else {
if numAbs > +maxInt64 {
return maxInt64
}
return +1 * int64(numAbs)
// Prospectively parse a negative integer.
switch abs, ok := jsonwire.ParseUint(buf[len("-"):]); {
case abs > -minInt64:
return minInt64, &SyntacticError{Err: strconv.ErrRange}
case ok:
return -1 * int64(abs), nil
}
} else {
// Prospectively parse a non-negative integer.
switch abs, ok := jsonwire.ParseUint(buf); {
case abs > +maxInt64:
return maxInt64, &SyntacticError{Err: strconv.ErrRange}
case ok:
return +1 * int64(abs), nil
}
}
// This is not a signed integer, which implies ErrSyntax.
if Kind(buf[0]).normalize() == '0' {
f64, _ := strconv.ParseFloat(string(buf), 64)
return f64toi64(f64), &SyntacticError{Err: strconv.ErrSyntax}
}
} else if t.num != 0 {
// Handle exact integer value.
// Handle typed Go number value.
switch t.str[0] {
case 'i':
return int64(t.num)
return int64(t.num), nil
case 'u':
if t.num > maxInt64 {
return maxInt64
return maxInt64, &SyntacticError{Err: strconv.ErrRange}
}
return int64(t.num), nil
case 'f', 'F':
f64 := float64(math.Float64frombits(uint64(t.num)))
if t.str[0] == 'F' {
f64 = float64(math.Float32frombits(uint32(t.num)))
}
switch i64 := f64toi64(f64); {
case math.IsNaN(f64), math.Trunc(f64) != f64:
return i64, &SyntacticError{Err: strconv.ErrSyntax}
case (i64 == minInt64 && f64 < minInt64) || (i64 == maxInt64 && f64 > maxInt64):
return i64, &SyntacticError{Err: strconv.ErrRange}
default:
return i64, nil
}
return int64(t.num)
}
}
// Handle JSON number that is a floating-point value.
if t.Kind() == '0' {
switch fv := t.Float(); {
case fv >= maxInt64:
return maxInt64
case fv <= minInt64:
return minInt64
default:
return int64(fv) // truncation toward zero
}
}
panic("invalid JSON token kind: " + t.Kind().String())
}
func f64toi64(f64 float64) int64 {
switch {
case math.IsNaN(f64):
return 0
case f64 >= maxInt64+1:
return maxInt64
case f64 < minInt64:
return minInt64
default:
return int64(f64) // NOTE: This may lead to loss of precision.
}
}
// Uint returns the unsigned integer value for a JSON number.
//
// It reports an error that matches [strconv.ErrSyntax] if the JSON number
// does not match the restricted grammar of just an unsigned integer.
// It reports an error that matches [strconv.ErrRange] if the JSON number
// is an unsigned integer, but outside the representable range of an uint64.
// Even if an error is reported, a reasonable value is still returned.
// The fractional component of any number is ignored (truncation toward zero).
// Any number beyond the representation of an uint64 will be saturated
// to the closest representable value.
//
// It panics if the token kind is not a JSON number.
func (t Token) Uint() uint64 {
func (t Token) Uint() (uint64, error) {
// NOTE: This accessor returns 0 for any negative JSON number,
// which might be surprising, but is at least consistent with the behavior
// of saturating out-of-bounds numbers to the closest representable number.
// We report ErrSyntax instead of ErrRange since the grammar for
// an unsigned integer does not permit a negative sign.
if raw := t.raw; raw != nil {
// Handle raw integer value.
// Handle raw JSON number value.
if uint64(raw.previousOffsetStart()) != t.num {
panic(invalidTokenPanic)
}
neg := false
buf := raw.previousBuffer()
if len(buf) > 0 && buf[0] == '-' {
neg, buf = true, buf[1:]
// Prospectively parse an unsigned integer.
switch abs, ok := jsonwire.ParseUint(buf); {
case ok:
return abs, nil
case abs == maxUint64: // implies overflows
return maxUint64, &SyntacticError{Err: strconv.ErrRange}
}
if num, ok := jsonwire.ParseUint(buf); ok {
if neg {
return minUint64
}
return num
// This is not an unsigned integer, which implies ErrSyntax.
if Kind(buf[0]).normalize() == '0' {
f64, _ := strconv.ParseFloat(string(buf), 64)
return f64tou64(f64), &SyntacticError{Err: strconv.ErrSyntax}
}
} else if t.num != 0 {
// Handle exact integer value.
// Handle typed Go number value.
switch t.str[0] {
case 'u':
return t.num
return t.num, nil
case 'i':
if int64(t.num) < minUint64 {
return minUint64
return minUint64, &SyntacticError{Err: strconv.ErrSyntax}
}
return uint64(int64(t.num)), nil
case 'f', 'F':
f64 := float64(math.Float64frombits(uint64(t.num)))
if t.str[0] == 'F' {
f64 = float64(math.Float32frombits(uint32(t.num)))
}
switch u64 := f64tou64(f64); {
case math.IsNaN(f64), math.Trunc(f64) != f64, math.Signbit(f64):
return u64, &SyntacticError{Err: strconv.ErrSyntax}
case (u64 == minUint64 && f64 < minUint64) || (u64 == maxUint64 && f64 > maxUint64):
return u64, &SyntacticError{Err: strconv.ErrRange}
default:
return u64, nil
}
return uint64(int64(t.num))
}
}
// Handle JSON number that is a floating-point value.
if t.Kind() == '0' {
switch fv := t.Float(); {
case fv >= maxUint64:
return maxUint64
case fv <= minUint64:
return minUint64
default:
return uint64(fv) // truncation toward zero
}
}
panic("invalid JSON token kind: " + t.Kind().String())
}
func f64tou64(f64 float64) uint64 {
switch {
case math.IsNaN(f64):
return 0
case f64 >= maxUint64+1:
return maxUint64
case f64 < minUint64:
return minUint64
default:
return uint64(f64) // NOTE: This may lead to loss of precision.
}
}
// Kind returns the token kind.
func (t Token) Kind() Kind {
switch {

View file

@ -9,6 +9,7 @@ package jsontext
import (
"math"
"reflect"
"strconv"
"testing"
)
@ -32,15 +33,32 @@ func TestTokenStringAllocations(t *testing.T) {
}
func TestTokenAccessors(t *testing.T) {
type valueError[T any] struct {
Value T
Error error
}
type token struct {
Bool bool
String string
Float32 float32
Float float64
Int int64
Uint uint64
Float32 valueError[float32]
Float valueError[float64]
Int valueError[int64]
Uint valueError[uint64]
Kind Kind
}
negZero := math.Copysign(0, -1)
errRange := &SyntacticError{Err: strconv.ErrRange}
errSyntax := &SyntacticError{Err: strconv.ErrSyntax}
f32 := func(f32 float32) valueError[float32] { return valueError[float32]{Value: f32} }
f32er := func(f32 float32) valueError[float32] { return valueError[float32]{Value: f32, Error: errRange} }
f64 := func(f64 float64) valueError[float64] { return valueError[float64]{Value: f64} }
f64er := func(f64 float64) valueError[float64] { return valueError[float64]{Value: f64, Error: errRange} }
i64 := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64} }
i64er := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64, Error: errRange} }
i64es := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64, Error: errSyntax} }
u64 := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64} }
u64er := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64, Error: errRange} }
u64es := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64, Error: errSyntax} }
tests := []struct {
in Token
@ -59,81 +77,104 @@ func TestTokenAccessors(t *testing.T) {
{String(""), token{String: "", Kind: '"'}},
{String("hello, world!"), token{String: "hello, world!", Kind: '"'}},
{rawToken(`"hello, world!"`), token{String: "hello, world!", Kind: '"'}},
{Float32(float32(0)), token{String: "0", Float32: 0, Float: 0, Int: 0, Uint: 0, Kind: '0'}},
{Float32(float32(math.Copysign(0, -1))), token{String: "-0", Float32: float32(math.Copysign(0, -1)), Float: math.Copysign(0, -1), Int: 0, Uint: 0, Kind: '0'}},
{Float32(float32(math.NaN())), token{String: "NaN", Float32: float32(math.NaN()), Float: math.NaN(), Int: 0, Uint: 0, Kind: '"'}},
{Float32(float32(math.Inf(+1))), token{String: "Infinity", Float32: float32(math.Inf(+1)), Float: math.Inf(+1), Kind: '"'}},
{Float32(float32(math.Inf(-1))), token{String: "-Infinity", Float32: float32(math.Inf(-1)), Float: math.Inf(-1), Kind: '"'}},
{Float32(float32(math.Pi)), token{String: "3.1415927", Float32: math.Pi, Float: float64(float32(math.Pi)), Int: 3, Uint: 3, Kind: '0'}},
{Float(0), token{String: "0", Float32: 0, Float: 0, Int: 0, Uint: 0, Kind: '0'}},
{Float(math.Copysign(0, -1)), token{String: "-0", Float32: float32(math.Copysign(0, -1)), Float: math.Copysign(0, -1), Int: 0, Uint: 0, Kind: '0'}},
{Float(math.NaN()), token{String: "NaN", Float32: float32(math.NaN()), Float: math.NaN(), Int: 0, Uint: 0, Kind: '"'}},
{Float(math.Inf(+1)), token{String: "Infinity", Float32: float32(math.Inf(+1)), Float: math.Inf(+1), Kind: '"'}},
{Float(math.Inf(-1)), token{String: "-Infinity", Float32: float32(math.Inf(-1)), Float: math.Inf(-1), Kind: '"'}},
{Float(math.Pi), token{String: "3.141592653589793", Float32: math.Pi, Float: math.Pi, Int: 3, Uint: 3, Kind: '0'}},
{Int(minInt64), token{String: "-9223372036854775808", Float32: minInt64, Float: minInt64, Int: minInt64, Uint: minUint64, Kind: '0'}},
{Int(minInt64 + 1), token{String: "-9223372036854775807", Float32: minInt64 + 1, Float: minInt64 + 1, Int: minInt64 + 1, Uint: minUint64, Kind: '0'}},
{Int(-1), token{String: "-1", Float32: -1, Float: -1, Int: -1, Uint: minUint64, Kind: '0'}},
{Int(0), token{String: "0", Float32: 0, Float: 0, Int: 0, Uint: 0, Kind: '0'}},
{Int(+1), token{String: "1", Float32: +1, Float: +1, Int: +1, Uint: +1, Kind: '0'}},
{Int(maxInt64 - 1), token{String: "9223372036854775806", Float32: maxInt64 - 1, Float: maxInt64 - 1, Int: maxInt64 - 1, Uint: maxInt64 - 1, Kind: '0'}},
{Int(maxInt64), token{String: "9223372036854775807", Float32: maxInt64, Float: maxInt64, Int: maxInt64, Uint: maxInt64, Kind: '0'}},
{Float32(float32(0)), token{String: "0", Float32: f32(0), Float: f64(0), Int: i64(0), Uint: u64(0), Kind: '0'}},
{Float32(float32(math.Copysign(0, -1))), token{String: "-0", Float32: f32(float32(negZero)), Float: f64(negZero), Int: i64(0), Uint: u64es(0), Kind: '0'}},
{Float32(float32(math.NaN())), token{String: "NaN", Float32: f32(float32(math.NaN())), Float: f64(math.NaN()), Kind: '"'}},
{Float32(float32(math.Inf(+1))), token{String: "Infinity", Float32: f32(float32(math.Inf(+1))), Float: f64(math.Inf(+1)), Kind: '"'}},
{Float32(float32(math.Inf(-1))), token{String: "-Infinity", Float32: f32(float32(math.Inf(-1))), Float: f64(math.Inf(-1)), Kind: '"'}},
{Float32(float32(math.Pi)), token{String: "3.1415927", Float32: f32(math.Pi), Float: f64(float64(float32(math.Pi))), Int: i64es(3), Uint: u64es(3), Kind: '0'}},
{Float32(float32(-1 * math.MaxFloat32)), token{String: "-3.4028235e+38", Float32: f32(float32(-1 * math.MaxFloat32)), Float: f64(-1 * math.MaxFloat32), Int: i64er(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{Float32(float32(+1 * math.MaxFloat32)), token{String: "3.4028235e+38", Float32: f32(float32(+1 * math.MaxFloat32)), Float: f64(+1 * math.MaxFloat32), Int: i64er(maxInt64), Uint: u64er(maxUint64), Kind: '0'}},
{Float32(float32(123)), token{String: "123", Float32: f32(123), Float: f64(123), Int: i64(123), Uint: u64(123), Kind: '0'}},
{Float(0), token{String: "0", Float32: f32(0), Float: f64(0), Int: i64(0), Uint: u64(0), Kind: '0'}},
{Float(negZero), token{String: "-0", Float32: f32(float32(negZero)), Float: f64(negZero), Int: i64(0), Uint: u64es(0), Kind: '0'}},
{Float(math.NaN()), token{String: "NaN", Float32: f32(float32(math.NaN())), Float: f64(math.NaN()), Int: i64(0), Uint: u64(0), Kind: '"'}},
{Float(math.Inf(+1)), token{String: "Infinity", Float32: f32(float32(math.Inf(+1))), Float: f64(math.Inf(+1)), Kind: '"'}},
{Float(math.Inf(-1)), token{String: "-Infinity", Float32: f32(float32(math.Inf(-1))), Float: f64(math.Inf(-1)), Kind: '"'}},
{Float(math.Pi), token{String: "3.141592653589793", Float32: f32(math.Pi), Float: f64(math.Pi), Int: i64es(3), Uint: u64es(3), Kind: '0'}},
{Float(-1 * math.MaxFloat64), token{String: "-1.7976931348623157e+308", Float32: f32er(float32(math.Inf(-1))), Float: f64(-1 * math.MaxFloat64), Int: i64er(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{Float(+1 * math.MaxFloat64), token{String: "1.7976931348623157e+308", Float32: f32er(float32(math.Inf(+1))), Float: f64(+1 * math.MaxFloat64), Int: i64er(maxInt64), Uint: u64er(maxUint64), Kind: '0'}},
{Float(123), token{String: "123", Float32: f32(123), Float: f64(123), Int: i64(123), Uint: u64(123), Kind: '0'}},
{Int(minInt64), token{String: "-9223372036854775808", Float32: f32(minInt64), Float: f64(minInt64), Int: i64(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{Int(minInt64 + 1), token{String: "-9223372036854775807", Float32: f32(minInt64 + 1), Float: f64(minInt64 + 1), Int: i64(minInt64 + 1), Uint: u64es(minUint64), Kind: '0'}},
{Int(-1), token{String: "-1", Float32: f32(-1), Float: f64(-1), Int: i64(-1), Uint: u64es(minUint64), Kind: '0'}},
{Int(0), token{String: "0", Float32: f32(0), Float: f64(0), Int: i64(0), Uint: u64(0), Kind: '0'}},
{Int(+1), token{String: "1", Float32: f32(+1), Float: f64(+1), Int: i64(+1), Uint: u64(+1), Kind: '0'}},
{Int(maxInt64 - 1), token{String: "9223372036854775806", Float32: f32(maxInt64 - 1), Float: f64(maxInt64 - 1), Int: i64(maxInt64 - 1), Uint: u64(maxInt64 - 1), Kind: '0'}},
{Int(maxInt64), token{String: "9223372036854775807", Float32: f32(maxInt64), Float: f64(maxInt64), Int: i64(maxInt64), Uint: u64(maxInt64), Kind: '0'}},
{Uint(minUint64), token{String: "0", Kind: '0'}},
{Uint(minUint64 + 1), token{String: "1", Float32: minUint64 + 1, Float: minUint64 + 1, Int: minUint64 + 1, Uint: minUint64 + 1, Kind: '0'}},
{Uint(maxUint64 - 1), token{String: "18446744073709551614", Float32: maxUint64 - 1, Float: maxUint64 - 1, Int: maxInt64, Uint: maxUint64 - 1, Kind: '0'}},
{Uint(maxUint64), token{String: "18446744073709551615", Float32: maxUint64 - 1, Float: maxUint64 - 1, Int: maxInt64, Uint: maxUint64, Kind: '0'}},
{rawToken(`-0`), token{String: "-0", Float32: float32(math.Copysign(0, -1)), Float: math.Copysign(0, -1), Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`1e1000`), token{String: "1e1000", Float32: math.MaxFloat32, Float: math.MaxFloat64, Int: maxInt64, Uint: maxUint64, Kind: '0'}},
{rawToken(`-1e1000`), token{String: "-1e1000", Float32: -math.MaxFloat32, Float: -math.MaxFloat64, Int: minInt64, Uint: minUint64, Kind: '0'}},
{rawToken(`0.1`), token{String: "0.1", Float32: 0.1, Float: 0.1, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`0.5`), token{String: "0.5", Float32: 0.5, Float: 0.5, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`0.9`), token{String: "0.9", Float32: 0.9, Float: 0.9, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`1.1`), token{String: "1.1", Float32: 1.1, Float: 1.1, Int: 1, Uint: 1, Kind: '0'}},
{rawToken(`-0.1`), token{String: "-0.1", Float32: -0.1, Float: -0.1, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`-0.5`), token{String: "-0.5", Float32: -0.5, Float: -0.5, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`-0.9`), token{String: "-0.9", Float32: -0.9, Float: -0.9, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`-1.1`), token{String: "-1.1", Float32: -1.1, Float: -1.1, Int: -1, Uint: 0, Kind: '0'}},
{rawToken(`99999999999999999999`), token{String: "99999999999999999999", Float32: 1e20 - 1, Float: 1e20 - 1, Int: maxInt64, Uint: maxUint64, Kind: '0'}},
{rawToken(`-99999999999999999999`), token{String: "-99999999999999999999", Float32: -1e20 - 1, Float: -1e20 - 1, Int: minInt64, Uint: minUint64, Kind: '0'}},
{rawToken(`3.1415927`), token{String: "3.1415927", Float32: math.Pi, Float: 3.1415927, Int: 3, Uint: 3, Kind: '0'}},
{rawToken(`3.141592653589793`), token{String: "3.141592653589793", Float32: math.Pi, Float: math.Pi, Int: 3, Uint: 3, Kind: '0'}},
{Uint(minUint64 + 1), token{String: "1", Float32: f32(minUint64 + 1), Float: f64(minUint64 + 1), Int: i64(minUint64 + 1), Uint: u64(minUint64 + 1), Kind: '0'}},
{Uint(maxUint64 - 1), token{String: "18446744073709551614", Float32: f32(maxUint64 - 1), Float: f64(maxUint64 - 1), Int: i64er(maxInt64), Uint: u64(maxUint64 - 1), Kind: '0'}},
{Uint(maxUint64), token{String: "18446744073709551615", Float32: f32(maxUint64 - 1), Float: f64(maxUint64 - 1), Int: i64er(maxInt64), Uint: u64(maxUint64), Kind: '0'}},
{rawToken(`-0`), token{String: "-0", Float32: f32(float32(negZero)), Float: f64(negZero), Int: i64(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`1e1000`), token{String: "1e1000", Float32: f32er(float32(math.Inf(+1))), Float: f64er(float64(math.Inf(+1))), Int: i64es(maxInt64), Uint: u64es(maxUint64), Kind: '0'}},
{rawToken(`-1e1000`), token{String: "-1e1000", Float32: f32er(float32(math.Inf(-1))), Float: f64er(float64(math.Inf(-1))), Int: i64es(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{rawToken(`0.1`), token{String: "0.1", Float32: f32(0.1), Float: f64(0.1), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`0.5`), token{String: "0.5", Float32: f32(0.5), Float: f64(0.5), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`0.9`), token{String: "0.9", Float32: f32(0.9), Float: f64(0.9), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`1.0`), token{String: "1.0", Float32: f32(1.0), Float: f64(1.0), Int: i64es(1), Uint: u64es(1), Kind: '0'}},
{rawToken(`1.1`), token{String: "1.1", Float32: f32(1.1), Float: f64(1.1), Int: i64es(1), Uint: u64es(1), Kind: '0'}},
{rawToken(`123`), token{String: "123", Float32: f32(123), Float: f64(123), Int: i64(123), Uint: u64(123), Kind: '0'}},
{rawToken(`-0.1`), token{String: "-0.1", Float32: f32(-0.1), Float: f64(-0.1), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`-0.5`), token{String: "-0.5", Float32: f32(-0.5), Float: f64(-0.5), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`-0.9`), token{String: "-0.9", Float32: f32(-0.9), Float: f64(-0.9), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{rawToken(`-1.0`), token{String: "-1.0", Float32: f32(-1.0), Float: f64(-1.0), Int: i64es(-1), Uint: u64es(0), Kind: '0'}},
{rawToken(`-1.1`), token{String: "-1.1", Float32: f32(-1.1), Float: f64(-1.1), Int: i64es(-1), Uint: u64es(0), Kind: '0'}},
{rawToken(`-123`), token{String: "-123", Float32: f32(-123), Float: f64(-123), Int: i64(-123), Uint: u64es(0), Kind: '0'}},
{rawToken(`99999999999999999999`), token{String: "99999999999999999999", Float32: f32(1e20 - 1), Float: f64(1e20 - 1), Int: i64er(maxInt64), Uint: u64er(maxUint64), Kind: '0'}},
{rawToken(`-99999999999999999999`), token{String: "-99999999999999999999", Float32: f32(-1e20 - 1), Float: f64(-1e20 - 1), Int: i64er(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{rawToken(`3.1415927`), token{String: "3.1415927", Float32: f32(math.Pi), Float: f64(3.1415927), Int: i64es(3), Uint: u64es(3), Kind: '0'}},
{rawToken(`3.141592653589793`), token{String: "3.141592653589793", Float32: f32(math.Pi), Float: f64(math.Pi), Int: i64es(3), Uint: u64es(3), Kind: '0'}},
{rawToken(`-9223372036854775807`), token{String: "-9223372036854775807", Float32: f32(-1 << 63), Float: f64(-1 << 63), Int: i64(minInt64 + 1), Uint: u64es(minUint64), Kind: '0'}},
{rawToken(`-9223372036854775808`), token{String: "-9223372036854775808", Float32: f32(-1 << 63), Float: f64(-1 << 63), Int: i64(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{rawToken(`-9223372036854775809`), token{String: "-9223372036854775809", Float32: f32(-1 << 63), Float: f64(-1 << 63), Int: i64er(minInt64), Uint: u64es(minUint64), Kind: '0'}},
{rawToken(`9223372036854775806`), token{String: "9223372036854775806", Float32: f32(1 << 63), Float: f64(1 << 63), Int: i64(maxInt64 - 1), Uint: u64(maxInt64 - 1), Kind: '0'}},
{rawToken(`9223372036854775807`), token{String: "9223372036854775807", Float32: f32(1 << 63), Float: f64(1 << 63), Int: i64(maxInt64), Uint: u64(maxInt64), Kind: '0'}},
{rawToken(`9223372036854775808`), token{String: "9223372036854775808", Float32: f32(1 << 63), Float: f64(1 << 63), Int: i64er(maxInt64), Uint: u64(maxInt64 + 1), Kind: '0'}},
{rawToken(`18446744073709551614`), token{String: "18446744073709551614", Float32: f32(1 << 64), Float: f64(1 << 64), Int: i64er(maxInt64), Uint: u64(maxUint64 - 1), Kind: '0'}},
{rawToken(`18446744073709551615`), token{String: "18446744073709551615", Float32: f32(1 << 64), Float: f64(1 << 64), Int: i64er(maxInt64), Uint: u64(maxUint64), Kind: '0'}},
{rawToken(`18446744073709551616`), token{String: "18446744073709551616", Float32: f32(1 << 64), Float: f64(1 << 64), Int: i64er(maxInt64), Uint: u64er(maxUint64), Kind: '0'}},
// NOTE: There exist many raw JSON numbers where:
// float32(ParseFloat(s, 32)) != float32(ParseFloat(s, 64))
// due to issues with double rounding in opposite directions.
// This suggests the need for a Token.Float32 accessor.
{rawToken(`9000000000.0000001`), token{String: "9000000000.0000001", Float32: 9000000000.0000001, Float: 9000000000.0000001, Int: 9e9, Uint: 9e9, Kind: '0'}},
{rawToken(`9000000000.0000001`), token{String: "9000000000.0000001", Float32: f32(9000000000.0000001), Float: f64(9000000000.0000001), Int: i64es(9e9), Uint: u64es(9e9), Kind: '0'}},
// NOTE: ±7.038531e-26 is the only 32-bit precision float where:
// f != float32(ParseFloat(FormatFloat(f, 32), 64))
// assuming FormatFloat uses ECMA-262, 10th edition, section 7.1.12.1.
{rawToken(`7.038531e-26`), token{String: "7.038531e-26", Float32: 7.038531e-26, Float: 7.038531e-26, Int: 0, Uint: 0, Kind: '0'}},
{Float32(7.038531e-26), token{String: "7.038531e-26", Float32: 7.038531e-26, Float: 7.038530691851209e-26, Int: 0, Uint: 0, Kind: '0'}},
{Float(7.038531e-26), token{String: "7.038531e-26", Float32: 7.0385313e-26, Float: 7.038531e-26, Int: 0, Uint: 0, Kind: '0'}},
{rawToken(`7.038531e-26`), token{String: "7.038531e-26", Float32: f32(7.038531e-26), Float: f64(7.038531e-26), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{Float32(7.038531e-26), token{String: "7.038531e-26", Float32: f32(7.038531e-26), Float: f64(7.038530691851209e-26), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
{Float(7.038531e-26), token{String: "7.038531e-26", Float32: f32(7.0385313e-26), Float: f64(7.038531e-26), Int: i64es(0), Uint: u64es(0), Kind: '0'}},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
t.Run(tt.in.String(), func(t *testing.T) {
got := token{
Bool: func() bool {
defer func() { recover() }()
return tt.in.Bool()
}(),
String: tt.in.String(),
Float32: func() float32 {
Float32: func() valueError[float32] {
defer func() { recover() }()
return tt.in.Float32()
f32, err := tt.in.Float32()
return valueError[float32]{f32, err}
}(),
Float: func() float64 {
Float: func() valueError[float64] {
defer func() { recover() }()
return tt.in.Float()
f64, err := tt.in.Float()
return valueError[float64]{f64, err}
}(),
Int: func() int64 {
Int: func() valueError[int64] {
defer func() { recover() }()
return tt.in.Int()
i64, err := tt.in.Int()
return valueError[int64]{i64, err}
}(),
Uint: func() uint64 {
Uint: func() valueError[uint64] {
defer func() { recover() }()
return tt.in.Uint()
u64, err := tt.in.Uint()
return valueError[uint64]{u64, err}
}(),
Kind: tt.in.Kind(),
}
@ -144,17 +185,17 @@ func TestTokenAccessors(t *testing.T) {
if got.String != tt.want.String {
t.Errorf("Token(%s).String() = %v, want %v", tt.in, got.String, tt.want.String)
}
if math.Float32bits(got.Float32) != math.Float32bits(tt.want.Float32) {
t.Errorf("Token(%s).Float32() = %v, want %v", tt.in, got.Float32, tt.want.Float32)
if math.Float32bits(got.Float32.Value) != math.Float32bits(tt.want.Float32.Value) || !reflect.DeepEqual(got.Float32.Error, tt.want.Float32.Error) {
t.Errorf("Token(%s).Float32() = (%v, %v), want (%v, %v)", tt.in, got.Float32.Value, got.Float32.Error, tt.want.Float32.Value, tt.want.Float32.Error)
}
if math.Float64bits(got.Float) != math.Float64bits(tt.want.Float) {
t.Errorf("Token(%s).Float() = %v, want %v", tt.in, got.Float, tt.want.Float)
if math.Float64bits(got.Float.Value) != math.Float64bits(tt.want.Float.Value) || !reflect.DeepEqual(got.Float.Error, tt.want.Float.Error) {
t.Errorf("Token(%s).Float() = (%v, %v), want (%v, %v)", tt.in, got.Float.Value, got.Float.Error, tt.want.Float.Value, tt.want.Float.Error)
}
if got.Int != tt.want.Int {
t.Errorf("Token(%s).Int() = %v, want %v", tt.in, got.Int, tt.want.Int)
if got.Int.Value != tt.want.Int.Value || !reflect.DeepEqual(got.Int.Error, tt.want.Int.Error) {
t.Errorf("Token(%s).Int() = (%v, %v), want (%v, %v)", tt.in, got.Int.Value, got.Int.Error, tt.want.Int.Value, tt.want.Int.Error)
}
if got.Uint != tt.want.Uint {
t.Errorf("Token(%s).Uint() = %v, want %v", tt.in, got.Uint, tt.want.Uint)
if got.Uint.Value != tt.want.Uint.Value || !reflect.DeepEqual(got.Uint.Error, tt.want.Uint.Error) {
t.Errorf("Token(%s).Uint() = (%v, %v), want (%v, %v)", tt.in, got.Uint.Value, got.Uint.Error, tt.want.Uint.Value, tt.want.Uint.Error)
}
if got.Kind != tt.want.Kind {
t.Errorf("Token(%s).Kind() = %v, want %v", tt.in, got.Kind, tt.want.Kind)
@ -183,7 +224,7 @@ func TestTokenClone(t *testing.T) {
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
t.Run(tt.in.String(), func(t *testing.T) {
got := tt.in.Clone()
if !reflect.DeepEqual(got, tt.in) {
t.Errorf("Token(%s) == Token(%s).Clone() = false, want true", tt.in, tt.in)

View file

@ -535,7 +535,7 @@ func TestHTTPDecoding(t *testing.T) {
}
}
func TestTokenTruncation(t *testing.T) {
func TestTokenError(t *testing.T) {
tests := []struct {
in string
err error
@ -556,13 +556,14 @@ func TestTokenTruncation(t *testing.T) {
{in: `nul`, err: io.ErrUnexpectedEOF},
{in: `fal `, err: &SyntaxError{"invalid character ' ' in literal false (expecting 's')", int64(len(`fal `))}},
{in: `false`, err: io.EOF},
{in: ` 1e1000`, err: &UnmarshalTypeError{Value: "number 1e1000", Type: reflect.TypeFor[float64](), Offset: int64(len(` 1e100`))}},
}
for _, tt := range tests {
d := NewDecoder(strings.NewReader(tt.in))
for i := 0; true; i++ {
if _, err := d.Token(); err != nil {
if !reflect.DeepEqual(err, tt.err) {
t.Errorf("`%s`: %d.Token error = %#v, want %v", tt.in, i, err, tt.err)
t.Errorf("`%s`: %d.Token error = %#v, want %#v", tt.in, i, err, tt.err)
}
break
}

View file

@ -8,6 +8,7 @@ package json
import (
"cmp"
"errors"
"math"
"reflect"
"slices"
@ -90,9 +91,9 @@ func unmarshalValueAny(dec *jsontext.Decoder, uo *jsonopts.Struct) (any, error)
if uo.Flags.Get(jsonflags.UnmarshalAnyWithRawNumber) {
return internal.RawNumberOf(val), nil
}
fv, ok := jsonwire.ParseFloat(val, 64)
if !ok {
return fv, newUnmarshalErrorAfterWithValue(dec, float64Type, strconv.ErrRange)
fv, err := strconv.ParseFloat(string(val), 64)
if err != nil {
return fv, newUnmarshalErrorAfterWithValue(dec, float64Type, errors.Unwrap(err))
}
return fv, nil
default:

View file

@ -718,10 +718,10 @@ func makeFloatArshaler(t reflect.Type) *arshaler {
if stringify && k == '0' {
break
}
fv, ok := jsonwire.ParseFloat(val, bits)
fv, err := strconv.ParseFloat(string(val), bits)
va.SetFloat(fv)
if !ok {
return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrRange)
if err != nil {
return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err))
}
return nil
}

View file

@ -5428,7 +5428,7 @@ func TestUnmarshal(t *testing.T) {
name: jsontest.Name("Floats/Float32/Overflow"),
inBuf: `-1e1000`,
inVal: addr(float32(32.32)),
want: addr(float32(-math.MaxFloat32)),
want: addr(float32(math.Inf(-1))),
wantErr: EU(strconv.ErrRange).withVal(`-1e1000`).withType('0', T[float32]()),
}, {
name: jsontest.Name("Floats/Float64/Pi"),
@ -5444,13 +5444,13 @@ func TestUnmarshal(t *testing.T) {
name: jsontest.Name("Floats/Float64/Overflow"),
inBuf: `-1e1000`,
inVal: addr(float64(64.64)),
want: addr(float64(-math.MaxFloat64)),
want: addr(float64(math.Inf(-1))),
wantErr: EU(strconv.ErrRange).withVal(`-1e1000`).withType('0', T[float64]()),
}, {
name: jsontest.Name("Floats/Any/Overflow"),
inBuf: `1e1000`,
inVal: new(any),
want: addr(any(float64(math.MaxFloat64))),
want: addr(any(float64(math.Inf(+1)))),
wantErr: EU(strconv.ErrRange).withVal(`1e1000`).withType('0', T[float64]()),
}, {
name: jsontest.Name("Floats/Named"),

View file

@ -471,7 +471,8 @@ func mustDecodeTokens(t testing.TB, data []byte) []jsontext.Token {
case '"':
tokens = append(tokens, jsontext.String(tok.String()))
case '0':
tokens = append(tokens, jsontext.Float(tok.Float()))
f, _ := tok.Float()
tokens = append(tokens, jsontext.Float(f))
default:
tokens = append(tokens, tok.Clone())
}

View file

@ -10,6 +10,7 @@ import (
"bytes"
"errors"
"io"
"reflect"
"encoding/json/jsontext"
jsonv2 "encoding/json/v2"
@ -231,7 +232,11 @@ func (dec *Decoder) Token() (Token, error) {
if useNumber, _ := jsonv2.GetOption(dec.opts, unmarshalAnyWithRawNumber); useNumber {
return Number(tok.String()), nil
}
return tok.Float(), nil
v, err := tok.Float()
if err != nil {
return nil, &UnmarshalTypeError{Value: "number " + tok.String(), Type: reflect.TypeFor[float64](), Offset: dec.InputOffset() - int64(len(tok.String()))}
}
return v, nil
case '{', '}', '[', ']':
return Delim(k), nil
default:

View file

@ -516,7 +516,7 @@ func TestHTTPDecoding(t *testing.T) {
}
}
func TestTokenTruncation(t *testing.T) {
func TestTokenError(t *testing.T) {
tests := []struct {
in string
err error
@ -537,13 +537,14 @@ func TestTokenTruncation(t *testing.T) {
{in: `nul`, err: io.ErrUnexpectedEOF},
{in: `fal `, err: &SyntaxError{"invalid character ' ' in literal false (expecting 's')", int64(len(`fal`))}},
{in: `false`, err: io.EOF},
{in: ` 1e1000`, err: &UnmarshalTypeError{Value: "number 1e1000", Type: reflect.TypeFor[float64](), Offset: int64(len(` `))}},
}
for _, tt := range tests {
d := NewDecoder(strings.NewReader(tt.in))
for i := 0; true; i++ {
if _, err := d.Token(); err != nil {
if !reflect.DeepEqual(err, tt.err) {
t.Errorf("`%s`: %d.Token error = %#v, want %v", tt.in, i, err, tt.err)
t.Errorf("`%s`: %d.Token error = %#v, want %#v", tt.in, i, err, tt.err)
}
break
}