diff --git a/CHANGELOG.md b/CHANGELOG.md index 591630a..76f9546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,14 +63,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and nested arrays of tables: `[[a]]`, `b.c = 1`, `[[a]]`, `[a.b]` parses, as the TOML examples in the spec shape it. The records of the previous element falsely rejected the same paths in the next one. -- `UseLiteralMultiline` falls back to the escaped basic string when the value - cannot be carried verbatim by the literal form: a run of three single quotes, - a control character, or a lone carriage return. Such values previously - produced output that did not re-parse. - `Marshal` returns an error for a table header key or an inline-table key that is not valid UTF-8, the way scalar keys already did, instead of silently emitting corrupt TOML (a header that lost its key, an inline table with a missing key). +- `UseLiteralMultiline` falls back to the escaped basic string when the value + cannot be carried verbatim by the literal form: a run of three single quotes, + a control character, or a lone carriage return. Such values previously + produced output that did not re-parse. +- Decoding into a `uint` destination checks the type's platform width instead + of only the fixed widths, so a 32-bit `uint` no longer truncates silently; + decoding a finite float beyond the `float32` range is an overflow error + instead of a silent infinity. - Struct fields that resolve to one key at equal depth decode through the field declared later, matching the documented rule; the first one won before. - A float with an exponent marker but no digits (`1e`, `0.0E`) is rejected; diff --git a/decode.go b/decode.go index 9124a32..16341e9 100644 --- a/decode.go +++ b/decode.go @@ -5,7 +5,6 @@ package interpres import ( "fmt" - "math" "reflect" "slices" "strings" @@ -205,21 +204,21 @@ func setInt(dst reflect.Value, v int64) error { if v < 0 { return fmt.Errorf("interpres: cannot assign negative %d to %s", v, dst.Type()) } - var max uint64 - switch dst.Kind() { - case reflect.Uint8: - max = math.MaxUint8 - case reflect.Uint16: - max = math.MaxUint16 - case reflect.Uint32: - max = math.MaxUint32 - } - if max != 0 && uint64(v) > max { + // OverflowUint knows every width, uint included on platforms where it + // is narrower than uint64; SetUint would silently truncate instead. + if dst.OverflowUint(uint64(v)) { return fmt.Errorf("interpres: integer %d overflows %s", v, dst.Type()) } dst.SetUint(uint64(v)) case reflect.Float32, reflect.Float64: - dst.SetFloat(float64(v)) + // A finite value beyond the float32 range would silently become ±Inf; + // infinities and NaN themselves pass through. An int64 never + // overflows either float width. + f := float64(v) + if dst.OverflowFloat(f) { + return fmt.Errorf("interpres: integer %d overflows %s", v, dst.Type()) + } + dst.SetFloat(f) default: return fmt.Errorf("interpres: cannot assign integer to %s", dst.Type()) } @@ -229,6 +228,9 @@ func setInt(dst reflect.Value, v int64) error { func setFloat(dst reflect.Value, v float64) error { switch dst.Kind() { case reflect.Float32, reflect.Float64: + if dst.OverflowFloat(v) { + return fmt.Errorf("interpres: float %g overflows %s", v, dst.Type()) + } dst.SetFloat(v) return nil default: diff --git a/decode_test.go b/decode_test.go index b7a2b11..99cacc3 100644 --- a/decode_test.go +++ b/decode_test.go @@ -223,6 +223,33 @@ func TestUnmarshalIntToUint64FitsMaxInt64(t *testing.T) { } } +func TestUnmarshalFloat32Overflow(t *testing.T) { + // A finite float64 beyond the float32 range must not decode silently as + // an infinity. + type C struct { + X float32 `toml:"x"` + } + var c C + err := Unmarshal([]byte("x = 1e300\n"), &c) + if err == nil { + t.Fatal("expected overflow error for float32") + } + if !strings.Contains(err.Error(), "overflow") { + t.Errorf("err = %v, want substring 'overflow'", err.Error()) + } + // Infinities themselves pass through, and in-range values are untouched. + var ok C + if err := Unmarshal([]byte("x = inf\n"), &ok); err != nil { + t.Fatalf("inf should decode into float32, got %v", err) + } + if !math.IsInf(float64(ok.X), 1) { + t.Errorf("X = %v, want +Inf", ok.X) + } + if err := Unmarshal([]byte("x = 1.5\n"), &ok); err != nil || ok.X != 1.5 { + t.Fatalf("1.5 should decode into float32, got %v (X=%v)", err, ok.X) + } +} + func TestUnmarshalNegativeIntToUint(t *testing.T) { type C struct { X uint8 `toml:"x"` diff --git a/docs/API.md b/docs/API.md index e2ff056..72f65c0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -126,8 +126,8 @@ The decoder converts to the destination type with explicit overflow checks: | Destination kind | Rule | |---|---| | `int`, `int8`, `int16`, `int32`, `int64` | the `int64` value must not overflow the destination | -| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | the value must be non-negative; `uint8`, `uint16` and `uint32` enforce their own maxima; `uint64` accepts any non-negative `int64` | -| `float32`, `float64` | copied verbatim; an integer also coerces, so TOML `5` decodes into `5.0` | +| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | the value must be non-negative and must not overflow the destination's own width, `uint` on a 32-bit platform included; `uint64` accepts any non-negative `int64` | +| `float32`, `float64` | copied verbatim, except that a finite value beyond the `float32` range is an overflow error rather than a silent infinity; an integer also coerces, so TOML `5` decodes into `5.0` | | `bool`, `string` | exact kind match only, no coercion across kinds | | `time.Time` | offset date-times only; no implicit conversion to or from the local variants |