Merge lp:~vila/bzr/1235099-illegal-option-names into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 6589
Proposed branch: lp:~vila/bzr/1235099-illegal-option-names
Merge into: lp:bzr
Prerequisite: lp:~vila/bzr/1235099-prereq
Diff against target: 214 lines (+102/-17)
4 files modified
bzrlib/config.py (+27/-16)
bzrlib/errors.py (+9/-1)
bzrlib/tests/test_config.py (+62/-0)
doc/en/release-notes/bzr-2.7.txt (+4/-0)
To merge this branch: bzr merge lp:~vila/bzr/1235099-illegal-option-names
Reviewer Review Type Date Requested Status
John A Meinel Approve
Review via email: mp+189262@code.launchpad.net

Commit message

Stricter checks on configuration option names.

Description of the change

There are several use case where config values expansion fails in unhelpful
ways:

- {1,2} as reported in http://pad.lv/1235099

- When templates are used to produce bash scripts (though that case could be
  better addressed by allowing some escape mechanism, I'd rather fix that as
  a separate bug).

Unfortunately for readability, I had to move the regexp definition so here
are the old and new ones for clarity:

-_option_ref_re = lazy_regex.lazy_compile('({[^{}\n]+})')
+_option_ref_re = lazy_regex.lazy_compile('({[^\d\W](?:\.\w|\w)*})')

I.e. valid python identifier (including dotted ones).

As the bug demonstrate we were allowing some illegal option names in the option values and were giving bad error reporting.

This proposal doesn't change which option names are legal (only invalid references could be used before and led to failures) but give better error messages.

I've made a few cleanup changes in lp:~vila/bzr/1235099-prereq which don't seem worth presenting here.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

Seems like a good change to me.

review: Approve
Revision history for this message
getriebesand (getriebesand) wrote :

Hi, after that change bzr-svn raise an exception:

Option "guessed-layout" is not allowed.
bzr: ERROR: exceptions.TypeError: 'NoneType' object is not callable

Your regex doesnt support - in option names.

When I modify the regex to:
_option_ref_re = lazy_regex.lazy_compile('({[^\d\W](?:\.\w|\w|-\w)*})')

it seems to fix that. But I am no regex or python expert. Can you have a look please?

Revision history for this message
dreamcat4 (dreamcat4) wrote :

Me too.
With bzr-svn v1.2.3, from here: https://www.samba.org/~jelmer/bzr/

id@emachines-e520:~/.bazaar/plugins/svn$ bzr svn
Option "guessed-layout" is not allowed.
bzr: ERROR: unknown command "svn"

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/config.py'
--- bzrlib/config.py 2013-08-09 15:09:23 +0000
+++ bzrlib/config.py 2013-10-04 10:33:57 +0000
@@ -2552,6 +2552,23 @@
2552 return "".join(ret)2552 return "".join(ret)
25532553
25542554
2555_option_ref_re = lazy_regex.lazy_compile('({[^\d\W](?:\.\w|\w)*})')
2556"""Describes an expandable option reference.
2557
2558We want to match the most embedded reference first.
2559
2560I.e. for '{{foo}}' we will get '{foo}',
2561for '{bar{baz}}' we will get '{baz}'
2562"""
2563
2564def iter_option_refs(string):
2565 # Split isolate refs so every other chunk is a ref
2566 is_ref = False
2567 for chunk in _option_ref_re.split(string):
2568 yield is_ref, chunk
2569 is_ref = not is_ref
2570
2571
2555class OptionRegistry(registry.Registry):2572class OptionRegistry(registry.Registry):
2556 """Register config options by their name.2573 """Register config options by their name.
25572574
@@ -2559,11 +2576,20 @@
2559 some information from the option object itself.2576 some information from the option object itself.
2560 """2577 """
25612578
2579 def _check_option_name(self, option_name):
2580 """Ensures an option name is valid.
2581
2582 :param option_name: The name to validate.
2583 """
2584 if _option_ref_re.match('{%s}' % option_name) is None:
2585 raise errors.IllegalOptionName(option_name)
2586
2562 def register(self, option):2587 def register(self, option):
2563 """Register a new option to its name.2588 """Register a new option to its name.
25642589
2565 :param option: The option to register. Its name is used as the key.2590 :param option: The option to register. Its name is used as the key.
2566 """2591 """
2592 self._check_option_name(option.name)
2567 super(OptionRegistry, self).register(option.name, option,2593 super(OptionRegistry, self).register(option.name, option,
2568 help=option.help)2594 help=option.help)
25692595
@@ -2578,6 +2604,7 @@
2578 :param member_name: the member of the module to return. If empty or 2604 :param member_name: the member of the module to return. If empty or
2579 None, get() will return the module itself.2605 None, get() will return the module itself.
2580 """2606 """
2607 self._check_option_name(key)
2581 super(OptionRegistry, self).register_lazy(key,2608 super(OptionRegistry, self).register_lazy(key,
2582 module_name, member_name)2609 module_name, member_name)
25832610
@@ -3622,22 +3649,6 @@
3622 yield self.store, section3649 yield self.store, section
36233650
36243651
3625_option_ref_re = lazy_regex.lazy_compile('({[^{}\n]+})')
3626"""Describes an expandable option reference.
3627
3628We want to match the most embedded reference first.
3629
3630I.e. for '{{foo}}' we will get '{foo}',
3631for '{bar{baz}}' we will get '{baz}'
3632"""
3633
3634def iter_option_refs(string):
3635 # Split isolate refs so every other chunk is a ref
3636 is_ref = False
3637 for chunk in _option_ref_re.split(string):
3638 yield is_ref, chunk
3639 is_ref = not is_ref
3640
3641# FIXME: _shared_stores should be an attribute of a library state once a3652# FIXME: _shared_stores should be an attribute of a library state once a
3642# library_state object is always available.3653# library_state object is always available.
3643_shared_stores = {}3654_shared_stores = {}
36443655
=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py 2013-05-23 10:04:17 +0000
+++ bzrlib/errors.py 2013-10-04 10:33:57 +0000
@@ -3250,13 +3250,21 @@
32503250
3251class ExpandingUnknownOption(BzrError):3251class ExpandingUnknownOption(BzrError):
32523252
3253 _fmt = 'Option %(name)s is not defined while expanding "%(string)s".'3253 _fmt = 'Option "%(name)s" is not defined while expanding "%(string)s".'
32543254
3255 def __init__(self, name, string):3255 def __init__(self, name, string):
3256 self.name = name3256 self.name = name
3257 self.string = string3257 self.string = string
32583258
32593259
3260class IllegalOptionName(BzrError):
3261
3262 _fmt = 'Option "%(name)s" is not allowed.'
3263
3264 def __init__(self, name):
3265 self.name = name
3266
3267
3260class NoCompatibleInter(BzrError):3268class NoCompatibleInter(BzrError):
32613269
3262 _fmt = ('No compatible object available for operations from %(source)r '3270 _fmt = ('No compatible object available for operations from %(source)r '
32633271
=== modified file 'bzrlib/tests/test_config.py'
--- bzrlib/tests/test_config.py 2013-10-04 10:33:57 +0000
+++ bzrlib/tests/test_config.py 2013-10-04 10:33:57 +0000
@@ -2224,6 +2224,44 @@
2224 self.assertSaveHook(remote_bzrdir._get_config())2224 self.assertSaveHook(remote_bzrdir._get_config())
22252225
22262226
2227class TestOptionNames(tests.TestCase):
2228
2229 def is_valid(self, name):
2230 return config._option_ref_re.match('{%s}' % name) is not None
2231
2232 def test_valid_names(self):
2233 self.assertTrue(self.is_valid('foo'))
2234 self.assertTrue(self.is_valid('foo.bar'))
2235 self.assertTrue(self.is_valid('f1'))
2236 self.assertTrue(self.is_valid('_'))
2237 self.assertTrue(self.is_valid('__bar__'))
2238 self.assertTrue(self.is_valid('a_'))
2239 self.assertTrue(self.is_valid('a1'))
2240
2241 def test_invalid_names(self):
2242 self.assertFalse(self.is_valid(' foo'))
2243 self.assertFalse(self.is_valid('foo '))
2244 self.assertFalse(self.is_valid('1'))
2245 self.assertFalse(self.is_valid('1,2'))
2246 self.assertFalse(self.is_valid('foo$'))
2247 self.assertFalse(self.is_valid('!foo'))
2248 self.assertFalse(self.is_valid('foo.'))
2249 self.assertFalse(self.is_valid('foo..bar'))
2250 self.assertFalse(self.is_valid('{}'))
2251 self.assertFalse(self.is_valid('{a}'))
2252 self.assertFalse(self.is_valid('a\n'))
2253
2254 def assertSingleGroup(self, reference):
2255 # the regexp is used with split and as such should match the reference
2256 # *only*, if more groups needs to be defined, (?:...) should be used.
2257 m = config._option_ref_re.match('{a}')
2258 self.assertLength(1, m.groups())
2259
2260 def test_valid_references(self):
2261 self.assertSingleGroup('{a}')
2262 self.assertSingleGroup('{{a}}')
2263
2264
2227class TestOption(tests.TestCase):2265class TestOption(tests.TestCase):
22282266
2229 def test_default_value(self):2267 def test_default_value(self):
@@ -2451,6 +2489,12 @@
2451 self.registry.register(opt)2489 self.registry.register(opt)
2452 self.assertEquals('A simple option', self.registry.get_help('foo'))2490 self.assertEquals('A simple option', self.registry.get_help('foo'))
24532491
2492 def test_dont_register_illegal_name(self):
2493 self.assertRaises(errors.IllegalOptionName,
2494 self.registry.register, config.Option(' foo'))
2495 self.assertRaises(errors.IllegalOptionName,
2496 self.registry.register, config.Option('bar,'))
2497
2454 lazy_option = config.Option('lazy_foo', help='Lazy help')2498 lazy_option = config.Option('lazy_foo', help='Lazy help')
24552499
2456 def test_register_lazy(self):2500 def test_register_lazy(self):
@@ -2463,6 +2507,19 @@
2463 'TestOptionRegistry.lazy_option')2507 'TestOptionRegistry.lazy_option')
2464 self.assertEquals('Lazy help', self.registry.get_help('lazy_foo'))2508 self.assertEquals('Lazy help', self.registry.get_help('lazy_foo'))
24652509
2510 def test_dont_lazy_register_illegal_name(self):
2511 # This is where the root cause of http://pad.lv/1235099 is better
2512 # understood: 'register_lazy' doc string mentions that key should match
2513 # the option name which indirectly requires that the option name is a
2514 # valid python identifier. We violate that rule here (using a key that
2515 # doesn't match the option name) to test the option name checking.
2516 self.assertRaises(errors.IllegalOptionName,
2517 self.registry.register_lazy, ' foo', self.__module__,
2518 'TestOptionRegistry.lazy_option')
2519 self.assertRaises(errors.IllegalOptionName,
2520 self.registry.register_lazy, '1,2', self.__module__,
2521 'TestOptionRegistry.lazy_option')
2522
24662523
2467class TestRegisteredOptions(tests.TestCase):2524class TestRegisteredOptions(tests.TestCase):
2468 """All registered options should verify some constraints."""2525 """All registered options should verify some constraints."""
@@ -3924,6 +3981,11 @@
3924 self.assertRaises(errors.ExpandingUnknownOption,3981 self.assertRaises(errors.ExpandingUnknownOption,
3925 self.conf.expand_options, '{foo}')3982 self.conf.expand_options, '{foo}')
39263983
3984 def test_illegal_def_is_ignored(self):
3985 self.assertExpansion('{1,2}', '{1,2}')
3986 self.assertExpansion('{ }', '{ }')
3987 self.assertExpansion('${Foo,f}', '${Foo,f}')
3988
3927 def test_indirect_ref(self):3989 def test_indirect_ref(self):
3928 self.conf.store._load_from_string('''3990 self.conf.store._load_from_string('''
3929foo=xxx3991foo=xxx
39303992
=== modified file 'doc/en/release-notes/bzr-2.7.txt'
--- doc/en/release-notes/bzr-2.7.txt 2013-10-01 13:13:46 +0000
+++ doc/en/release-notes/bzr-2.7.txt 2013-10-04 10:33:57 +0000
@@ -32,6 +32,10 @@
32.. Fixes for situations where bzr would previously crash or give incorrect32.. Fixes for situations where bzr would previously crash or give incorrect
33 or undesirable results.33 or undesirable results.
3434
35* Option names are now checked to be valid [dotted] python identifiers. Also
36 ignore invalid references (i.e. using invalid option names) while
37 expanding option values. (Vincent Ladeuil, #1235099)
38
35Documentation39Documentation
36*************40*************
3741