Rev 6589: Stricter checks on configuration option names in http://bazaar.launchpad.net/~vila/bzr/integration/

Vincent Ladeuil v.ladeuil+lp at free.fr
Mon Oct 7 16:35:59 UTC 2013


At http://bazaar.launchpad.net/~vila/bzr/integration/

------------------------------------------------------------
revno: 6589 [merge]
revision-id: v.ladeuil+lp at free.fr-20131007163559-28eg2hyu6m2pbuug
parent: pqm at pqm.ubuntu.com-20131004093729-to7tbu9w6ws03wge
parent: v.ladeuil+lp at free.fr-20131004095623-xlan34vg0y51gdb5
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: trunk
timestamp: Mon 2013-10-07 18:35:59 +0200
message:
  Stricter checks on configuration option names
modified:
  bzrlib/config.py               config.py-20051011043216-070c74f4e9e338e8
  bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
  bzrlib/tests/test_config.py    testconfig.py-20051011041908-742d0c15d8d8c8eb
  doc/en/release-notes/bzr-2.7.txt bzr2.7.txt-20130727124539-wnx897hy9l2h9f7x-1
-------------- next part --------------
=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py	2013-08-09 15:09:23 +0000
+++ b/bzrlib/config.py	2013-10-04 09:56:23 +0000
@@ -2552,6 +2552,23 @@
         return "".join(ret)
 
 
+_option_ref_re = lazy_regex.lazy_compile('({[^\d\W](?:\.\w|\w)*})')
+"""Describes an expandable option reference.
+
+We want to match the most embedded reference first.
+
+I.e. for '{{foo}}' we will get '{foo}',
+for '{bar{baz}}' we will get '{baz}'
+"""
+
+def iter_option_refs(string):
+    # Split isolate refs so every other chunk is a ref
+    is_ref = False
+    for chunk  in _option_ref_re.split(string):
+        yield is_ref, chunk
+        is_ref = not is_ref
+
+
 class OptionRegistry(registry.Registry):
     """Register config options by their name.
 
@@ -2559,11 +2576,20 @@
     some information from the option object itself.
     """
 
+    def _check_option_name(self, option_name):
+        """Ensures an option name is valid.
+
+        :param option_name: The name to validate.
+        """
+        if _option_ref_re.match('{%s}' % option_name) is None:
+            raise errors.IllegalOptionName(option_name)
+
     def register(self, option):
         """Register a new option to its name.
 
         :param option: The option to register. Its name is used as the key.
         """
+        self._check_option_name(option.name)
         super(OptionRegistry, self).register(option.name, option,
                                              help=option.help)
 
@@ -2578,6 +2604,7 @@
         :param member_name: the member of the module to return.  If empty or 
                 None, get() will return the module itself.
         """
+        self._check_option_name(key)
         super(OptionRegistry, self).register_lazy(key,
                                                   module_name, member_name)
 
@@ -3622,22 +3649,6 @@
             yield self.store, section
 
 
-_option_ref_re = lazy_regex.lazy_compile('({[^{}\n]+})')
-"""Describes an expandable option reference.
-
-We want to match the most embedded reference first.
-
-I.e. for '{{foo}}' we will get '{foo}',
-for '{bar{baz}}' we will get '{baz}'
-"""
-
-def iter_option_refs(string):
-    # Split isolate refs so every other chunk is a ref
-    is_ref = False
-    for chunk  in _option_ref_re.split(string):
-        yield is_ref, chunk
-        is_ref = not is_ref
-
 # FIXME: _shared_stores should be an attribute of a library state once a
 # library_state object is always available.
 _shared_stores = {}

=== modified file 'bzrlib/errors.py'
--- a/bzrlib/errors.py	2013-05-23 10:04:17 +0000
+++ b/bzrlib/errors.py	2013-10-04 09:56:23 +0000
@@ -3250,13 +3250,21 @@
 
 class ExpandingUnknownOption(BzrError):
 
-    _fmt = 'Option %(name)s is not defined while expanding "%(string)s".'
+    _fmt = 'Option "%(name)s" is not defined while expanding "%(string)s".'
 
     def __init__(self, name, string):
         self.name = name
         self.string = string
 
 
+class IllegalOptionName(BzrError):
+
+    _fmt = 'Option "%(name)s" is not allowed.'
+
+    def __init__(self, name):
+        self.name = name
+
+
 class NoCompatibleInter(BzrError):
 
     _fmt = ('No compatible object available for operations from %(source)r '

=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py	2012-08-01 14:05:11 +0000
+++ b/bzrlib/tests/test_config.py	2013-10-04 09:56:23 +0000
@@ -16,7 +16,6 @@
 
 """Tests for finding and reading the bzr config file[s]."""
 
-import base64
 from cStringIO import StringIO
 from textwrap import dedent
 import os
@@ -629,7 +628,7 @@
 class TestIniConfigBuilding(TestIniConfig):
 
     def test_contructs(self):
-        my_config = config.IniBasedConfig()
+        config.IniBasedConfig()
 
     def test_from_fp(self):
         my_config = config.IniBasedConfig.from_string(sample_config_text)
@@ -678,8 +677,8 @@
 
     def test_saved_with_content(self):
         content = 'foo = bar\n'
-        conf = config.IniBasedConfig.from_string(
-            content, file_name='./test.conf', save=True)
+        config.IniBasedConfig.from_string(content, file_name='./test.conf',
+                                          save=True)
         self.assertFileEqual(content, 'test.conf')
 
 
@@ -1047,7 +1046,7 @@
 class TestGetConfig(tests.TestCase):
 
     def test_constructs(self):
-        my_config = config.GlobalConfig()
+        config.GlobalConfig()
 
     def test_calls_read_filenames(self):
         # replace the class that is constructed, to check its parameters
@@ -1065,9 +1064,12 @@
 
 class TestBranchConfig(tests.TestCaseWithTransport):
 
-    def test_constructs(self):
+    def test_constructs_valid(self):
         branch = FakeBranch()
         my_config = config.BranchConfig(branch)
+        self.assertIsNot(None, my_config)
+
+    def test_constructs_error(self):
         self.assertRaises(TypeError, config.BranchConfig)
 
     def test_get_location_config(self):
@@ -1105,6 +1107,7 @@
         conf = config.LocationConfig.from_string(
             '[%s]\nnickname = foobar' % (local_url,),
             local_url, save=True)
+        self.assertIsNot(None, conf)
         self.assertEqual('foobar', branch.nick)
 
     def test_config_local_path(self):
@@ -1113,9 +1116,10 @@
         self.assertEqual('branch', branch.nick)
 
         local_path = osutils.getcwd().encode('utf8')
-        conf = config.LocationConfig.from_string(
+        config.LocationConfig.from_string(
             '[%s/branch]\nnickname = barry' % (local_path,),
             'branch',  save=True)
+        # Now the branch will find its nick via the location config
         self.assertEqual('barry', branch.nick)
 
     def test_config_creates_local(self):
@@ -1369,8 +1373,10 @@
 
 class TestLocationConfig(tests.TestCaseInTempDir, TestOptionsMixin):
 
-    def test_constructs(self):
-        my_config = config.LocationConfig('http://example.com')
+    def test_constructs_valid(self):
+        config.LocationConfig('http://example.com')
+
+    def test_constructs_error(self):
         self.assertRaises(TypeError, config.LocationConfig)
 
     def test_branch_calls_read_filenames(self):
@@ -1662,10 +1668,9 @@
         if location_config is None:
             location_config = sample_branches_text
 
-        my_global_config = config.GlobalConfig.from_string(global_config,
-                                                           save=True)
-        my_location_config = config.LocationConfig.from_string(
-            location_config, my_branch.base, save=True)
+        config.GlobalConfig.from_string(global_config, save=True)
+        config.LocationConfig.from_string(location_config, my_branch.base,
+                                          save=True)
         my_config = config.BranchConfig(my_branch)
         self.my_config = my_config
         self.my_location_config = my_config._get_location_config()
@@ -1736,11 +1741,10 @@
                           location_config=None, branch_data_config=None):
         my_branch = FakeBranch(location)
         if global_config is not None:
-            my_global_config = config.GlobalConfig.from_string(global_config,
-                                                               save=True)
+            config.GlobalConfig.from_string(global_config, save=True)
         if location_config is not None:
-            my_location_config = config.LocationConfig.from_string(
-                location_config, my_branch.base, save=True)
+            config.LocationConfig.from_string(location_config, my_branch.base,
+                                              save=True)
         my_config = config.BranchConfig(my_branch)
         if branch_data_config is not None:
             my_config.branch.control_files.files['branch.conf'] = \
@@ -2220,6 +2224,44 @@
         self.assertSaveHook(remote_bzrdir._get_config())
 
 
+class TestOptionNames(tests.TestCase):
+
+    def is_valid(self, name):
+        return config._option_ref_re.match('{%s}' % name) is not None
+
+    def test_valid_names(self):
+        self.assertTrue(self.is_valid('foo'))
+        self.assertTrue(self.is_valid('foo.bar'))
+        self.assertTrue(self.is_valid('f1'))
+        self.assertTrue(self.is_valid('_'))
+        self.assertTrue(self.is_valid('__bar__'))
+        self.assertTrue(self.is_valid('a_'))
+        self.assertTrue(self.is_valid('a1'))
+
+    def test_invalid_names(self):
+        self.assertFalse(self.is_valid(' foo'))
+        self.assertFalse(self.is_valid('foo '))
+        self.assertFalse(self.is_valid('1'))
+        self.assertFalse(self.is_valid('1,2'))
+        self.assertFalse(self.is_valid('foo$'))
+        self.assertFalse(self.is_valid('!foo'))
+        self.assertFalse(self.is_valid('foo.'))
+        self.assertFalse(self.is_valid('foo..bar'))
+        self.assertFalse(self.is_valid('{}'))
+        self.assertFalse(self.is_valid('{a}'))
+        self.assertFalse(self.is_valid('a\n'))
+
+    def assertSingleGroup(self, reference):
+        # the regexp is used with split and as such should match the reference
+        # *only*, if more groups needs to be defined, (?:...) should be used.
+        m = config._option_ref_re.match('{a}')
+        self.assertLength(1, m.groups())
+
+    def test_valid_references(self):
+        self.assertSingleGroup('{a}')
+        self.assertSingleGroup('{{a}}')
+
+
 class TestOption(tests.TestCase):
 
     def test_default_value(self):
@@ -2447,6 +2489,12 @@
         self.registry.register(opt)
         self.assertEquals('A simple option', self.registry.get_help('foo'))
 
+    def test_dont_register_illegal_name(self):
+        self.assertRaises(errors.IllegalOptionName,
+                          self.registry.register, config.Option(' foo'))
+        self.assertRaises(errors.IllegalOptionName,
+                          self.registry.register, config.Option('bar,'))
+
     lazy_option = config.Option('lazy_foo', help='Lazy help')
 
     def test_register_lazy(self):
@@ -2459,6 +2507,19 @@
                                     'TestOptionRegistry.lazy_option')
         self.assertEquals('Lazy help', self.registry.get_help('lazy_foo'))
 
+    def test_dont_lazy_register_illegal_name(self):
+        # This is where the root cause of http://pad.lv/1235099 is better
+        # understood: 'register_lazy' doc string mentions that key should match
+        # the option name which indirectly requires that the option name is a
+        # valid python identifier. We violate that rule here (using a key that
+        # doesn't match the option name) to test the option name checking.
+        self.assertRaises(errors.IllegalOptionName,
+                          self.registry.register_lazy, ' foo', self.__module__,
+                          'TestOptionRegistry.lazy_option')
+        self.assertRaises(errors.IllegalOptionName,
+                          self.registry.register_lazy, '1,2', self.__module__,
+                          'TestOptionRegistry.lazy_option')
+
 
 class TestRegisteredOptions(tests.TestCase):
     """All registered options should verify some constraints."""
@@ -3320,7 +3381,7 @@
 
     def test_build_doesnt_load_store(self):
         store = self.get_store(self)
-        matcher = self.matcher(store, '/bar')
+        self.matcher(store, '/bar')
         self.assertFalse(store.is_loaded())
 
 
@@ -3461,16 +3522,16 @@
 
     def test_url_vs_local_paths(self):
         # The matcher location is an url and the section names are local paths
-        sections = self.assertSectionIDs(['/foo/bar', '/foo'],
-                                         'file:///foo/bar/baz', '''\
+        self.assertSectionIDs(['/foo/bar', '/foo'],
+                              'file:///foo/bar/baz', '''\
 [/foo]
 [/foo/bar]
 ''')
 
     def test_local_path_vs_url(self):
         # The matcher location is a local path and the section names are urls
-        sections = self.assertSectionIDs(['file:///foo/bar', 'file:///foo'],
-                                         '/foo/bar/baz', '''\
+        self.assertSectionIDs(['file:///foo/bar', 'file:///foo'],
+                              '/foo/bar/baz', '''\
 [file:///foo]
 [file:///foo/bar]
 ''')
@@ -3693,7 +3754,7 @@
 
     def test_build_stack(self):
         # Just a smoke test to help debug builders
-        stack = self.get_stack(self)
+        self.get_stack(self)
 
 
 class TestStackGet(TestStackWithTransport):
@@ -3920,6 +3981,11 @@
         self.assertRaises(errors.ExpandingUnknownOption,
                           self.conf.expand_options, '{foo}')
 
+    def test_illegal_def_is_ignored(self):
+        self.assertExpansion('{1,2}', '{1,2}')
+        self.assertExpansion('{ }', '{ }')
+        self.assertExpansion('${Foo,f}', '${Foo,f}')
+
     def test_indirect_ref(self):
         self.conf.store._load_from_string('''
 foo=xxx
@@ -4297,7 +4363,7 @@
         """
         sections = list(conf._get_sections(name))
         self.assertLength(len(expected), sections)
-        self.assertEqual(expected, [name for name, _, _ in sections])
+        self.assertEqual(expected, [n for n, _, _ in sections])
 
     def test_bazaar_default_section(self):
         self.assertSectionNames(['DEFAULT'], self.bazaar_config)

=== modified file 'doc/en/release-notes/bzr-2.7.txt'
--- a/doc/en/release-notes/bzr-2.7.txt	2013-10-01 13:13:46 +0000
+++ b/doc/en/release-notes/bzr-2.7.txt	2013-10-07 16:35:59 +0000
@@ -32,6 +32,10 @@
 .. Fixes for situations where bzr would previously crash or give incorrect
    or undesirable results.
 
+* Option names are now checked to be valid [dotted] python identifiers. Also
+  ignore invalid references (i.e. using invalid option names) while
+  expanding option values. (Vincent Ladeuil, #1235099)
+
 Documentation
 *************
 



More information about the bazaar-commits mailing list