diff --git a/README.md b/README.md index fe7515e..7cd0dbc 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,8 @@ hyphenated field can't be an operand. Renders e.g. `19.99 x 3 = 59.97`. An operand that can never be a number (`"abc"`, or a choice of such) is rejected at load, as is a division by a constant zero (`1/0`, or a fixed `"0"` field); an operand that sometimes is not a number yields -`NaN`, and a division by a sometimes-zero one `Inf` — both print rather than fail. +`NaN`, and a division by one that is not constant `Inf` — both print rather than +fail. ### Transforms @@ -390,14 +391,16 @@ tokens add cost in proportion to the output. - **64-bit targets only.** The gate builds amd64, and the buffer sizing a render pre-computes (renders × bytes) assumes a 64-bit int; on a 32-bit target it could overflow and panic. -- **A constant zero divisor is a load error; a sometimes-zero one prints `Inf`.** - `1/0` and a fixed `"0"` field are decidable, so they join the never-numeric - operand as a load error; a divisor that is only sometimes zero is data, and - `Inf` printing visibly is the never-fail rule. -- **A default written out, and a constant spelled as a sample, are load errors.** - `weight: 1`, `repeat: 1`, `separator: ""`, `int(5,5)`, `float(1,1,2)`, `+5` - and `05` each spell what a shorter form already spells, so each is rejected - naming that form. +- **A constant zero divisor is a load error; a divisor that is not constant prints + `Inf`.** `1/0` and a fixed `"0"` field are decidable, so they join the + never-numeric operand as a load error; the fold stops where an operand varies, + so `a/(b*c)` with `b` fixed at `0` and `c` varying loads and prints `Inf` every + draw — catching it needs zero-absorbing algebra for a shape nobody writes. +- **In data, a default written out and a constant spelled as a sample are load + errors.** `weight: 1`, `repeat: 1`, `separator: ""`, `int(5,5)`, `float(1,1,2)`, + `+5` and `05` each spell what a shorter form already spells, so each is rejected + naming that form. The CLI's numbers follow the shell instead: `--seed 007` and + `--repeat +3` are 7 and 3, as every command line reads them. - **Samples say what they emit, transforms what they do.** `{upper(2)}` is two letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on whether the argument looks like a number. diff --git a/builtins.go b/builtins.go index c592d6e..d72ce2e 100644 --- a/builtins.go +++ b/builtins.go @@ -2,6 +2,7 @@ package fejkdata import ( "encoding/base64" + "errors" "fmt" "math" "strconv" @@ -9,12 +10,12 @@ import ( "unicode" ) -// maxLen caps sample output lengths (hex, nanoid, base64) and the renders a repeat -// multiplies to along any path; maxDecimals caps float/calc decimal places. So a -// fat-fingered or overflowing argument fails at New instead of trying to allocate -// gigabytes — or panicking — at render. +// maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower) +// and maxDecimals float/calc decimal places, so a fat-fingered or overflowing +// argument fails at New instead of trying to allocate gigabytes — or panicking — +// at render. const ( - maxLen = MaxRepeat + maxLen = 1 << 20 maxDecimals = 1024 ) @@ -224,6 +225,9 @@ func randChars(r rng, n int, alphabet string) string { // plainInt parses an integer arg written the one way: no sign, no leading zero. func plainInt(s string) (int, error) { n, err := strconv.Atoi(s) + if errors.Is(err, strconv.ErrRange) { + return 0, fmt.Errorf("%q is past the integer range: %w", s, err) + } if err != nil { return 0, fmt.Errorf("%q is not an integer", s) } @@ -235,6 +239,9 @@ func plainInt(s string) (int, error) { func posIntArg(_ map[string]node, a []string) error { n, err := plainInt(a[0]) + if errors.Is(err, strconv.ErrRange) { + return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen) + } if err != nil { return fmt.Errorf("count %w", err) } @@ -248,10 +255,13 @@ func posIntArg(_ map[string]node, a []string) error { } func intRangeArgs(_ map[string]node, a []string) error { - lo, e1 := plainInt(a[0]) - hi, e2 := plainInt(a[1]) - if e1 != nil || e2 != nil { - return fmt.Errorf("int(min,max) needs plain integers, got %q,%q", a[0], a[1]) + lo, err := plainInt(a[0]) + if err != nil { + return fmt.Errorf("int(min,max): min %w", err) + } + hi, err := plainInt(a[1]) + if err != nil { + return fmt.Errorf("int(min,max): max %w", err) } if lo > hi { return fmt.Errorf("int(min,max): min %d > max %d", lo, hi) @@ -268,9 +278,12 @@ func intRangeArgs(_ map[string]node, a []string) error { func floatArgs(_ map[string]node, a []string) error { lo, e1 := strconv.ParseFloat(a[0], 64) hi, e2 := strconv.ParseFloat(a[1], 64) - dp, e3 := plainInt(a[2]) - if e1 != nil || e2 != nil || e3 != nil { - return fmt.Errorf("float(min,max,dp) needs numeric bounds and a plain decimals count, got %q,%q,%q", a[0], a[1], a[2]) + if e1 != nil || e2 != nil { + return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1]) + } + dp, err := plainInt(a[2]) + if err != nil { + return fmt.Errorf("float(min,max,dp): decimals %w", err) } if math.IsNaN(lo) || math.IsNaN(hi) || math.IsInf(lo, 0) || math.IsInf(hi, 0) { return fmt.Errorf("float(min,max,dp) needs finite bounds, got %q,%q", a[0], a[1]) diff --git a/calc.go b/calc.go index 212466f..cd776b7 100644 --- a/calc.go +++ b/calc.go @@ -82,8 +82,12 @@ func checkCalc(fields map[string]node, args []string) error { return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor) } if len(args) == 2 { - if dp, err := plainInt(args[1]); err != nil || dp < 0 || dp > maxDecimals { - return fmt.Errorf("calc decimals %q must be a plain integer in 0..%d", args[1], maxDecimals) + dp, err := plainInt(args[1]) + if err != nil { + return fmt.Errorf("calc decimals %w", err) + } + if dp < 0 || dp > maxDecimals { + return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals) } } return nil diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index a5514c0..d6b2d05 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -215,8 +215,9 @@ func (in invocation) options() []fejkdata.Option { } // write streams the path's renders to w, repeat of them joined by the separator -// and ended by a newline. A path that renders once renders every time, so the only -// failure comes before anything is written. +// and ended by a newline. A path that renders once renders every time, so a Fake +// failure comes before anything is written; a write failure surfaces from Flush, +// bufio keeping the first one. func (in invocation) write(f *fejkdata.Generator, w io.Writer) error { out := bufio.NewWriter(w) for i := 0; i < in.repeat; i++ { diff --git a/fejkdata.go b/fejkdata.go index 599a73d..6cfdc01 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -28,9 +28,12 @@ import ( //go:embed data var shippedFS embed.FS -// MaxRepeat caps a repeat, and the renders nested repeats multiply to along any path. +// MaxRepeat caps a repeat, and the renders nested repeats multiply to along any +// path; the CLI's --repeat shares it. const MaxRepeat = 1 << 20 +var _ [^uint(0)>>63 - 1]struct{} // 64-bit only, per the README's Decisions + // ErrNoData is returned by New when no source is loaded at all. var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS") @@ -207,7 +210,7 @@ func join(prefix, name string) string { return prefix + "." + name } -// randomBytes seeds an unseeded generator; a failure is an error, never a fixed seed. +// randomBytes seeds an unseeded generator. var randomBytes = crand.Read func newRand(seed uint64, seeded bool) (*session, error) { diff --git a/graph.go b/graph.go index 4e2d09a..887c0ec 100644 --- a/graph.go +++ b/graph.go @@ -181,8 +181,8 @@ func checkRepeatReach(root map[string]node) error { return r } return walkNodes(root, func(path string, n node) error { - if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > maxLen { - return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), maxLen) + if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > MaxRepeat { + return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), MaxRepeat) } return nil }) diff --git a/node.go b/node.go index c1ecf04..fb6aee9 100644 --- a/node.go +++ b/node.go @@ -288,8 +288,8 @@ func repeatOf(m map[string]any) (int, error) { if r == 1 { return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it") } - if r > maxLen { // caps the renders one repeat asks for; checkRepeatReach bounds what nested ones multiply to - return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, maxLen) + if r > MaxRepeat { // caps the renders one repeat asks for; checkRepeatReach bounds what nested ones multiply to + return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat) } return int(r), nil }