Skip to content

Commit 06b9e17

Browse files
codexByron
authored andcommitted
fix: parse actor identities without regular expressions
GHSA-g5vv-9gxw-82hx reports quadratic backtracking when an actor identity contains a long unterminated email delimiter. Add a regression that exercises a 20,000-character malformed identity, then replace both actor regexes with direct delimiter scans following Git's first-opening, first-closing delimiter behavior. Keep GitPython's whole-string fallback when either delimiter is absent. Reference Git baseline cf5497b14c5a24f10c13f7e0ee85cb95 ident.c::split_ident_line and its invalid-committer cases in t/t9300-fast-import.sh. Also reference gix-actor's signature decoder and lenient identity tests. Validation: - test/test_actor.py: 7 passed - TestUtils::test_actor_from_string passes - ruff check and format pass
1 parent 849ca34 commit 06b9e17

2 files changed

Lines changed: 28 additions & 16 deletions

File tree

git/util.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -858,10 +858,6 @@ class Actor:
858858
committers and authors or anything with a name and an email as mentioned in the git
859859
log entries."""
860860

861-
# PRECOMPILED REGEX
862-
name_only_regex = re.compile(r"<(.*)>")
863-
name_email_regex = re.compile(r"(.*) <(.*?)>")
864-
865861
# ENVIRONMENT VARIABLES
866862
# These are read when creating new commits.
867863
env_author_name = "GIT_AUTHOR_NAME"
@@ -906,18 +902,14 @@ def _from_string(cls, string: str) -> "Actor":
906902
:return:
907903
:class:`Actor`
908904
"""
909-
m = cls.name_email_regex.search(string)
910-
if m:
911-
name, email = m.groups()
912-
return Actor(name, email)
913-
else:
914-
m = cls.name_only_regex.search(string)
915-
if m:
916-
return Actor(m.group(1), None)
917-
# Assume the best and use the whole string as name.
918-
return Actor(string, None)
919-
# END special case name
920-
# END handle name/email matching
905+
line = string.partition("\n")[0]
906+
left_bracket = line.find("<")
907+
right_bracket = line.find(">", left_bracket + 1)
908+
if left_bracket >= 0 and right_bracket >= 0:
909+
return Actor(line[:left_bracket].rstrip(), line[left_bracket + 1 : right_bracket])
910+
911+
# Assume the best and use the whole string as name.
912+
return Actor(string, None)
921913

922914
@classmethod
923915
def _main_actor(

test/test_actor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ def test_from_string_should_handle_just_name(self):
2727
self.assertEqual("Michael Trier", a.name)
2828
self.assertEqual(None, a.email)
2929

30+
def test_from_string_handles_unterminated_email_without_regex_backtracking(self):
31+
value = "A" * 20_000 + " <unterminated"
32+
actor = Actor._from_string(value)
33+
self.assertNotIn("name_email_regex", vars(Actor))
34+
self.assertEqual(actor, Actor(value, None))
35+
36+
def test_from_string_does_not_parse_across_lines(self):
37+
self.assertEqual(Actor._from_string("x <a>\n y <b>"), Actor("x", "a"))
38+
39+
def test_from_string_uses_git_delimiters(self):
40+
for value, expected in (
41+
("Name <e<mail>", Actor("Name", "e<mail")),
42+
("Name <email>>", Actor("Name", "email")),
43+
("Name<email>", Actor("Name", "email")),
44+
(" <>", Actor("", "")),
45+
("Name <email", Actor("Name <email", None)),
46+
("Name email>", Actor("Name email>", None)),
47+
):
48+
self.assertEqual(Actor._from_string(value), expected)
49+
3050
def test_should_display_representation(self):
3151
a = Actor._from_string("Michael Trier <mtrier@example.com>")
3252
self.assertEqual('<git.Actor "Michael Trier <mtrier@example.com>">', repr(a))

0 commit comments

Comments
 (0)