diff --git a/mercurial/revset.py b/mercurial/revset.py --- a/mercurial/revset.py +++ b/mercurial/revset.py @@ -1308,6 +1308,27 @@ def optimize(x, small): return w + wa, (op, x[1], ta) return 1, x +_aliasarg = ('func', ('symbol', '_aliasarg')) +def _getaliasarg(tree): + """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X)) + return X, None otherwise. + """ + if (len(tree) == 3 and tree[:2] == _aliasarg + and tree[2][0] == 'string'): + return tree[2][1] + return None + +def _checkaliasarg(tree, known=None): + """Check tree contains no _aliasarg construct or only ones which + value is in known. Used to avoid alias placeholders injection. + """ + if isinstance(tree, tuple): + arg = _getaliasarg(tree) + if arg is not None and (not known or arg not in known): + raise error.ParseError(_("not a function: %s") % '_aliasarg') + for t in tree: + _checkaliasarg(t, known) + class revsetalias(object): funcre = re.compile('^([^(]+)\(([^)]+)\)$') args = None @@ -1324,7 +1345,9 @@ class revsetalias(object): self.tree = ('func', ('symbol', m.group(1))) self.args = [x.strip() for x in m.group(2).split(',')] for arg in self.args: - value = value.replace(arg, repr(arg)) + # _aliasarg() is an unknown symbol only used separate + # alias argument placeholders from regular strings. + value = value.replace(arg, '_aliasarg(%r)' % (arg,)) else: self.name = name self.tree = ('symbol', name) @@ -1332,6 +1355,8 @@ class revsetalias(object): self.replacement, pos = parse(value) if pos != len(value): raise error.ParseError(_('invalid token'), pos) + # Check for placeholder injection + _checkaliasarg(self.replacement, self.args) def _getalias(aliases, tree): """If tree looks like an unexpanded alias, return it. Return None @@ -1352,13 +1377,14 @@ def _getalias(aliases, tree): return None def _expandargs(tree, args): - """Replace all occurences of ('string', name) with the - substitution value of the same name in args, recursively. + """Replace _aliasarg instances with the substitution value of the + same name in args, recursively. """ - if not isinstance(tree, tuple): + if not tree or not isinstance(tree, tuple): return tree - if len(tree) == 2 and tree[0] == 'string': - return args.get(tree[1], tree) + arg = _getaliasarg(tree) + if arg is not None: + return args[arg] return tuple(_expandargs(t, args) for t in tree) def _expandaliases(aliases, tree, expanding): @@ -1376,22 +1402,22 @@ def _expandaliases(aliases, tree, expand raise error.ParseError(_('infinite expansion of revset alias "%s" ' 'detected') % alias.name) expanding.append(alias) - result = alias.replacement + result = _expandaliases(aliases, alias.replacement, expanding) + expanding.pop() if alias.args is not None: l = getlist(tree[2]) if len(l) != len(alias.args): raise error.ParseError( _('invalid number of arguments: %s') % len(l)) + l = [_expandaliases(aliases, a, []) for a in l] result = _expandargs(result, dict(zip(alias.args, l))) - # Recurse in place, the base expression may have been rewritten - result = _expandaliases(aliases, result, expanding) - expanding.pop() else: result = tuple(_expandaliases(aliases, t, expanding) for t in tree) return result def findaliases(ui, tree): + _checkaliasarg(tree) aliases = {} for k, v in ui.configitems('revsetalias'): alias = revsetalias(k, v) diff --git a/tests/test-revset.t b/tests/test-revset.t --- a/tests/test-revset.t +++ b/tests/test-revset.t @@ -527,6 +527,27 @@ test infinite recursion hg: parse error: infinite expansion of revset alias "recurse1" detected [255] + $ echo 'level1($1, $2) = $1 or $2' >> .hg/hgrc + $ echo 'level2($1, $2) = level1($2, $1)' >> .hg/hgrc + $ try "level2(level1(1, 2), 3)" + (func + ('symbol', 'level2') + (list + (func + ('symbol', 'level1') + (list + ('symbol', '1') + ('symbol', '2'))) + ('symbol', '3'))) + (or + ('symbol', '3') + (or + ('symbol', '1') + ('symbol', '2'))) + 3 + 1 + 2 + test nesting and variable passing $ echo 'nested($1) = nested2($1)' >> .hg/hgrc @@ -565,6 +586,19 @@ far away. abort: unknown revision '$1'! [255] + $ echo 'injectparamasstring2 = max(_aliasarg("$1"))' >> .hg/hgrc + $ echo 'callinjection2($1) = descendants(injectparamasstring2)' >> .hg/hgrc + $ try 'callinjection2(2:5)' + (func + ('symbol', 'callinjection2') + (range + ('symbol', '2') + ('symbol', '5'))) + hg: parse error: not a function: _aliasarg + [255] + >>> data = file('.hg/hgrc', 'rb').read() + >>> file('.hg/hgrc', 'wb').write(data.replace('_aliasarg', '')) + $ try 'd(2:5)' (func ('symbol', 'd')