Skip to content

Commit 25a1a69

Browse files
authored
Merge pull request #2259 from radhika1314/config-option-line-bound
fix(config): bound option-line parsing
2 parents a3e1ea4 + 2235723 commit 25a1a69

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

‎git/config.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,11 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
334334

335335
OPTVALUEONLY = re.compile(optvalueonly_source)
336336

337-
OPTCRE = re.compile(optvalueonly_source + r"\s*(?P<vi>[:=])\s*" + r"(?P<value>.*)$")
337+
# The option name class [^:=#;]* already consumes any spaces up to the ":" or "=",
338+
# so a second \s* before the indicator would overlap it and backtrack quadratically
339+
# on a line that never reaches an indicator (for example a key followed by a long
340+
# whitespace run). Drop the redundant \s*; the name is right-stripped after parsing.
341+
OPTCRE = re.compile(optvalueonly_source + r"(?P<vi>[:=])\s*" + r"(?P<value>.*)$")
338342

339343
del optvalueonly_source
340344

‎test/test_config.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os.path as osp
1010
import subprocess
1111
import sys
12+
import time
1213
from unittest import mock
1314

1415
import pytest
@@ -262,6 +263,25 @@ def test_inline_comments_are_stripped_like_git(self):
262263
with self.subTest(content=content):
263264
self.assertEqual(config.get_value("a", "k"), expected)
264265

266+
def test_option_line_with_long_whitespace_run_is_not_quadratic(self):
267+
"""A key followed by a long whitespace run and no indicator must not make
268+
the option regex backtrack quadratically.
269+
270+
`.gitmodules` and other config files are fully controlled by any repository
271+
that is inspected, so a crafted line must stay cheap to parse. Keys that come
272+
before the malformed line are still read.
273+
"""
274+
malformed = b'[submodule "x"]\n\tpath = x\n\tbranch' + b" " * 200_000 + b"\n"
275+
config_file = io.BytesIO(malformed)
276+
config_file.name = ".gitmodules"
277+
config = GitConfigParser(config_file)
278+
start = time.process_time()
279+
config.read()
280+
elapsed = time.process_time() - start
281+
# Leave ample CPU time for slow runners, but catch quadratic backtracking.
282+
self.assertLess(elapsed, 1.0)
283+
self.assertEqual(config.get_value('submodule "x"', "path"), "x")
284+
265285
@with_rw_directory
266286
def test_inline_comments_preserve_balanced_quotes_and_following_settings(self, rw_dir):
267287
config_path = osp.join(rw_dir, "config")

0 commit comments

Comments
 (0)