From 1d566e84d7c39805ff1acb58526f3830b9336ffe Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Fri, 17 Apr 2026 17:08:40 +0800 Subject: [PATCH] fix: prevent infinite loop caused by unnormalized line breaks Signed-off-by: Frost Ming --- CHANGELOG.md | 1 + marko/source.py | 6 ++++-- tests/test_basic.py | 7 +++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc9ba3..8b58203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Fix a potential security issue in the URL escaping logic of the Markdown renderer. - Fix GFM table indentation in list. +- Fix an infinite loop caused by unnormalized line breaks. ## v2.2.2(2026-01-05) diff --git a/marko/source.py b/marko/source.py index 7c9c355..5a539f6 100644 --- a/marko/source.py +++ b/marko/source.py @@ -15,7 +15,8 @@ def _preprocess_text(text: str) -> str: - return text.replace("\r\n", "\n") + # Normalize line terminators so block parsers can always advance on line reads. + return text.replace("\r\n", "\n").replace("\r", "\n").replace("\f", "\n") class Source: @@ -102,7 +103,8 @@ def expect_re(self, regexp: Pattern[str] | str) -> Match[str] | None: :returns: the match object. """ prefix_len = self.match_prefix( - self.prefix, self.next_line(require_prefix=False) # type: ignore + self.prefix, + self.next_line(require_prefix=False), # type: ignore ) if prefix_len >= 0: match = self._expect_re(regexp, self.pos + prefix_len) diff --git a/tests/test_basic.py b/tests/test_basic.py index ab5d655..8215d90 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,17 +2,21 @@ import re import textwrap -from marko.source import Source import pytest import marko from marko import block from marko.ast_renderer import ASTRenderer, XMLRenderer from marko.md_renderer import MarkdownRenderer +from marko.source import Source from tests.normalize import normalize_html class TestBasic: + @pytest.mark.parametrize("text", ["-\r-", "-\f-"]) + def test_non_lf_line_terminators_no_infinite_loop(self, text): + assert marko.convert(text) == marko.convert("-\n-") + def test_xml_renderer(self): text = "[Overview](#overview)\n\n* * *" markdown = marko.Markdown(renderer=XMLRenderer) @@ -169,7 +173,6 @@ class CustomElement(block.BlockElement): def __init__(self, match: re.Match[str]) -> None: self.inline_body = match.group(1).strip() - @classmethod def match(cls, source: Source) -> "re.Match[str] | None": return source.expect_re(cls.pattern)