From 8730ef333d24050f42d2463b0444a2042a288241 Mon Sep 17 00:00:00 2001 From: Ari Koivula Date: Tue, 8 Jul 2014 01:55:08 +0300 Subject: [PATCH] [calc] Add guards to pow and mul functions to avoid hangs. - Fixes #538. - Removed the drivel from calc when exceptions are raised. Printing the actual error might not be pretty, but at least it's not completely useless. --- willie/modules/calc.py | 9 ++-- willie/tools.py | 119 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 19 deletions(-) diff --git a/willie/modules/calc.py b/willie/modules/calc.py index a459322735..d2c9a3e1a6 100644 --- a/willie/modules/calc.py +++ b/willie/modules/calc.py @@ -23,7 +23,7 @@ @commands('c', 'calc') @example('.c 5 + 3', '8') def c(bot, trigger): - """Google calculator.""" + """Evaluate some calculation.""" if not trigger.group(2): return bot.reply("Nothing to calculate.") # Account for the silly non-Anglophones and their silly radix point. @@ -32,10 +32,9 @@ def c(bot, trigger): result = str(eval_equation(eqn)) except ZeroDivisionError: result = "Division by zero is not supported in this universe." - except Exception: - result = ("Sorry, I can't calculate that with this command. " - "I might have another one that can. " - "Use .commands for a list.") + except Exception as e: + result = "{error}: {msg}".format( + error=type(e), msg=e) bot.reply(result) diff --git a/willie/tools.py b/willie/tools.py index 90c9b72d7b..655a7f0c27 100644 --- a/willie/tools.py +++ b/willie/tools.py @@ -16,6 +16,7 @@ from __future__ import print_function from __future__ import unicode_literals +import math import datetime import sys import os @@ -106,20 +107,110 @@ def _eval_node(self, node): "Ast.Node '%s' not implemented." % (type(node).__name__,) ) -_bin_ops = { - ast.Add: operator.add, - ast.Sub: operator.sub, - ast.Mult: operator.mul, - ast.Div: operator.truediv, - ast.Pow: operator.pow, - ast.Mod: operator.mod, - ast.FloorDiv: operator.floordiv, -} -_unary_ops = { - ast.USub: operator.neg, - ast.UAdd: operator.pos, -} -eval_equation = ExpressionEvaluator(_bin_ops, _unary_ops) +def guarded_mul(left, right): + """Decorate a function to raise an error for values > limit.""" + if not isinstance(left, int) or not isinstance(right, int): + # Only handle ints because floats will overflow anyway. + pass + elif left in (0, 1) or right in (0, 1): + # Ignore trivial cases. + pass + elif left.bit_length() + right.bit_length() > 664386: + # 664386 is the number of bits (10**100000)**2 has, which is instant on my + # laptop, while (10**1000000)**2 has a noticeable delay. It could certainly + # be improved. + raise ValueError("Value is too large to be handled in limited time and memory.") + + return operator.mul(left, right) + +def pow_complexity(num, exp): + """Estimate the worst case time pow(num, exp) takes to calculate. + + This function is based on experimetal data from the time it takes to + calculate "num**exp" on laptop with i7-2670QM processor on a 32 bit + CPython 2.7.6 interpreter on Windows. + + It tries to implement this surface: x=exp, y=num + 1e5 2e5 3e5 4e5 5e5 6e5 7e5 8e5 9e5 + e1 0.03 0.09 0.16 0.25 0.35 0.46 0.60 0.73 0.88 + e2 0.08 0.24 0.46 0.73 1.03 1.40 1.80 2.21 2.63 + e3 0.15 0.46 0.87 1.39 1.99 2.63 3.35 4.18 5.15 + e4 0.24 0.73 1.39 2.20 3.11 4.18 5.39 6.59 7.88 + e5 0.34 1.03 2.00 3.12 4.48 5.97 7.56 9.37 11.34 + e6 0.46 1.39 2.62 4.16 5.97 7.86 10.09 12.56 15.39 + e7 0.60 1.79 3.34 5.39 7.60 10.16 13.00 16.23 19.44 + e8 0.73 2.20 4.18 6.60 9.37 12.60 16.26 19.83 23.70 + e9 0.87 2.62 5.15 7.93 11.34 15.44 19.40 23.66 28.58 + + For powers of 2 it tries to implement this surface: + 1e7 2e7 3e7 4e7 5e7 6e7 7e7 8e7 9e7 + 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 + 2 0.21 0.44 0.71 0.92 1.20 1.49 1.66 1.95 2.23 + 4 0.43 0.91 1.49 1.96 2.50 3.13 3.54 4.10 4.77 + 8 0.70 1.50 2.24 3.16 3.83 4.66 5.58 6.56 7.67 + + The function number were selected by starting with the theoretical + complexity of exp * log2(num)**2 and fiddling with the exponents + untill it more or less matched with the table. + + Because this function is based on a limited set of data it might + not give accurate results outside these boundaries. The results + derived from large num and exp were quite accurate for small num + and very large exp though, except when num was a power of 2. + + """ + if num in (0, 1) or exp in (0, 1): + return 0 + elif (num & (num - 1)) == 0: + # For powers of 2 the scaling is a bit different. + return exp**1.092 * num.bit_length()**1.65 / 623212911.121 + else: + return exp**1.590 * num.bit_length()**1.73 / 36864057619.3 + + +def guarded_pow(left, right): + if not isinstance(left, int) or not isinstance(right, int): + # Only handle ints because floats will overflow anyway. + pass + elif pow_complexity(left, right) < 0.5: + # Value 0.5 is arbitary and based on a estimated runtime of 0.5s + # on a fairly decent laptop processor. + pass + else: + raise ValueError("Pow expression too complex to calculate.") + + return operator.pow(left, right) + + +class EquationEvaluator(ExpressionEvaluator): + __bin_ops = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: guarded_mul, + ast.Div: operator.truediv, + ast.Pow: guarded_pow, + ast.Mod: operator.mod, + ast.FloorDiv: operator.floordiv, + } + __unary_ops = { + ast.USub: operator.neg, + ast.UAdd: operator.pos, + } + + def __init__(self): + ExpressionEvaluator.__init__(self, + bin_ops=self.__bin_ops, + unary_ops=self.__unary_ops) + + def __call__(self, expression_str): + result = ExpressionEvaluator.__call__(self, expression_str) + if math.log10(result) > 255: + # Guard against very large values because converting them into + # string might take a very long time. + raise ValueError("Result is too large to be printed.") + return result + +eval_equation = EquationEvaluator() """Evaluates a Python equation expression and returns the result. Supports addition (+), subtraction (-), multiplication (*), division (/),