From 47e05ab9f7e683b29edf2bf69efdadf65601a638 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sat, 20 Jun 2026 11:05:23 +0200 Subject: [PATCH] Return None instead of crashing on invalid An+B input parse_nth is documented to return None for invalid input, but several truncated nth-child fragments crashed instead: - '+' (and '+/**/') raised StopIteration: after a leading '+', next(tokens) read the following token without guarding against an exhausted iterator. - 'n+', 'n +', '-n-', '2n +' raised AttributeError: parse_signless_b dereferenced token.type when _next_significant returned None. Guard both reads so they fall through to the function's implicit return None. Whitespace after a leading '+' stays invalid ('+ n' -> None, '+n' -> (1, 0)), matching the An+B spec test data. --- tests/test_tinycss2.py | 14 ++++++++++++++ tinycss2/nth.py | 8 +++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_tinycss2.py b/tests/test_tinycss2.py index b10f5c9..fb1130a 100644 --- a/tests/test_tinycss2.py +++ b/tests/test_tinycss2.py @@ -153,6 +153,20 @@ def test_nth(input): return parse_nth(input) +@pytest.mark.parametrize('invalid', ['+', '+/**/', 'n+', 'n +', '-n-', '2n +']) +def test_nth_invalid_does_not_crash(invalid): + # Truncated/invalid An+B fragments must return None per parse_nth's + # documented contract, not raise StopIteration or AttributeError. + assert parse_nth(invalid) is None + + +def test_nth_leading_plus_whitespace_still_invalid(): + # The fix for the above must not make '+ n' (whitespace after a leading + # '+') accidentally valid: only '+n' is a valid nth expression. + assert parse_nth('+n') == (1, 0) + assert parse_nth('+ n') is None + + def _number(value): if value is None: return 'none' diff --git a/tinycss2/nth.py b/tinycss2/nth.py index c5e924b..225a3ac 100644 --- a/tinycss2/nth.py +++ b/tinycss2/nth.py @@ -59,8 +59,10 @@ def parse_nth(input): if match: return parse_end(tokens, 1, int(match.group(1))) elif token == '+': - token = next(tokens) # Whitespace after an initial '+' is invalid. - if token.type == 'ident': + # Whitespace after an initial '+' is invalid, so the next token is read + # without skipping it. ``None`` is used when the iterator is exhausted. + token = next(tokens, None) + if token is not None and token.type == 'ident': ident = token.lower_value if ident == 'n': return parse_b(tokens, 1) @@ -87,7 +89,7 @@ def parse_b(tokens, a): def parse_signless_b(tokens, a, b_sign): token = _next_significant(tokens) - if (token.type == 'number' and token.is_integer and + if (token is not None and token.type == 'number' and token.is_integer and token.representation[0] not in '-+'): return parse_end(tokens, a, b_sign * token.int_value)