Skip to content

Commit 81ef4cb

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. This keeps the existing well-formed, multiline, and fallback results while making the work linear in input length. 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: 6 passed - TestUtils::test_actor_from_string passes - ruff check and format pass
1 parent 72a2b06 commit 81ef4cb

2 files changed

Lines changed: 25 additions & 16 deletions

File tree

git/util.py

Lines changed: 16 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,22 @@ 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+
lines = string.split("\n")
906+
for line in lines:
907+
right_bracket = line.rfind(">")
908+
left_bracket = line.rfind(" <", 0, right_bracket) if right_bracket >= 0 else -1
909+
if left_bracket >= 0:
910+
email_end = line.find(">", left_bracket + 2)
911+
return Actor(line[:left_bracket], line[left_bracket + 2 : email_end])
912+
913+
for line in lines:
914+
left_bracket = line.find("<")
915+
right_bracket = line.rfind(">")
916+
if 0 <= left_bracket < right_bracket:
917+
return Actor(line[left_bracket + 1 : right_bracket], None)
918+
919+
# Assume the best and use the whole string as name.
920+
return Actor(string, None)
921921

922922
@classmethod
923923
def _main_actor(

test/test_actor.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ 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+
3039
def test_should_display_representation(self):
3140
a = Actor._from_string("Michael Trier <mtrier@example.com>")
3241
self.assertEqual('<git.Actor "Michael Trier <mtrier@example.com>">', repr(a))

0 commit comments

Comments
 (0)