Name the setting and value when a size does not parse (closes #429)
check / check (push) Successful in 9m51s

A size setting such as chunk_size: banana failed with "invalid size
format: strconv.ParseFloat: parsing "": invalid syntax", naming neither
the setting nor the value. The size decoder now quotes the value, and
Parse checks every size setting after ${...} references are filled in,
so the error reads chunk_size: "banana" is not a size; use a number of
bytes or a unit such as 10MB. This covers chunk_size, blob_size_limit
and s3.part_size, and config set.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 04:18:36 +00:00
1 parent d8d13d1c8b
commit c33639cb69
5 files changed
+169 -17

No files matched your search

+63 -8
View File
@@ -4,13 +4,20 @@ import (
"errors"
"fmt"
"math"
"reflect"
"slices"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"gopkg.in/yaml.v3"
)
var (
errSizeType = errors.New("size must be a number or string")
errSizeTooLarge = errors.New("size exceeds maximum supported value")
errNotASize = errors.New(
"is not a size; use a number of bytes or a unit such as 10MB")
)
// Size represents a byte size that can be specified in configuration files.
@@ -40,17 +47,64 @@ func (s *Size) UnmarshalYAML(unmarshal func(any) error) error {
return errSizeType
}
// Parse the string using go-humanize
bytes, err := humanize.ParseBytes(strVal)
size, err := ParseSize(strVal)
if err != nil {
return fmt.Errorf("invalid size format: %w", err)
return err
}
if bytes > math.MaxInt64 {
return fmt.Errorf("%w: %s", errSizeTooLarge, strVal)
*s = size
return nil
}
// checkSizes returns an error naming the first size setting in node whose
// value is not a size. node is the config, or the part of it at the dotted
// keys, after smartconfig has filled in its ${...} references.
func checkSizes(node *yaml.Node, keys []string) error {
typ, err := SettingType(keys)
if err != nil {
// Unknown keys are refused before this check, so this is the env
// section, which holds no settings.
return nil //nolint:nilerr // not an error here
}
*s = Size(bytes)
if typ == reflect.TypeFor[Size]() {
var size Size
err = node.Decode(&size)
if err != nil {
return fmt.Errorf("%s: %w", strings.Join(keys, "."), err)
}
return nil
}
switch node.Kind {
case yaml.DocumentNode:
for _, child := range node.Content {
err = checkSizes(child, keys)
if err != nil {
return err
}
}
case yaml.MappingNode:
for i := 0; i+1 < len(node.Content); i += 2 {
err = checkSizes(node.Content[i+1],
slices.Concat(keys, []string{node.Content[i].Value}))
if err != nil {
return err
}
}
case yaml.SequenceNode:
for i, child := range node.Content {
err = checkSizes(child, slices.Concat(keys, []string{strconv.Itoa(i)}))
if err != nil {
return err
}
}
case yaml.ScalarNode, yaml.AliasNode:
// A scalar holds no settings, and the filled config has no aliases.
}
return nil
}
@@ -70,11 +124,12 @@ func (s Size) String() string {
return humanize.Bytes(uint64(s))
}
// ParseSize parses a size string into a Size value
// ParseSize parses a size string, such as "10MB", into a Size value. An
// error quotes s.
func ParseSize(s string) (Size, error) {
bytes, err := humanize.ParseBytes(s)
if err != nil {
return 0, fmt.Errorf("invalid size format: %w", err)
return 0, fmt.Errorf("%q %w", s, errNotASize)
}
if bytes > math.MaxInt64 {