Skip to content

Commit 2b04d7b

Browse files
committed
fix(block): avoid quadratic ref link scans
1 parent 25f2503 commit 2b04d7b

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

src/mistune/block_parser.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from bisect import bisect_left
12
import re
23
from typing import Optional, List, Tuple, Match, Pattern, Set
34
import string
@@ -256,11 +257,11 @@ def parse_ref_link(self, m: Match[str], state: BlockState) -> Optional[int]:
256257

257258
assert href_pos is not None
258259

259-
_blank = self.BLANK_LINE.search(state.src, href_pos)
260-
if _blank:
261-
max_pos = _blank.start()
262-
else:
260+
blank_pos = _find_next_blank_line(state, href_pos, self.BLANK_LINE)
261+
if blank_pos is None:
263262
max_pos = state.cursor_max
263+
else:
264+
max_pos = blank_pos
264265

265266
title, title_pos = parse_link_title(state.src, href_pos, max_pos)
266267
if title_pos:
@@ -537,6 +538,19 @@ def _parse_block_quote_line(line: str) -> Optional[str]:
537538
return _BLOCK_QUOTE_TRIM.sub("", text)
538539

539540

541+
def _find_next_blank_line(state: BlockState, pos: int, pattern: Pattern[str]) -> Optional[int]:
542+
cache = state.env.get("__blank_line_starts__")
543+
if cache is None or cache[0] is not state.src:
544+
cache = (state.src, [m.start() for m in pattern.finditer(state.src)])
545+
state.env["__blank_line_starts__"] = cache
546+
547+
positions = cache[1]
548+
index = bisect_left(positions, pos)
549+
if index < len(positions):
550+
return positions[index]
551+
return None
552+
553+
540554
def _trim_partial_next_line_indent(text: str, end_pos: int) -> int:
541555
line_start = text.rfind("\n") + 1
542556
if line_start == 0:

tests/test_security_ref_links.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import time
2+
from unittest import TestCase
3+
4+
from mistune import create_markdown
5+
6+
7+
class TestRefLinkSecurity(TestCase):
8+
def test_many_reference_links_return_quickly(self):
9+
text = "".join("[a{}]: u\n".format(i) for i in range(3000))
10+
11+
start = time.monotonic()
12+
create_markdown()(text)
13+
elapsed = time.monotonic() - start
14+
15+
self.assertLess(elapsed, 0.5)

0 commit comments

Comments
 (0)