diff --git a/internal/compose/resolution/config_lines.go b/internal/compose/resolution/config_lines.go index 456f6e8d..b6dc64e7 100644 --- a/internal/compose/resolution/config_lines.go +++ b/internal/compose/resolution/config_lines.go @@ -3,6 +3,7 @@ package resolution import ( "encoding/json" "fmt" + "math" "regexp" "slices" "strconv" @@ -45,7 +46,7 @@ func configLinesInto(content string, layers []mcat.Layer, module string) (string return token } if mib { - n, err := strconv.ParseUint(text, 10, 64) + n, err := mibWhole(text) if err != nil || n > 1<<44 { refused = fmt.Errorf("%s: the config value %q holds %q, and mib-bytes writes a whole number of MiB "+ "in bytes (design 42 §3)", module, key, text) @@ -92,6 +93,19 @@ func configJSONInto(content string, layers []mcat.Layer, declared []string) (str return configJSONRef.ReplaceAllLiteralString(content, object), nil } +// mibWhole is a size in MiB as a whole number: digits, or a whole number a program wrote in exponent form (`1e+06`), +// which is how a float of a million or more prints (design 42 §3). +func mibWhole(text string) (uint64, error) { + if n, err := strconv.ParseUint(text, 10, 64); err == nil { + return n, nil + } + f, err := strconv.ParseFloat(text, 64) + if err != nil || f < 0 || f != math.Trunc(f) || f > 1<<44 { + return 0, fmt.Errorf("not a whole number of MiB") + } + return uint64(f), nil +} + // declaredConfig is the keys a module's manifest declares under `config`. func declaredConfig(m mcat.Manifest) []string { keys := make([]string, 0, len(m.Settings)) diff --git a/internal/compose/resolution/setting_into_test.go b/internal/compose/resolution/setting_into_test.go index f497f411..9b7dd551 100644 --- a/internal/compose/resolution/setting_into_test.go +++ b/internal/compose/resolution/setting_into_test.go @@ -54,6 +54,14 @@ func TestAConfigValueReachesAResourceAndDecidesWhetherItIsDeclared(t *testing.T) []string{"${setting:domain}", "settings set mail", "set today: other"}}, {"config-lines: a list of two writes two lines in order", values(map[string]any{"k": []any{"a", "b"}}), filled("h\n${config-lines:k:options zfs x=}\nt\n"), "h\noptions zfs x=a\noptions zfs x=b\nt\n", nil}, + {"config-lines: mib-bytes writes a million MiB as a whole number", values(map[string]any{"k": float64(1000000)}), + filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=1048576000000\n", nil}, + {"config-lines: mib-bytes writes a million MiB given as text as a whole number", values(map[string]any{"k": "1000000"}), + filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=1048576000000\n", nil}, + {"config-lines: mib-bytes writes a size printed in exponent form as a whole number", values(map[string]any{"k": "1e+06"}), + filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "zfs_arc_max=1048576000000\n", nil}, + {"config-lines: mib-bytes refuses a fraction", values(map[string]any{"k": "1.5"}), + filled("${config-lines:k:zfs_arc_max=:mib-bytes}\n"), "${config-lines:k:zfs_arc_max=:mib-bytes}\n", []string{"mib-bytes"}}, {"config-lines: an empty list writes nothing", values(map[string]any{"k": []any{}}), filled("h\n${config-lines:k:p=}\nt\n"), "h\nt\n", nil}, {"config-lines: an unset value writes nothing", nil, filled("h\n${config-lines:k:p=}\nt\n"), "h\nt\n", nil},