diff --git a/git/config.py b/git/config.py index 9bb82273c..a3437d525 100644 --- a/git/config.py +++ b/git/config.py @@ -334,7 +334,11 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): OPTVALUEONLY = re.compile(optvalueonly_source) - OPTCRE = re.compile(optvalueonly_source + r"\s*(?P[:=])\s*" + r"(?P.*)$") + # The option name class [^:=#;]* already consumes any spaces up to the ":" or "=", + # so a second \s* before the indicator would overlap it and backtrack quadratically + # on a line that never reaches an indicator (for example a key followed by a long + # whitespace run). Drop the redundant \s*; the name is right-stripped after parsing. + OPTCRE = re.compile(optvalueonly_source + r"(?P[:=])\s*" + r"(?P.*)$") del optvalueonly_source diff --git a/test/test_config.py b/test/test_config.py index b0f7f72eb..274738075 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -9,6 +9,7 @@ import os.path as osp import subprocess import sys +import time from unittest import mock import pytest @@ -262,6 +263,25 @@ def test_inline_comments_are_stripped_like_git(self): with self.subTest(content=content): self.assertEqual(config.get_value("a", "k"), expected) + def test_option_line_with_long_whitespace_run_is_not_quadratic(self): + """A key followed by a long whitespace run and no indicator must not make + the option regex backtrack quadratically. + + `.gitmodules` and other config files are fully controlled by any repository + that is inspected, so a crafted line must stay cheap to parse. Keys that come + before the malformed line are still read. + """ + malformed = b'[submodule "x"]\n\tpath = x\n\tbranch' + b" " * 200_000 + b"\n" + config_file = io.BytesIO(malformed) + config_file.name = ".gitmodules" + config = GitConfigParser(config_file) + start = time.process_time() + config.read() + elapsed = time.process_time() - start + # Leave ample CPU time for slow runners, but catch quadratic backtracking. + self.assertLess(elapsed, 1.0) + self.assertEqual(config.get_value('submodule "x"', "path"), "x") + @with_rw_directory def test_inline_comments_preserve_balanced_quotes_and_following_settings(self, rw_dir): config_path = osp.join(rw_dir, "config")