From 5359da9305d9401268dce2cae07e3bcd66d2c555 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Thu, 10 Sep 2026 05:48:17 +0900 Subject: [PATCH] fix modulo by zero panic `{{ a % b }}` with a zero divisor panicked with Go's `runtime error: integer divide by zero`, and the panic escaped Template.Execute: recover() in eval.go re-panics anything that is a runtime.Error, so an operator that can produce one has to guard before it runs. itemDiv does that already; itemMod did not. Modulo by zero now results in a runtime error instead of a panic, like division by zero since f1947cd. `%` has no float form, so the divisor is truncated by toInt/toUint before it is used. `{{ 10 % 0.5 }}` therefore divides by zero as well, and the guard sits on the converted divisor rather than on the raw right operand. --- eval.go | 19 ++++++++++++++++--- eval_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/eval.go b/eval.go index a9ba415..580e627 100644 --- a/eval.go +++ b/eval.go @@ -1023,12 +1023,25 @@ func (st *Runtime) evalMultiplicativeExpression(node *MultiplicativeExprNode) re node.Left.errorf("a non numeric value in multiplicative expression") } case itemMod: + // the divisor is truncated to an integer, so a non-zero fraction is a zero divisor too if isInt(kind) { - left = reflect.ValueOf(left.Int() % toInt(right)) + divisor := toInt(right) + if divisor == 0 { + node.Left.errorf("modulo by zero") + } + left = reflect.ValueOf(left.Int() % divisor) } else if isFloat(kind) { - left = reflect.ValueOf(int64(left.Float()) % toInt(right)) + divisor := toInt(right) + if divisor == 0 { + node.Left.errorf("modulo by zero") + } + left = reflect.ValueOf(int64(left.Float()) % divisor) } else if isUint(kind) { - left = reflect.ValueOf(left.Uint() % toUint(right)) + divisor := toUint(right) + if divisor == 0 { + node.Left.errorf("modulo by zero") + } + left = reflect.ValueOf(left.Uint() % divisor) } else { node.Left.errorf("a non numeric value in multiplicative expression") } diff --git a/eval_test.go b/eval_test.go index 23a006b..9689cf0 100644 --- a/eval_test.go +++ b/eval_test.go @@ -869,6 +869,59 @@ func TestDivisionByZero(t *testing.T) { } } +func TestModuloByZero(t *testing.T) { + vars := VarMap{"u": reflect.ValueOf(uint(5))} + + for _, tc := range []struct{ name, template string }{ + {"int modulo by zero", `{{ 5 % 0 }}`}, + {"int modulo by zero float", `{{ 5 % 0.0 }}`}, + {"int modulo by fraction", `{{ 5 % 0.5 }}`}, + {"float modulo by zero", `{{ 5.0 % 0 }}`}, + {"float modulo by zero float", `{{ 5.0 % 0.0 }}`}, + {"float modulo by fraction", `{{ 5.0 % 0.5 }}`}, + {"uint modulo by zero", `{{ u % 0 }}`}, + {"uint modulo by fraction", `{{ u % 0.5 }}`}, + } { + t.Run(tc.name, func(t *testing.T) { + var set = NewSet(NewInMemLoader(), WithSafeWriter(nil)) + tt, err := set.parse(tc.name, tc.template, false) + if err != nil { + t.Fatal(err) + } + err = tt.Execute(io.Discard, vars, nil) + if err == nil { + t.Fatal("expected modulo by zero to fail with a runtime error, but got nil") + } + if !strings.Contains(err.Error(), "modulo by zero") { + t.Fatalf("expected runtime error to be about modulo by zero, but got %q", err.Error()) + } + }) + } + + for _, tc := range []struct{ name, template, expected string }{ + {"int modulo", `{{ 5 % 2 }}`, "1"}, + {"int modulo negative divisor", `{{ 5 % -3 }}`, "2"}, + {"int modulo truncated divisor", `{{ 5 % 2.5 }}`, "1"}, + {"float modulo", `{{ 5.5 % 2 }}`, "1"}, + {"uint modulo", `{{ u % 3 }}`, "2"}, + } { + t.Run(tc.name, func(t *testing.T) { + var set = NewSet(NewInMemLoader(), WithSafeWriter(nil)) + tt, err := set.parse(tc.name, tc.template, false) + if err != nil { + t.Fatal(err) + } + buf := new(bytes.Buffer) + if err := tt.Execute(buf, vars, nil); err != nil { + t.Fatal(err) + } + if buf.String() != tc.expected { + t.Fatalf("expected %q, but got %q", tc.expected, buf.String()) + } + }) + } +} + func TestRecursiveInclude(t *testing.T) { l := NewInMemLoader() l.Set("recursive_incl_1", `{{ include "./recursive_incl_2" }}`)