From e06b245e31ad4892283f4146767cda965dbcbf5c Mon Sep 17 00:00:00 2001 From: Yushion Yang <2685310462@qq.com> Date: Fri, 23 Oct 2026 17:46:17 +0800 Subject: [PATCH] Validate star arguments in Python format strings --- babel/messages/checkers.py | 3 ++ tests/messages/test_checkers.py | 53 ++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 4026ab1b3..50db2341d 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -98,6 +98,9 @@ def _parse(string: str) -> list[tuple[str, str]]: name, format, typechar = match.groups() if typechar == '%' and name is None: continue + if name is None: + # A star width or precision consumes an integer argument first. + result.extend((None, 'd') for _ in range(format.count('*'))) result.append((name, str(typechar))) return result diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index 9b0b4b779..5b26a4988 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -19,7 +19,7 @@ from babel.core import Locale, UnknownLocaleError from babel.dates import format_datetime from babel.messages import Message, checkers -from babel.messages.catalog import TranslationError +from babel.messages.catalog import Catalog, TranslationError from babel.messages.checkers import _validate_format, python_format from babel.messages.plurals import PLURALS from babel.messages.pofile import read_po @@ -370,6 +370,57 @@ def test_python_format_valid(msgid, msgstr): python_format(None, msg) +@pytest.mark.parametrize( + ('msgid', 'msgstr', 'arguments'), + [ + ('%*s', '%s', (4, 'word')), + ('%s', '%*s', ('word',)), + ('%.*f', '%.2f', (2, 1.234)), + ('%.2f', '%.*f', (1.234,)), + ('%*.*f', '%.*f', (8, 2, 1.234)), + ('%.*f', '%*.*f', (2, 1.234)), + ('%*.*f', '%*f', (8, 2, 1.234)), + ('%*f', '%*.*f', (8, 1.234)), + ('%s %*s', '%*s %s', ('label', 5, 'value')), + ('%*s %s', '%s %*s', (5, 'value', 'label')), + ], +) +def test_python_format_star_arguments_invalid(msgid, msgstr, arguments): + assert isinstance(msgid % arguments, str) + with pytest.raises(TypeError): + msgstr % arguments + with pytest.raises(TranslationError): + python_format(None, Message(msgid, msgstr)) + + +@pytest.mark.parametrize( + ('msgid', 'msgstr', 'arguments'), + [ + ('%*s', '[%*s]', (-6, 'word')), + ('%.*f', '%.*G', (2, 1.234)), + ('%*.*f', '%*.*F', (8, 2, 1.234)), + ('%10.2f', '%12.3f', (1.234,)), + ('%s %%*s', '%% %s', ('word',)), + ('%(name)10s', '%(name)s', {'name': 'word'}), + ('%*s', '%.*s', (4, 'example')), + ], +) +def test_python_format_star_arguments_valid(msgid, msgstr, arguments): + assert isinstance(msgid % arguments, str) + assert isinstance(msgstr % arguments, str) + python_format(None, Message(msgid, msgstr)) + + +def test_python_format_star_plural_catalog_check(): + catalog = Catalog(locale='en') + message = catalog.add(('%*s', '%*s'), ('%*s', '%s')) + errors = list(catalog.check()) + assert len(errors) == 1 + assert errors[0][0] is message + assert len(errors[0][1]) == 1 + assert isinstance(errors[0][1][0], TranslationError) + + @pytest.mark.parametrize( ('msgid', 'msgstr', 'error'), [