Skip to content

Commit 540d850

Browse files
committed
Fix quadratic complexity parsing long backtick code spans with no matching closers
1 parent e1cfa8d commit 540d850

File tree

2 files changed

+68
-5
lines changed

2 files changed

+68
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
110110
- Fixed quadratic complexity parsing emphasis and strikethrough delimiters
111111
- Fixed issue where having 500,000+ delimiters could trigger a [known segmentation fault issue in PHP's garbage collection](https://bugs.php.net/bug.php?id=68606)
112112
- Fixed quadratic complexity deactivating link openers
113+
- Fixed quadratic complexity parsing long backtick code spans with no matching closers
113114
- Fixed catastrophic backtracking when parsing link labels/titles
114115

115116
## [2.4.1] - 2023-08-30

src/Extension/CommonMark/Parser/Inline/BacktickParser.php

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,27 @@
1818

1919
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
2020
use League\CommonMark\Node\Inline\Text;
21+
use League\CommonMark\Parser\Cursor;
2122
use League\CommonMark\Parser\Inline\InlineParserInterface;
2223
use League\CommonMark\Parser\Inline\InlineParserMatch;
2324
use League\CommonMark\Parser\InlineParserContext;
2425

2526
final class BacktickParser implements InlineParserInterface
2627
{
28+
/**
29+
* Max bound for backtick code span delimiters.
30+
*
31+
* @see https://github.com/commonmark/cmark/commit/8ed5c9d
32+
*/
33+
private const MAX_BACKTICKS = 1000;
34+
35+
/** @var \WeakReference<Cursor>|null */
36+
private ?\WeakReference $lastCursor = null;
37+
private bool $lastCursorScanned = false;
38+
39+
/** @var array<int, int> backtick count => position of known ender */
40+
private array $seenBackticks = [];
41+
2742
public function getMatchDefinition(): InlineParserMatch
2843
{
2944
return InlineParserMatch::regex('`+');
@@ -38,11 +53,7 @@ public function parse(InlineParserContext $inlineContext): bool
3853
$currentPosition = $cursor->getPosition();
3954
$previousState = $cursor->saveState();
4055

41-
while ($matchingTicks = $cursor->match('/`+/m')) {
42-
if ($matchingTicks !== $ticks) {
43-
continue;
44-
}
45-
56+
if ($this->findMatchingTicks(\strlen($ticks), $cursor)) {
4657
$code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
4758

4859
$c = \preg_replace('/\n/m', ' ', $code) ?? '';
@@ -67,4 +78,55 @@ public function parse(InlineParserContext $inlineContext): bool
6778

6879
return true;
6980
}
81+
82+
/**
83+
* Locates the matching closer for a backtick code span.
84+
*
85+
* Leverages some caching to avoid traversing the same cursor multiple times when
86+
* we've already seen all the potential backtick closers.
87+
*
88+
* @see https://github.com/commonmark/cmark/commit/8ed5c9d
89+
*
90+
* @param int $openTickLength Number of backticks in the opening sequence
91+
* @param Cursor $cursor Cursor to scan
92+
*
93+
* @return bool True if a matching closer was found, false otherwise
94+
*/
95+
private function findMatchingTicks(int $openTickLength, Cursor $cursor): bool
96+
{
97+
// Reset the seenBackticks cache if this is a new cursor
98+
if ($this->lastCursor === null || $this->lastCursor->get() !== $cursor) {
99+
$this->seenBackticks = [];
100+
$this->lastCursor = \WeakReference::create($cursor);
101+
$this->lastCursorScanned = false;
102+
}
103+
104+
if ($openTickLength > self::MAX_BACKTICKS) {
105+
return false;
106+
}
107+
108+
// Return if we already know there's no closer
109+
if ($this->lastCursorScanned && isset($this->seenBackticks[$openTickLength]) && $this->seenBackticks[$openTickLength] <= $cursor->getPosition()) {
110+
return false;
111+
}
112+
113+
while ($ticks = $cursor->match('/`{1,' . self::MAX_BACKTICKS . '}/m')) {
114+
$numTicks = \strlen($ticks);
115+
116+
// Did we find the closer?
117+
if ($numTicks === $openTickLength) {
118+
return true;
119+
}
120+
121+
// Store position of closer
122+
if ($numTicks <= self::MAX_BACKTICKS) {
123+
$this->seenBackticks[$numTicks] = $cursor->getPosition() - $numTicks;
124+
}
125+
}
126+
127+
// Got through whole input without finding closer
128+
$this->lastCursorScanned = true;
129+
130+
return false;
131+
}
70132
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy