diff --git a/gclient_eval.py b/gclient_eval.py index a5d1deeb6..5066a2df4 100644 --- a/gclient_eval.py +++ b/gclient_eval.py @@ -196,3 +196,69 @@ def Exec(content, global_scope, local_scope, filename=''): getattr(node_or_string, 'lineno', ''))) _GCLIENT_SCHEMA.validate(local_scope) + + +def EvaluateCondition(condition, variables, referenced_variables=None): + """Safely evaluates a boolean condition. Returns the result.""" + if not referenced_variables: + referenced_variables = set() + _allowed_names = {'None': None, 'True': True, 'False': False} + main_node = ast.parse(condition, mode='eval') + if isinstance(main_node, ast.Expression): + main_node = main_node.body + def _convert(node): + if isinstance(node, ast.Str): + return node.s + elif isinstance(node, ast.Name): + if node.id in referenced_variables: + raise ValueError( + 'invalid cyclic reference to %r (inside %r)' % ( + node.id, condition)) + elif node.id in _allowed_names: + return _allowed_names[node.id] + elif node.id in variables: + return EvaluateCondition( + variables[node.id], + variables, + referenced_variables.union([node.id])) + else: + raise ValueError( + 'invalid name %r (inside %r)' % (node.id, condition)) + elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or): + if len(node.values) != 2: + raise ValueError( + 'invalid "or": exactly 2 operands required (inside %r)' % ( + condition)) + return _convert(node.values[0]) or _convert(node.values[1]) + elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And): + if len(node.values) != 2: + raise ValueError( + 'invalid "and": exactly 2 operands required (inside %r)' % ( + condition)) + return _convert(node.values[0]) and _convert(node.values[1]) + elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + return not _convert(node.operand) + elif isinstance(node, ast.Compare): + if len(node.ops) != 1: + raise ValueError( + 'invalid compare: exactly 1 operator required (inside %r)' % ( + condition)) + if len(node.comparators) != 1: + raise ValueError( + 'invalid compare: exactly 1 comparator required (inside %r)' % ( + condition)) + + left = _convert(node.left) + right = _convert(node.comparators[0]) + + if isinstance(node.ops[0], ast.Eq): + return left == right + + raise ValueError( + 'unexpected operator: %s %s (inside %r)' % ( + node.ops[0], ast.dump(node), condition)) + else: + raise ValueError( + 'unexpected AST node: %s %s (inside %r)' % ( + node, ast.dump(node), condition)) + return _convert(main_node) diff --git a/tests/gclient_eval_unittest.py b/tests/gclient_eval_unittest.py index 5cca55ac8..7ef6b38ae 100755 --- a/tests/gclient_eval_unittest.py +++ b/tests/gclient_eval_unittest.py @@ -118,6 +118,34 @@ class ExecTest(unittest.TestCase): 'deps': collections.OrderedDict([('a_dep', 'abarb')]), }, local_scope) + +class EvaluateConditionTest(unittest.TestCase): + def test_true(self): + self.assertTrue(gclient_eval.EvaluateCondition('True', {})) + + def test_variable(self): + self.assertFalse(gclient_eval.EvaluateCondition('foo', {'foo': 'False'})) + + def test_variable_cyclic_reference(self): + with self.assertRaises(ValueError) as cm: + self.assertTrue(gclient_eval.EvaluateCondition('bar', {'bar': 'bar'})) + self.assertIn( + 'invalid cyclic reference to \'bar\' (inside \'bar\')', + str(cm.exception)) + + def test_operators(self): + self.assertFalse(gclient_eval.EvaluateCondition( + 'a and not (b or c)', {'a': 'True', 'b': 'False', 'c': 'True'})) + + def test_expansion(self): + self.assertTrue(gclient_eval.EvaluateCondition( + 'a or b', {'a': 'b and c', 'b': 'not c', 'c': 'False'})) + + def test_string_equality(self): + self.assertFalse(gclient_eval.EvaluateCondition( + 'foo == "bar"', {'foo': '"baz"'})) + + if __name__ == '__main__': level = logging.DEBUG if '-v' in sys.argv else logging.FATAL logging.basicConfig(