From ac9d08d6fc3d842be73d2ed96c9f6d2d7444cdc3 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Fri, 8 Dec 2023 18:35:56 +0100 Subject: [PATCH 1/4] reorganized Eval() return errors and call EvalItem() on each item --- calc.go | 180 ++++++++++++++++++++++++++++++-------------------------- main.go | 18 ++++-- util.go | 4 ++ 3 files changed, 113 insertions(+), 89 deletions(-) diff --git a/calc.go b/calc.go index 5fdd6da..567ba04 100644 --- a/calc.go +++ b/calc.go @@ -222,12 +222,12 @@ func (c *Calc) Prompt() string { } // the actual work horse, evaluate a line of calc command[s] -func (c *Calc) Eval(line string) { +func (c *Calc) Eval(line string) error { // remove surrounding whitespace and comments, if any line = strings.TrimSpace(c.Comment.ReplaceAllString(line, "")) if line == "" { - return + return nil } items := c.Space.Split(line, -1) @@ -239,110 +239,120 @@ func (c *Calc) Eval(line string) { c.notdone = false } - num, err := strconv.ParseFloat(item, 64) + if err := c.EvalItem(item); err != nil { + return err + } + } + + if c.showstack && !c.stdin { + dots := "" + + if c.stack.Len() > 5 { + dots = "... " + } + last := c.stack.Last(5) + fmt.Printf("stack: %s%s\n", dots, list2str(last)) + } + + return nil +} + +func (c *Calc) EvalItem(item string) error { + num, err := strconv.ParseFloat(item, 64) + if err == nil { + c.stack.Backup() + c.stack.Push(num) + } else { + // try hex + var i int + _, err := fmt.Sscanf(item, "0x%x", &i) if err == nil { c.stack.Backup() - c.stack.Push(num) - } else { - // try hex - var i int - _, err := fmt.Sscanf(item, "0x%x", &i) - if err == nil { - c.stack.Backup() - c.stack.Push(float64(i)) - continue - } - - if contains(c.Constants, item) { - // put the constant onto the stack - c.stack.Backup() - c.stack.Push(const2num(item)) - continue - } + c.stack.Push(float64(i)) + return nil + } - if exists(c.Funcalls, item) { - if err := c.DoFuncall(item); err != nil { - fmt.Println(err) - } else { - c.Result() - } - continue - } + if contains(c.Constants, item) { + // put the constant onto the stack + c.stack.Backup() + c.stack.Push(const2num(item)) + return nil + } - if exists(c.BatchFuncalls, item) { - if !c.batch { - fmt.Println("only supported in batch mode") - continue - } - - if err := c.DoFuncall(item); err != nil { - fmt.Println(err) - } else { - c.Result() - } - continue + if exists(c.Funcalls, item) { + if err := c.DoFuncall(item); err != nil { + return Error(err.Error()) + } else { + c.Result() } + return nil + } - if contains(c.LuaFunctions, item) { - // user provided custom lua functions - c.EvalLuaFunction(item) - continue + if exists(c.BatchFuncalls, item) { + if !c.batch { + return Error("only supported in batch mode") } - regmatches := c.Register.FindStringSubmatch(item) - if len(regmatches) == 3 { - switch regmatches[1] { - case ">": - c.PutVar(regmatches[2]) - case "<": - c.GetVar(regmatches[2]) - } - continue + if err := c.DoFuncall(item); err != nil { + return Error(err.Error()) + } else { + c.Result() } + return nil + } - // internal commands - if exists(c.Commands, item) { - c.Commands[item].Func(c) - continue - } + if contains(c.LuaFunctions, item) { + // user provided custom lua functions + c.EvalLuaFunction(item) + return nil + } - if exists(c.ShowCommands, item) { - c.ShowCommands[item].Func(c) - continue + regmatches := c.Register.FindStringSubmatch(item) + if len(regmatches) == 3 { + switch regmatches[1] { + case ">": + c.PutVar(regmatches[2]) + case "<": + c.GetVar(regmatches[2]) } + return nil + } - if exists(c.StackCommands, item) { - c.StackCommands[item].Func(c) - continue - } + // internal commands + // FIXME: propagate errors + if exists(c.Commands, item) { + c.Commands[item].Func(c) + return nil + } - if exists(c.SettingsCommands, item) { - c.SettingsCommands[item].Func(c) - continue - } + if exists(c.ShowCommands, item) { + c.ShowCommands[item].Func(c) + return nil + } - switch item { - case "?": - fallthrough - case "help": - c.PrintHelp() + if exists(c.StackCommands, item) { + c.StackCommands[item].Func(c) + return nil + } - default: - fmt.Println("unknown command or operator!") - } + if exists(c.SettingsCommands, item) { + c.SettingsCommands[item].Func(c) + return nil } - } - if c.showstack && !c.stdin { - dots := "" + switch item { + case "?": + fallthrough + case "help": + c.PrintHelp() - if c.stack.Len() > 5 { - dots = "... " + default: + return Error("unknown command or operator") } - last := c.stack.Last(5) - fmt.Printf("stack: %s%s\n", dots, list2str(last)) } + + return nil } // Execute a math function, check if it is defined just in case @@ -355,7 +365,7 @@ func (c *Calc) DoFuncall(funcname string) error { } if function == nil { - panic("function not defined but in completion list") + return Error("function not defined but in completion list") } var args Numbers diff --git a/main.go b/main.go index 4c05cfe..825d280 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ import ( lua "github.com/yuin/gopher-lua" ) -const VERSION string = "2.0.12" +const VERSION string = "2.0.13" const Usage string = `This is rpn, a reverse polish notation calculator cli. @@ -119,7 +119,11 @@ func Main() int { // commandline calc operation, no readline etc needed // called like rpn 2 2 + calc.stdin = true - calc.Eval(strings.Join(flag.Args(), " ")) + if err := calc.Eval(strings.Join(flag.Args(), " ")); err != nil { + fmt.Println(err) + return 1 + } + return 0 } @@ -153,7 +157,10 @@ func Main() int { break } - calc.Eval(line) + err = calc.Eval(line) + if err != nil { + fmt.Println(err) + } rl.SetPrompt(calc.Prompt()) } @@ -162,7 +169,10 @@ func Main() int { // echo 1 2 3 4 | rpn + // batch mode enabled automatically calc.batch = true - calc.Eval(flag.Args()[0]) + if err = calc.Eval(flag.Args()[0]); err != nil { + fmt.Println(err) + return 1 + } } return 0 diff --git a/util.go b/util.go index f9b7d81..46593a5 100644 --- a/util.go +++ b/util.go @@ -71,3 +71,7 @@ func const2num(name string) float64 { func list2str(list Numbers) string { return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]") } + +func Error(m string) error { + return fmt.Errorf("Error: %s!", m) +} From e4a8af9b5bc6a59d781dd2dc020994d6c12fba52 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Fri, 8 Dec 2023 18:36:33 +0100 Subject: [PATCH 2/4] fix negative shift amount error, found with fuzzy testing :) --- funcs.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/funcs.go b/funcs.go index 0033113..4b116d4 100644 --- a/funcs.go +++ b/funcs.go @@ -457,12 +457,20 @@ func DefineFunctions() Funcalls { "<": NewFuncall( func(arg Numbers) R { + // Shift by negative number provibited, so check it. + // Note that we check agains uint64 overflow as well here + if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 { + return NewR(0, errors.New("negative shift amount")) + } return NewR(float64(int(arg[0])<": NewFuncall( func(arg Numbers) R { + if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 { + return NewR(0, errors.New("negative shift amount")) + } return NewR(float64(int(arg[0])>>int(arg[1])), nil) }, 2), From 49e01565b90dea775712a7c8aa962eda0a6ec72c Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Fri, 8 Dec 2023 18:37:35 +0100 Subject: [PATCH 3/4] catch exec errors --- t/cmdline-invalidcommand.txtar | 2 +- t/cmdline-short-stack.txtar | 2 +- t/cmdlinecalc-divzero.txtar | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/t/cmdline-invalidcommand.txtar b/t/cmdline-invalidcommand.txtar index f30dbe4..f1c0949 100644 --- a/t/cmdline-invalidcommand.txtar +++ b/t/cmdline-invalidcommand.txtar @@ -1,2 +1,2 @@ -exec testrpn 1 2 dumb +! exec testrpn 1 2 dumb stdout 'unknown command or operator' diff --git a/t/cmdline-short-stack.txtar b/t/cmdline-short-stack.txtar index f3ce466..30ac14f 100644 --- a/t/cmdline-short-stack.txtar +++ b/t/cmdline-short-stack.txtar @@ -1,2 +1,2 @@ -exec testrpn 4 + +! exec testrpn 4 + stdout 'stack doesn''t provide enough arguments' diff --git a/t/cmdlinecalc-divzero.txtar b/t/cmdlinecalc-divzero.txtar index 411c636..bc32f83 100644 --- a/t/cmdlinecalc-divzero.txtar +++ b/t/cmdlinecalc-divzero.txtar @@ -1,2 +1,2 @@ -exec testrpn 100 50 50 - / -stdout 'division by null\n' +! exec testrpn 100 50 50 - / +stdout 'division by null' From 222dc3a7346c16ab93d4f2ee71156d3c397826aa Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Fri, 8 Dec 2023 18:37:59 +0100 Subject: [PATCH 4/4] added fuzzy testing --- Makefile | 9 ++++--- calc_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 6404ab8..a449299 100644 --- a/Makefile +++ b/Makefile @@ -51,10 +51,13 @@ install: buildlocal install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/ clean: - rm -rf $(tool) coverage.out + rm -rf $(tool) coverage.out testdata -test: - go test -v ./... +test: clean + go test ./... $(ARGS) + +testfuzzy: clean + go test -fuzz ./... $(ARGS) singletest: @echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v" diff --git a/calc_test.go b/calc_test.go index dd38780..87f0925 100644 --- a/calc_test.go +++ b/calc_test.go @@ -19,6 +19,8 @@ package main import ( "fmt" + "strconv" + "strings" "testing" lua "github.com/yuin/gopher-lua" @@ -75,7 +77,9 @@ func TestCommentsAndWhitespace(t *testing.T) { t.Run(testname, func(t *testing.T) { for _, line := range tt.cmd { - calc.Eval(line) + if err := calc.Eval(line); err != nil { + t.Errorf(err.Error()) + } } got := calc.stack.Last() @@ -288,7 +292,9 @@ func TestCalc(t *testing.T) { t.Run(testname, func(t *testing.T) { calc.batch = tt.batch - calc.Eval(tt.cmd) + if err := calc.Eval(tt.cmd); err != nil { + t.Errorf(err.Error()) + } got := calc.Result() calc.stack.Clear() if got != tt.exp { @@ -350,3 +356,60 @@ func TestCalcLua(t *testing.T) { }) } } + +func FuzzEval(f *testing.F) { + legal := []string{ + "dump", + "showstack", + "help", + "Pi 31 *", + "SqrtE Pi /", + "55.5 yards-to-meters", + "2 4 +", + "7 8 batch sum", + "7 8 %-", + "7 8 clear", + "7 8 /", + "b", + "#444", + "", line) + // not corpus and empty? + if !contains(legal, line) && len(line) > 0 { + item := strings.TrimSpace(calc.Comment.ReplaceAllString(line, "")) + _, hexerr := fmt.Sscanf(item, "0x%x", &i) + // no comment? + if len(item) > 0 { + // no known command or function? + if _, err := strconv.ParseFloat(item, 64); err != nil { + if !contains(calc.Constants, item) && + !exists(calc.Funcalls, item) && + !exists(calc.BatchFuncalls, item) && + !contains(calc.LuaFunctions, item) && + !exists(calc.Commands, item) && + !exists(calc.ShowCommands, item) && + !exists(calc.SettingsCommands, item) && + !exists(calc.StackCommands, item) && + !calc.Register.MatchString(item) && + item != "?" && item != "help" && + hexerr != nil { + t.Errorf("Fuzzy input accepted: <%s>", line) + } + } + } + } + } + }) +}