-
-
Notifications
You must be signed in to change notification settings - Fork 1
🐛 fix decode: enforce list_limit on comma-parsed values (qs v6.14.2 parity)
#41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
54b7e33
fa8d4d0
62357cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||||||||||||||||||||
|
Comment on lines
+293
to
+298
|
||||||||||||||||||||||||||||||
| 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) | |
| if options.comma and isinstance(val, (list, tuple)): | |
| val_sequence = list(val) | |
| if len(val_sequence) > 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_sequence, options) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
| ), | ||
|
Comment on lines
+1373
to
+1407
|
||
| ], | ||
| ) | ||
| 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 --- | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With inputs like
a[]=1,2,3,4andDecodeOptions(comma=True, list_limit=3, raise_on_limit_exceeded=False), the overflow check is bypassed becausevalis wrapped to[val]before the new limit guard runs, makinglen(val)equal to 1. This means over-limit comma-parsed payloads that use[]=still return oversized inner lists instead of being normalized to overflow-object semantics, so the list-limit enforcement added here is incomplete for a common query form.Useful? React with 👍 / 👎.