fix(decode): overflow-check uint and float32 destinations

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-17 23:10:24 +02:00
parent 30b28fe7fc
commit c485aab227
4 changed files with 51 additions and 18 deletions
+8 -4
View File
@@ -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 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 the TOML examples in the spec shape it. The records of the previous element
falsely rejected the same paths in the next one. 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 - `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 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 emitting corrupt TOML (a header that lost its key, an inline table with a
missing key). 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 - 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. 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; - A float with an exponent marker but no digits (`1e`, `0.0E`) is rejected;
+14 -12
View File
@@ -5,7 +5,6 @@ package interpres
import ( import (
"fmt" "fmt"
"math"
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
@@ -205,21 +204,21 @@ func setInt(dst reflect.Value, v int64) error {
if v < 0 { if v < 0 {
return fmt.Errorf("interpres: cannot assign negative %d to %s", v, dst.Type()) return fmt.Errorf("interpres: cannot assign negative %d to %s", v, dst.Type())
} }
var max uint64 // OverflowUint knows every width, uint included on platforms where it
switch dst.Kind() { // is narrower than uint64; SetUint would silently truncate instead.
case reflect.Uint8: if dst.OverflowUint(uint64(v)) {
max = math.MaxUint8
case reflect.Uint16:
max = math.MaxUint16
case reflect.Uint32:
max = math.MaxUint32
}
if max != 0 && uint64(v) > max {
return fmt.Errorf("interpres: integer %d overflows %s", v, dst.Type()) return fmt.Errorf("interpres: integer %d overflows %s", v, dst.Type())
} }
dst.SetUint(uint64(v)) dst.SetUint(uint64(v))
case reflect.Float32, reflect.Float64: 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: default:
return fmt.Errorf("interpres: cannot assign integer to %s", dst.Type()) 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 { func setFloat(dst reflect.Value, v float64) error {
switch dst.Kind() { switch dst.Kind() {
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
if dst.OverflowFloat(v) {
return fmt.Errorf("interpres: float %g overflows %s", v, dst.Type())
}
dst.SetFloat(v) dst.SetFloat(v)
return nil return nil
default: default:
+27
View File
@@ -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) { func TestUnmarshalNegativeIntToUint(t *testing.T) {
type C struct { type C struct {
X uint8 `toml:"x"` X uint8 `toml:"x"`
+2 -2
View File
@@ -126,8 +126,8 @@ The decoder converts to the destination type with explicit overflow checks:
| Destination kind | Rule | | Destination kind | Rule |
|---|---| |---|---|
| `int`, `int8`, `int16`, `int32`, `int64` | the `int64` value must not overflow the destination | | `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` | | `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; an integer also coerces, so TOML `5` decodes into `5.0` | | `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 | | `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 | | `time.Time` | offset date-times only; no implicit conversion to or from the local variants |