diff --git a/src/RestrictedPython/Eval.py b/src/RestrictedPython/Eval.py index 5051a6d..836ea5e 100644 --- a/src/RestrictedPython/Eval.py +++ b/src/RestrictedPython/Eval.py @@ -12,16 +12,22 @@ ############################################################################## """Restricted Python Expressions.""" -from RestrictedPython.RCompile import compile_restricted_eval -from string import strip -from string import translate +from ._compat import IS_PY2 +from .compile import compile_restricted_eval -import string +import ast -nltosp = string.maketrans('\r\n', ' ') +if IS_PY2: + from string import maketrans +else: + maketrans = str.maketrans -default_guarded_getattr = getattr # No restrictions. + +nltosp = maketrans('\r\n', ' ') + +# No restrictions. +default_guarded_getattr = getattr def default_guarded_getitem(ob, index): @@ -29,15 +35,17 @@ def default_guarded_getitem(ob, index): return ob[index] -PROFILE = 0 - - class RestrictionCapableEval(object): """A base class for restricted code.""" globals = {'__builtins__': None} - rcode = None # restricted - ucode = None # unrestricted + # restricted + rcode = None + + # unrestricted + ucode = None + + # Names used by the expression used = None def __init__(self, expr): @@ -47,74 +55,60 @@ def __init__(self, expr): expr -- a string containing the expression to be evaluated. """ - expr = strip(expr) + expr = expr.strip() self.__name__ = expr - expr = translate(expr, nltosp) + expr = expr.translate(nltosp) self.expr = expr - self.prepUnrestrictedCode() # Catch syntax errors. + # Catch syntax errors. + self.prepUnrestrictedCode() def prepRestrictedCode(self): if self.rcode is None: - if PROFILE: - from time import clock - start = clock() - co, err, warn, used = compile_restricted_eval( - self.expr, '') - if PROFILE: - end = clock() - print('prepRestrictedCode: %d ms for %s' % ( - (end - start) * 1000, repr(self.expr))) - if err: - raise SyntaxError(err[0]) - self.used = tuple(used.keys()) - self.rcode = co + result = compile_restricted_eval(self.expr, '') + if result.errors: + raise SyntaxError(result.errors[0]) + self.used = tuple(result.used_names) + self.rcode = result.code def prepUnrestrictedCode(self): if self.ucode is None: - # Use the standard compiler. - co = compile(self.expr, '', 'eval') + exp_node = compile( + self.expr, + '', + 'eval', + ast.PyCF_ONLY_AST) + + co = compile(exp_node, '', 'eval') + + # Examine the ast to discover which names the expression needs. if self.used is None: - # Examine the code object, discovering which names - # the expression needs. - names = list(co.co_names) - used = {} - i = 0 - code = co.co_code - l = len(code) - LOAD_NAME = 101 - HAVE_ARGUMENT = 90 - while(i < l): - c = ord(code[i]) - if c == LOAD_NAME: - name = names[ord(code[i + 1]) + 256 * ord(code[i + 2])] - used[name] = 1 - i = i + 3 - elif c >= HAVE_ARGUMENT: - i = i + 3 - else: - i = i + 1 - self.used = tuple(used.keys()) + used = set() + for node in ast.walk(exp_node): + if isinstance(node, ast.Name): + if isinstance(node.ctx, ast.Load): + used.add(node.id) + + self.used = tuple(used) + self.ucode = co def eval(self, mapping): # This default implementation is probably not very useful. :-( # This is meant to be overridden. self.prepRestrictedCode() - code = self.rcode - d = {'_getattr_': default_guarded_getattr, - '_getitem_': default_guarded_getitem} - d.update(self.globals) - has_key = d.has_key + + global_scope = { + '_getattr_': default_guarded_getattr, + '_getitem_': default_guarded_getitem + } + + global_scope.update(self.globals) + for name in self.used: - try: - if not has_key(name): - d[name] = mapping[name] - except KeyError: - # Swallow KeyErrors since the expression - # might not actually need the name. If it - # does need the name, a NameError will occur. - pass - return eval(code, d) + if (name not in global_scope) and (name in mapping): + global_scope[name] = mapping[name] + + return eval(self.rcode, global_scope) def __call__(self, **kw): return self.eval(kw) diff --git a/src/RestrictedPython/tests/testRestrictions.py b/src/RestrictedPython/tests/testRestrictions.py index 369b125..c13e04d 100644 --- a/src/RestrictedPython/tests/testRestrictions.py +++ b/src/RestrictedPython/tests/testRestrictions.py @@ -6,7 +6,6 @@ # here instead. from RestrictedPython import PrintCollector -from RestrictedPython.Eval import RestrictionCapableEval from RestrictedPython.RCompile import compile_restricted from RestrictedPython.RCompile import RFunction from RestrictedPython.RCompile import RModule @@ -307,17 +306,6 @@ def test_NestedScopes1(self): res = self.execFunc('nested_scopes_1') self.assertEqual(res, 2) - def test_UnrestrictedEval(self): - expr = RestrictionCapableEval("{'a':[m.pop()]}['a'] + [m[0]]") - v = [12, 34] - expect = v[:] - expect.reverse() - res = expr.eval({'m': v}) - self.assertEqual(res, expect) - v = [12, 34] - res = expr(m=v) - self.assertEqual(res, expect) - def test_StackSize(self): for k, rfunc in rmodule.items(): if not k.startswith('_') and hasattr(rfunc, 'func_code'): diff --git a/src/RestrictedPython/transformer.py b/src/RestrictedPython/transformer.py index 7235a28..454ef4c 100644 --- a/src/RestrictedPython/transformer.py +++ b/src/RestrictedPython/transformer.py @@ -90,7 +90,13 @@ def __init__(self, errors=None, warnings=None, used_names=None): super(RestrictingNodeTransformer, self).__init__() self.errors = [] if errors is None else errors self.warnings = [] if warnings is None else warnings - self.used_names = [] if used_names is None else used_names + + # All the variables used by the incoming source. + # Internal names/variables, like the ones from 'gen_tmp_name', don't + # have to be added. + # 'used_names' is for example needed by 'RestrictionCapableEval' to + # know wich names it has to supply when calling the final code. + self.used_names = {} if used_names is None else used_names # Global counter to construct temporary variable names. self._tmp_idx = 0 @@ -116,12 +122,6 @@ def warn(self, node, info): self.warnings.append( 'Line {lineno}: {info}'.format(lineno=lineno, info=info)) - def use_name(self, node, info): - """Record a security error discovered during transformation.""" - lineno = getattr(node, 'lineno', None) - self.used_names.append( - 'Line {lineno}: {info}'.format(lineno=lineno, info=info)) - def guard_iter(self, node): """ Converts: @@ -585,6 +585,8 @@ def visit_Name(self, node): copy_locations(new_node, node) return new_node + self.used_names[node.id] = True + self.check_name(node, node.id) return node diff --git a/tests/test_compile.py b/tests/test_compile.py index d62a783..2e3d5cb 100644 --- a/tests/test_compile.py +++ b/tests/test_compile.py @@ -147,3 +147,11 @@ def test_compile__compile_restricted_eval__1(c_eval): def test_compile__compile_restricted_eval__2(e_eval): """It compiles code as an Expression.""" assert e_eval('4 * 6') == 24 + + +@pytest.mark.parametrize(*c_eval) +def test_compile__compile_restricted_eval__used_names(c_eval): + result = c_eval("a + b + func(x)") + assert result.errors == () + assert result.warnings == [] + assert result.used_names == {'a': True, 'b': True, 'x': True, 'func': True} diff --git a/tests/test_eval.py b/tests/test_eval.py new file mode 100644 index 0000000..f9046a4 --- /dev/null +++ b/tests/test_eval.py @@ -0,0 +1,42 @@ +from RestrictedPython.Eval import RestrictionCapableEval + +import pytest + + +exp = """ + {'a':[m.pop()]}['a'] \ + + [m[0]] +""" + + +def test_init(): + ob = RestrictionCapableEval(exp) + + assert ob.expr == "{'a':[m.pop()]}['a'] + [m[0]]" + assert ob.used == ('m', ) + assert ob.ucode is not None + assert ob.rcode is None + + +def test_init_with_syntax_error(): + with pytest.raises(SyntaxError): + RestrictionCapableEval("if:") + + +def test_prepRestrictedCode(): + ob = RestrictionCapableEval(exp) + ob.prepRestrictedCode() + assert ob.used == ('m', ) + assert ob.rcode is not None + + +def test_call(): + ob = RestrictionCapableEval(exp) + ret = ob(m=[1, 2]) + assert ret == [2, 1] + + +def test_eval(): + ob = RestrictionCapableEval(exp) + ret = ob.eval({'m': [1, 2]}) + assert ret == [2, 1] diff --git a/tests/transformer/test_transformer.py b/tests/transformer/test_transformer.py index 41651a2..9080c50 100644 --- a/tests/transformer/test_transformer.py +++ b/tests/transformer/test_transformer.py @@ -62,11 +62,7 @@ def test_transformer__RestrictingNodeTransformer__visit_Call__1(c_exec): loc = {} exec(result.code, {}, loc) assert loc['a'] == 3 - if c_exec is RestrictedPython.compile.compile_restricted_exec: - # The new version not yet supports `used_names`: - assert result.used_names == {} - else: - assert result.used_names == {'max': True} + assert result.used_names == {'max': True} YIELD = """\