diff --git a/src/qs_codec/decode.py b/src/qs_codec/decode.py index 0d75e47..8275746 100644 --- a/src/qs_codec/decode.py +++ b/src/qs_codec/decode.py @@ -137,7 +137,8 @@ def _parse_array_value(value: t.Any, options: DecodeOptions, current_list_length - When ``list_limit`` is negative: * if ``raise_on_limit_exceeded=True``, **any** list-growth operation here (e.g., comma-splitting) raises immediately; - * if ``raise_on_limit_exceeded=False`` (default), comma-splitting still returns a list; numeric + * if ``raise_on_limit_exceeded=False`` (default), comma-splitting is allowed here and is normalized + later in ``_parse_query_string_values`` (which applies overflow conversion when needed). Numeric bracket indices are handled later by ``_parse_object`` (where negative ``list_limit`` disables numeric-index parsing only). @@ -176,7 +177,10 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str * Decode key/value via ``options.decoder`` (default: percent-decoding using the selected ``charset``). Keys are passed with ``kind=DecodeKind.KEY`` and values with ``kind=DecodeKind.VALUE``; a custom decoder may return the raw token or ``None``. - * Apply comma-split list logic to values (handled here). Index-based list growth from bracket segments is applied later in ``_parse_object``. When ``list_limit < 0`` and ``raise_on_limit_exceeded=True``, any comma-split that would increase the list length raises immediately; otherwise the split proceeds. + * Apply comma-split list logic to values (handled here). Index-based list growth from bracket segments + is applied later in ``_parse_object``. When ``comma=True`` yields a list longer than ``list_limit``, + the value either raises (``raise_on_limit_exceeded=True``) or is normalized through overflow conversion + (``raise_on_limit_exceeded=False``), mirroring indexed/bracket list-limit handling. * Interpret numeric entities for Latin-1 when requested. * Handle empty brackets ``[]`` as list markers (wrapping exactly once). * Merge duplicate keys according to ``duplicates`` policy. @@ -285,6 +289,14 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str if options.parse_lists and "[]=" in part: val = [val] + # Enforce comma-split list limits consistently with other list construction paths. + if options.comma and isinstance(val, list) and len(val) > options.list_limit: + if options.raise_on_limit_exceeded: + raise ValueError( + f"List limit exceeded: Only {options.list_limit} element{'' if options.list_limit == 1 else 's'} allowed in a list." + ) + val = Utils.combine([], val, options) + existing: bool = key in obj # Combine/overwrite according to the configured duplicates policy. diff --git a/tests/unit/decode_test.py b/tests/unit/decode_test.py index c2175a1..9cac89f 100644 --- a/tests/unit/decode_test.py +++ b/tests/unit/decode_test.py @@ -1370,6 +1370,75 @@ def test_list_limit( else: assert decode(query, options) == expected + @pytest.mark.parametrize( + "query, options, expected, raises_error, expect_overflow", + [ + pytest.param( + "a=1,2,3", + DecodeOptions(comma=True, list_limit=5), + {"a": ["1", "2", "3"]}, + False, + False, + id="comma-within-list-limit", + ), + pytest.param( + "a=1,2,3,4", + DecodeOptions(comma=True, list_limit=3), + {"a": {"0": "1", "1": "2", "2": "3", "3": "4"}}, + False, + True, + id="comma-over-list-limit-converts-to-overflow", + ), + pytest.param( + "a=1,2,3,4", + DecodeOptions(comma=True, list_limit=3, raise_on_limit_exceeded=True), + None, + True, + False, + id="comma-over-list-limit-raises", + ), + pytest.param( + "a=1,2,3", + DecodeOptions(comma=True, list_limit=3), + {"a": ["1", "2", "3"]}, + False, + False, + id="comma-at-list-limit-stays-list", + ), + ], + ) + def test_comma_list_limit_parity( + self, + query: str, + options: DecodeOptions, + expected: t.Optional[t.Mapping[str, t.Any]], + raises_error: bool, + expect_overflow: bool, + ) -> None: + if raises_error: + with pytest.raises(ValueError, match="List limit exceeded"): + decode(query, options) + return + + result = decode(query, options) + assert result == expected + if expect_overflow: + assert isinstance(result["a"], OverflowDict) + else: + assert isinstance(result["a"], list) + + def test_comma_list_limit_raises_when_decoder_returns_oversized_list(self) -> None: + def _decoder(s: t.Optional[str], charset: t.Optional[Charset], *, kind: DecodeKind = DecodeKind.VALUE) -> t.Any: + if kind is DecodeKind.VALUE and s == "1": + return ["x", "y"] + return DecodeUtils.decode(s, charset=charset, kind=kind) + + with pytest.raises(ValueError, match="List limit exceeded"): + decode( + "a=1", + DecodeOptions(comma=True, list_limit=1, raise_on_limit_exceeded=True, decoder=_decoder), + ) + # --- Additional tests for decoder kind and parser state isolation ---