From 8c51970f22f514aca04e8f95a37ddd971c7ab934 Mon Sep 17 00:00:00 2001 From: mohammed arib Date: Thu, 9 Jul 2026 11:26:47 +0530 Subject: [PATCH] restrict parser numeric fields to ASCII digits --- arrow/parser.py | 26 +++++++++++++++----------- tests/test_parser.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index fc3774b09..087dda6f6 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -155,24 +155,28 @@ class DateTimeParser: ) _ESCAPE_RE: ClassVar[Pattern[str]] = re.compile(r"\[[^\[\]]*\]") - _ONE_OR_TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,2}") - _ONE_OR_TWO_OR_THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,3}") - _ONE_OR_MORE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d+") - _TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{2}") - _THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{3}") - _FOUR_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{4}") - _TZ_Z_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") - _TZ_ZZ_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") + _ONE_OR_TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"[0-9]{1,2}") + _ONE_OR_TWO_OR_THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"[0-9]{1,3}") + _ONE_OR_MORE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"[0-9]+") + _TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"[0-9]{2}") + _THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"[0-9]{3}") + _FOUR_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"[0-9]{4}") + _TZ_Z_RE: ClassVar[Pattern[str]] = re.compile( + r"([\+\-])([0-9]{2})(?:([0-9]{2}))?|Z" + ) + _TZ_ZZ_RE: ClassVar[Pattern[str]] = re.compile( + r"([\+\-])([0-9]{2})(?:\:([0-9]{2}))?|Z" + ) _TZ_NAME_RE: ClassVar[Pattern[str]] = re.compile(r"\w[\w+\-/]+") # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) _TIMESTAMP_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+\.?\d+$") _TIMESTAMP_EXPANDED_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+$") _TIME_RE: ClassVar[Pattern[str]] = re.compile( - r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$" + r"^([0-9]{2})(?:\:?([0-9]{2}))?(?:\:?([0-9]{2}))?(?:([\.\,])([0-9]+))?$" ) _WEEK_DATE_RE: ClassVar[Pattern[str]] = re.compile( - r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?" + r"(?P[0-9]{4})[\-]?W(?P[0-9]{2})[\-]?(?P[0-9])?" ) _BASE_INPUT_RE_MAP: ClassVar[Dict[_FORMAT_TYPE, Pattern[str]]] = { @@ -888,7 +892,7 @@ class TzinfoParser: """ _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( - r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?" + r"^(?:\(UTC)*([\+\-])?([0-9]{2})(?:\:?([0-9]{2}))?" ) @classmethod diff --git a/tests/test_parser.py b/tests/test_parser.py index 7038d880f..6d4fb76cd 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -202,6 +202,20 @@ def test_parse_numbers(self): == self.expected ) + def test_parse_non_ascii_digits_rejected(self): + # \d matches the whole Unicode decimal category, so non-ASCII numerals + # (fullwidth, Arabic-Indic, ...) used to slip through the numeric tokens. + with pytest.raises(ParserMatchError): + self.parser.parse("2021-01-02", "YYYY-MM-DD") + + with pytest.raises(ParserMatchError): + self.parser.parse("٢٠٢١-٠١-٠٢", "YYYY-MM-DD") + + with pytest.raises(ParserMatchError): + self.parser.parse( + "2021-01-02T12:30:45", "YYYY-MM-DDTHH:mm:ss" + ) + def test_parse_am(self): with pytest.raises(ParserMatchError): self.parser.parse("2021-01-30 14:00:00 AM", "YYYY-MM-DD HH:mm:ss A")