-
-
Notifications
You must be signed in to change notification settings - Fork 0
🐛 enhance comma-split behavior to enforce listLimit and handle overflow
#48
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
1c5c71c
e5b09d5
fcf3c20
133152b
a46a0c4
788cfe2
caac169
5018c16
585b3d4
f6b585e
f870325
13e73ea
74e7975
3db5c81
cb40ce9
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 |
|---|---|---|
|
|
@@ -27,10 +27,12 @@ part of '../qs.dart'; | |
| /// string → structure pipeline used by `QS.decode`. | ||
| extension _$Decode on QS { | ||
| /// Interprets a single scalar value as a list element when the `comma` | ||
| /// option is enabled and a comma is present, and enforces `listLimit`. | ||
| /// option is enabled and a comma is present. | ||
| /// | ||
| /// If `throwOnLimitExceeded` is true, exceeding `listLimit` will throw a | ||
| /// `RangeError`; otherwise the caller can decide how to degrade. | ||
| /// For comma-splits with non-negative `listLimit`, this method returns the | ||
| /// full split result and lets `_parseQueryStringValues` decide whether to | ||
| /// throw or convert to an overflow map. For negative `listLimit`, legacy | ||
| /// Dart-port semantics are preserved here. | ||
| /// | ||
| /// The `currentListLength` is used to guard incremental growth when we are | ||
| /// already building a list for a given key path. | ||
|
|
@@ -39,8 +41,8 @@ extension _$Decode on QS { | |
| /// elsewhere (e.g. `[2]` segments become string keys). For comma‑splits specifically: | ||
| /// when `throwOnLimitExceeded` is `true` and `listLimit < 0`, any non‑empty split throws | ||
| /// immediately; when `false`, growth is effectively capped at zero (the split produces | ||
| /// an empty list). Empty‑bracket pushes (`a[]=`) are handled during structure building | ||
| /// in `_parseObject`. | ||
| /// an empty list). Empty‑bracket pushes (`a[]=`) are enforced in | ||
| /// `_parseQueryStringValues`. | ||
| static dynamic _parseListValue( | ||
| dynamic val, | ||
| DecodeOptions options, | ||
|
|
@@ -50,28 +52,23 @@ extension _$Decode on QS { | |
| if (val is String && val.isNotEmpty && options.comma && val.contains(',')) { | ||
| final List<String> splitVal = val.split(','); | ||
| if (options.throwOnLimitExceeded && | ||
| (currentListLength + splitVal.length) > options.listLimit) { | ||
| final String msg = options.listLimit < 0 | ||
| ? 'List parsing is disabled (listLimit < 0).' | ||
| : 'List limit exceeded. Only ${options.listLimit} ' | ||
| 'element${options.listLimit == 1 ? '' : 's'} allowed in a list.'; | ||
| throw RangeError(msg); | ||
| options.listLimit < 0 && | ||
| splitVal.isNotEmpty) { | ||
| throw RangeError('List parsing is disabled (listLimit < 0).'); | ||
| } | ||
| final int remaining = options.listLimit - currentListLength; | ||
| if (remaining <= 0) return const <String>[]; | ||
| return splitVal.length <= remaining | ||
| ? splitVal | ||
| : splitVal.sublist(0, remaining); | ||
| if (options.listLimit < 0) { | ||
| // For negative list limits, comma growth is always capped at zero | ||
| // (with currentListLength always >= 0 at call sites). | ||
| return const <String>[]; | ||
| } | ||
| return splitVal; | ||
| } | ||
|
|
||
| // Guard incremental growth of an existing list as we parse additional items. | ||
| if (options.throwOnLimitExceeded && | ||
| options.listLimit >= 0 && | ||
| currentListLength >= options.listLimit) { | ||
| final String msg = options.listLimit < 0 | ||
| ? 'List parsing is disabled (listLimit < 0).' | ||
| : 'List limit exceeded. Only ${options.listLimit} ' | ||
| 'element${options.listLimit == 1 ? '' : 's'} allowed in a list.'; | ||
| throw RangeError(msg); | ||
| throw RangeError(_listLimitExceededMessage(options.listLimit)); | ||
| } | ||
|
|
||
| return val; | ||
|
|
@@ -84,8 +81,9 @@ extension _$Decode on QS { | |
| /// - charset sentinel detection (`utf8=`) per `qs` | ||
| /// - duplicate key policy (combine/first/last) | ||
| /// - parameter and list limits with optional throwing behavior | ||
| /// - Comma‑split growth honors `throwOnLimitExceeded` (see `_parseListValue`); | ||
| /// empty‑bracket pushes (`[]=`) are created during structure building in `_parseObject`. | ||
| /// - Comma‑split overflow (`listLimit >= 0`) is enforced after decoding: | ||
| /// throw in strict mode, otherwise convert to an overflow map. | ||
| /// - Empty‑bracket pushes (`[]=`) are created during structure building in `_parseObject`. | ||
| static Map<String, dynamic> _parseQueryStringValues( | ||
| String str, [ | ||
| DecodeOptions options = const DecodeOptions(), | ||
|
|
@@ -172,26 +170,50 @@ extension _$Decode on QS { | |
| ); | ||
| } | ||
|
|
||
| // Enforce comma-split overflow behavior before any normalization that can | ||
| // collapse iterables into scalars (e.g. numeric entity interpretation). | ||
| if (options.comma && | ||
| options.listLimit >= 0 && | ||
| val is Iterable && | ||
| val.length > options.listLimit) { | ||
| if (options.throwOnLimitExceeded) { | ||
| throw RangeError(_listLimitExceededMessage(options.listLimit)); | ||
| } | ||
| val = Utils.combine([], val, listLimit: options.listLimit); | ||
| } | ||
|
|
||
| // Optional HTML numeric entity interpretation (legacy Latin-1 queries). | ||
| if (val != null && | ||
| !Utils.isEmpty(val) && | ||
| options.interpretNumericEntities && | ||
| charset == latin1) { | ||
| if (val is Iterable) { | ||
| val = Utils.interpretNumericEntities(_joinIterableToCommaString(val)); | ||
| } else { | ||
| } else if (!(val is Map && Utils.isOverflow(val))) { | ||
| val = Utils.interpretNumericEntities(val.toString()); | ||
| } | ||
| } | ||
|
|
||
| // Quirk: a literal `[]=` suffix forces an array container (qs behavior). | ||
| if (options.parseLists && part.contains('[]=')) { | ||
| if (options.throwOnLimitExceeded && options.listLimit < 0) { | ||
| throw RangeError('List parsing is disabled (listLimit < 0).'); | ||
|
Comment on lines
198
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In Useful? React with 👍 / 👎. |
||
| } | ||
| val = [val]; | ||
| } | ||
|
Comment on lines
197
to
203
|
||
|
|
||
| // Duplicate key policy: combine/first/last (default: combine). | ||
| final bool existing = obj.containsKey(key); | ||
| if (existing && options.duplicates == Duplicates.combine) { | ||
| if (options.throwOnLimitExceeded && options.listLimit >= 0) { | ||
| final int? existingCount = _listLikeCount(obj[key]); | ||
| final int? incomingCount = _listLikeCount(val); | ||
| if (existingCount != null && | ||
| incomingCount != null && | ||
| existingCount + incomingCount > options.listLimit) { | ||
| throw RangeError(_listLimitExceededMessage(options.listLimit)); | ||
| } | ||
| } | ||
| obj[key] = Utils.combine(obj[key], val, listLimit: options.listLimit); | ||
| } else if (!existing || options.duplicates == Duplicates.last) { | ||
| obj[key] = val; | ||
|
|
@@ -201,6 +223,25 @@ extension _$Decode on QS { | |
| return obj; | ||
| } | ||
|
|
||
| /// Standard error text used for list limit overflows. | ||
| static String _listLimitExceededMessage(int listLimit) => | ||
| 'List limit exceeded. Only $listLimit ' | ||
| 'element${listLimit == 1 ? '' : 's'} allowed in a list.'; | ||
|
|
||
| /// Returns element count for values that participate in list growth checks. | ||
| /// | ||
| /// Maps that are not marked as overflow containers are treated as object values | ||
| /// and return `null` (not list-like). | ||
| static int? _listLikeCount(dynamic value) { | ||
| if (value is Iterable) return value.length; | ||
| if (value is Map) { | ||
| final int? overflowCount = Utils.overflowCount(value); | ||
| if (overflowCount != null) return overflowCount; | ||
| return null; | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| /// Reduces a list of key segments (e.g. `["a", "[b]", "[0]"]`) and a | ||
| /// leaf value into a nested structure. Operates right-to-left, constructing | ||
| /// maps or lists based on segment content and `DecodeOptions`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,6 @@ | |
| "author": "Klemen Tusar", | ||
| "license": "BSD-3-Clause", | ||
| "dependencies": { | ||
| "qs": "^6.14.1" | ||
| "qs": "^6.14.2" | ||
| } | ||
| } | ||
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.
The comma-overflow enforcement triggers for any Iterable
valwhenoptions.commais true, including Iterables returned by a customDecodeOptions.decodereven when the raw value was not comma-split. This can cause unexpected RangeErrors or overflow-map conversions for callers using custom decoders. Consider restricting this check to values that actually came from comma-splitting (e.g., track awasCommaSplitflag from_parseListValue, or check the raw substring for,before decoding) so non-comma Iterables from custom decoders aren’t treated as comma lists.