From 1c5c71ca96017a515b96dee1a190dbe434f93823 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 16 Feb 2026 23:48:00 +0000 Subject: [PATCH 01/15] chore: update `qs` dependency version to ^6.14.2 --- test/comparison/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/comparison/package.json b/test/comparison/package.json index 43215c2..0342b53 100644 --- a/test/comparison/package.json +++ b/test/comparison/package.json @@ -5,6 +5,6 @@ "author": "Klemen Tusar", "license": "BSD-3-Clause", "dependencies": { - "qs": "^6.14.1" + "qs": "^6.14.2" } } From e5b09d5e2488d2b5921f2bd20fbfc7aebc77b582 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 16 Feb 2026 23:51:17 +0000 Subject: [PATCH 02/15] :bug: enhance comma-split behavior to enforce list limits and handle overflow --- lib/src/extensions/decode.dart | 42 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index 35d42f6..1097a10 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -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. @@ -50,18 +52,19 @@ extension _$Decode on QS { if (val is String && val.isNotEmpty && options.comma && val.contains(',')) { final List splitVal = val.split(','); if (options.throwOnLimitExceeded && - (currentListLength + splitVal.length) > options.listLimit) { + options.listLimit < 0 && + splitVal.isNotEmpty) { 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); } - final int remaining = options.listLimit - currentListLength; - if (remaining <= 0) return const []; - return splitVal.length <= remaining - ? splitVal - : splitVal.sublist(0, remaining); + if (options.listLimit < 0) { + final int remaining = options.listLimit - currentListLength; + if (remaining <= 0) return const []; + } + return splitVal; } // Guard incremental growth of an existing list as we parse additional items. @@ -84,8 +87,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 _parseQueryStringValues( String str, [ DecodeOptions options = const DecodeOptions(), @@ -189,6 +193,22 @@ extension _$Decode on QS { val = [val]; } + // Enforce comma-split overflow behavior to match Node `qs`: + // - strict mode throws + // - non-strict mode preserves all elements by converting to overflow map + if (options.comma && + options.listLimit >= 0 && + val is Iterable && + val.length > options.listLimit) { + if (options.throwOnLimitExceeded) { + throw RangeError( + 'List limit exceeded. Only ${options.listLimit} ' + 'element${options.listLimit == 1 ? '' : 's'} allowed in a list.', + ); + } + val = Utils.combine([], val, listLimit: options.listLimit); + } + // Duplicate key policy: combine/first/last (default: combine). final bool existing = obj.containsKey(key); if (existing && options.duplicates == Duplicates.combine) { From fcf3c2018967fed0b7ed778fa25c63315432bc15 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 16 Feb 2026 23:51:29 +0000 Subject: [PATCH 03/15] :white_check_mark: enhance comma splitting behavior to handle overflow and strict mode limits --- test/unit/decode_test.dart | 58 ++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/test/unit/decode_test.dart b/test/unit/decode_test.dart index 4b05881..a21ae9f 100644 --- a/test/unit/decode_test.dart +++ b/test/unit/decode_test.dart @@ -2913,23 +2913,55 @@ void main() { }); group('Targeted coverage additions', () { - test('comma splitting truncates to remaining list capacity', () { + test('comma splitting at limit stays as list', () { final result = QS.decode( 'a=1,2,3', - const DecodeOptions(comma: true, listLimit: 2), + const DecodeOptions(comma: true, listLimit: 3), ); final Iterable iterable = result['a'] as Iterable; - expect(iterable.toList(), equals(['1', '2'])); + expect(iterable.toList(), equals(['1', '2', '3'])); + }); + + test('comma splitting over limit converts to overflow map', () { + final result = QS.decode( + 'a=1,2,3,4', + const DecodeOptions(comma: true, listLimit: 3), + ); + + final aValue = result['a']; + expect(aValue, { + '0': '1', + '1': '2', + '2': '3', + '3': '4', + }); + expect(Utils.isOverflow(aValue), isTrue); }); test('comma splitting throws when limit exceeded in strict mode', () { expect( () => QS.decode( - 'a=1,2', + 'a=1,2,3,4', + const DecodeOptions( + comma: true, + listLimit: 3, + throwOnLimitExceeded: true, + ), + ), + throwsA(isA()), + ); + }); + + test('GHSA payload throws when limit exceeded in strict mode', () { + final payload = 'a=${','.padLeft(25, ',')}'; + + expect( + () => QS.decode( + payload, const DecodeOptions( comma: true, - listLimit: 1, + listLimit: 5, throwOnLimitExceeded: true, ), ), @@ -2937,6 +2969,22 @@ void main() { ); }); + test('GHSA payload converts to overflow map without throw', () { + final payload = 'a=${','.padLeft(25, ',')}'; + final result = QS.decode( + payload, + const DecodeOptions( + comma: true, + listLimit: 5, + ), + ); + + final aValue = result['a']; + expect(aValue, isA>()); + expect((aValue as Map).length, 26); + expect(Utils.isOverflow(aValue), isTrue); + }); + test('strict depth throws when additional bracket groups remain', () { expect( () => QS.decode( From 133152bb806ab956ae7edc51b74c86d8ae4f2a20 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:08:42 +0000 Subject: [PATCH 04/15] :zap: enhance list limit handling to throw errors for overflow conditions --- lib/src/extensions/decode.dart | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index 1097a10..145d58e 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -54,11 +54,7 @@ extension _$Decode on QS { if (options.throwOnLimitExceeded && options.listLimit < 0 && splitVal.isNotEmpty) { - 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('List parsing is disabled (listLimit < 0).'); } if (options.listLimit < 0) { final int remaining = options.listLimit - currentListLength; @@ -212,6 +208,15 @@ extension _$Decode on QS { // 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; @@ -221,6 +226,33 @@ 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) { + if (Utils.isOverflow(value)) { + int maxIndex = -1; + for (final dynamic key in value.keys) { + final int? parsed = int.tryParse(key.toString()); + if (parsed != null && parsed >= 0 && parsed > maxIndex) { + maxIndex = parsed; + } + } + return maxIndex + 1; + } + 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`. From a46a0c4ae26d115bd242f97c74bbc406c7c9c5c9 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:08:54 +0000 Subject: [PATCH 05/15] :white_check_mark: enhance comma splitting tests for negative list limits and strict mode handling --- test/unit/decode_test.dart | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/unit/decode_test.dart b/test/unit/decode_test.dart index a21ae9f..c722e02 100644 --- a/test/unit/decode_test.dart +++ b/test/unit/decode_test.dart @@ -223,6 +223,39 @@ void main() { ); }); + test('comma: true with negative listLimit returns empty list', () { + expect( + QS.decode( + 'a=1,2', + const DecodeOptions( + comma: true, + listLimit: -1, + ), + ), + equals({'a': []}), + ); + }); + + test('comma: true with negative listLimit throws in strict mode', () { + expect( + () => QS.decode( + 'a=1,2', + const DecodeOptions( + comma: true, + listLimit: -1, + throwOnLimitExceeded: true, + ), + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('List parsing is disabled'), + ), + ), + ); + }); + test('allows enabling dot notation', () { expect(QS.decode('a.b=c'), equals({'a.b': 'c'})); expect( @@ -2939,6 +2972,64 @@ void main() { expect(Utils.isOverflow(aValue), isTrue); }); + test('comma splitting across duplicate keys throws in strict mode', () { + expect( + () => QS.decode( + 'a=1,2&a=3,4', + const DecodeOptions( + comma: true, + listLimit: 3, + throwOnLimitExceeded: true, + ), + ), + throwsA(isA()), + ); + }); + + test('comma splitting across duplicate keys over limit converts to map', + () { + final result = QS.decode( + 'a=1,2&a=3,4', + const DecodeOptions( + comma: true, + listLimit: 3, + ), + ); + + final aValue = result['a']; + expect(aValue, { + '0': '1', + '1': '2', + '2': '3', + '3': '4', + }); + expect(Utils.isOverflow(aValue), isTrue); + }); + + test( + 'strict duplicate limit check counts existing overflow-map values from decoder', + () { + final overflow = Utils.combine([], ['1', '2', '3', '4'], listLimit: 3); + expect(Utils.isOverflow(overflow), isTrue); + + expect( + () => QS.decode( + 'a=first&a=second', + DecodeOptions( + listLimit: 3, + throwOnLimitExceeded: true, + decoder: (value, {charset, kind}) { + if (kind == DecodeKind.value && value == 'first') { + return overflow; + } + return value; + }, + ), + ), + throwsA(isA()), + ); + }); + test('comma splitting throws when limit exceeded in strict mode', () { expect( () => QS.decode( From 788cfe2f3337568e93cf72e1ecbd2d2b4a6213ee Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:12:32 +0000 Subject: [PATCH 06/15] :zap: cap comma growth at zero for negative list limits in decoding --- lib/src/extensions/decode.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index 145d58e..00306a2 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -57,8 +57,9 @@ extension _$Decode on QS { throw RangeError('List parsing is disabled (listLimit < 0).'); } if (options.listLimit < 0) { - final int remaining = options.listLimit - currentListLength; - if (remaining <= 0) return const []; + // For negative list limits, comma growth is always capped at zero + // (with currentListLength always >= 0 at call sites). + return const []; } return splitVal; } From caac169ff9d53764261789789e23fa38f125528b Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:24:27 +0000 Subject: [PATCH 07/15] :zap: enhance comma-split overflow handling to enforce list limits and improve error management --- lib/src/extensions/decode.dart | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index 00306a2..ea5305e 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -173,6 +173,18 @@ 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) && @@ -180,7 +192,7 @@ extension _$Decode on QS { 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()); } } @@ -190,22 +202,6 @@ extension _$Decode on QS { val = [val]; } - // Enforce comma-split overflow behavior to match Node `qs`: - // - strict mode throws - // - non-strict mode preserves all elements by converting to overflow map - if (options.comma && - options.listLimit >= 0 && - val is Iterable && - val.length > options.listLimit) { - if (options.throwOnLimitExceeded) { - throw RangeError( - 'List limit exceeded. Only ${options.listLimit} ' - 'element${options.listLimit == 1 ? '' : 's'} allowed in a list.', - ); - } - val = Utils.combine([], val, listLimit: options.listLimit); - } - // Duplicate key policy: combine/first/last (default: combine). final bool existing = obj.containsKey(key); if (existing && options.duplicates == Duplicates.combine) { From 5018c16ad9e54d77263873b0d957f55f673a92b5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:24:48 +0000 Subject: [PATCH 08/15] :white_check_mark: enhance comma decoding tests for strict limit handling with latin1 numeric-entity interpretation --- test/unit/decode_test.dart | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/unit/decode_test.dart b/test/unit/decode_test.dart index c722e02..524fc04 100644 --- a/test/unit/decode_test.dart +++ b/test/unit/decode_test.dart @@ -3044,6 +3044,47 @@ void main() { ); }); + test( + 'comma strict limit still throws with latin1 numeric-entity interpretation', + () { + expect( + () => QS.decode( + 'a=1,2,3,4', + const DecodeOptions( + comma: true, + listLimit: 3, + throwOnLimitExceeded: true, + charset: latin1, + interpretNumericEntities: true, + ), + ), + throwsA(isA()), + ); + }); + + test( + 'comma overflow still converts to map with latin1 numeric-entity interpretation', + () { + final result = QS.decode( + 'a=1,2,3,4', + const DecodeOptions( + comma: true, + listLimit: 3, + charset: latin1, + interpretNumericEntities: true, + ), + ); + + final aValue = result['a']; + expect(aValue, { + '0': '1', + '1': '2', + '2': '3', + '3': '4', + }); + expect(Utils.isOverflow(aValue), isTrue); + }); + test('GHSA payload throws when limit exceeded in strict mode', () { final payload = 'a=${','.padLeft(25, ',')}'; From 585b3d4433009fcd7453aadb742977485e0c1326 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:27:25 +0000 Subject: [PATCH 09/15] :zap: streamline error handling for list limit exceeded conditions --- lib/src/extensions/decode.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index ea5305e..885fffe 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -67,11 +67,10 @@ extension _$Decode on QS { // Guard incremental growth of an existing list as we parse additional items. if (options.throwOnLimitExceeded && 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); + if (options.listLimit < 0) { + throw RangeError('List parsing is disabled (listLimit < 0).'); + } + throw RangeError(_listLimitExceededMessage(options.listLimit)); } return val; @@ -236,6 +235,8 @@ extension _$Decode on QS { if (value is Iterable) return value.length; if (value is Map) { if (Utils.isOverflow(value)) { + // Overflow containers append by numeric index, so strict limit checks + // must use maxIndex + 1 rather than entry count (sparse keys included). int maxIndex = -1; for (final dynamic key in value.keys) { final int? parsed = int.tryParse(key.toString()); From f6b585ec15cce463579f93118e0ae94754f9b7ec Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:27:37 +0000 Subject: [PATCH 10/15] :white_check_mark: add test for strict duplicate limit check using max overflow index in sparse overflow maps --- test/unit/decode_test.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/unit/decode_test.dart b/test/unit/decode_test.dart index 524fc04..84f7edc 100644 --- a/test/unit/decode_test.dart +++ b/test/unit/decode_test.dart @@ -3030,6 +3030,31 @@ void main() { ); }); + test( + 'strict duplicate limit check uses max overflow index for sparse overflow maps', + () { + final sparseOverflow = + Utils.markOverflow({'100': 'x'}, 100); + expect(Utils.isOverflow(sparseOverflow), isTrue); + + expect( + () => QS.decode( + 'a=first&a=second', + DecodeOptions( + listLimit: 3, + throwOnLimitExceeded: true, + decoder: (value, {charset, kind}) { + if (kind == DecodeKind.value && value == 'first') { + return sparseOverflow; + } + return value; + }, + ), + ), + throwsA(isA()), + ); + }); + test('comma splitting throws when limit exceeded in strict mode', () { expect( () => QS.decode( From f870325fe5725dfad38ef1f0b4e55b25085d091a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:28:35 +0000 Subject: [PATCH 11/15] :white_check_mark: format GHSA payload initialization for clarity in tests --- test/unit/decode_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/decode_test.dart b/test/unit/decode_test.dart index 84f7edc..1892f68 100644 --- a/test/unit/decode_test.dart +++ b/test/unit/decode_test.dart @@ -3111,7 +3111,8 @@ void main() { }); test('GHSA payload throws when limit exceeded in strict mode', () { - final payload = 'a=${','.padLeft(25, ',')}'; + final payload = + 'a=${','.padLeft(25, ',')}'; // 25 commas => 26 elements after split. expect( () => QS.decode( @@ -3127,7 +3128,8 @@ void main() { }); test('GHSA payload converts to overflow map without throw', () { - final payload = 'a=${','.padLeft(25, ',')}'; + final payload = + 'a=${','.padLeft(25, ',')}'; // 25 commas => 26 elements after split. final result = QS.decode( payload, const DecodeOptions( From 13e73ea5125203ab0c3aa8a6b160dbe719c5cc2e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:33:05 +0000 Subject: [PATCH 12/15] :recycle: simplify overflow handling by introducing overflowCount utility --- lib/src/extensions/decode.dart | 14 ++------------ lib/src/utils.dart | 7 +++++++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index 885fffe..6728964 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -234,18 +234,8 @@ extension _$Decode on QS { static int? _listLikeCount(dynamic value) { if (value is Iterable) return value.length; if (value is Map) { - if (Utils.isOverflow(value)) { - // Overflow containers append by numeric index, so strict limit checks - // must use maxIndex + 1 rather than entry count (sparse keys included). - int maxIndex = -1; - for (final dynamic key in value.keys) { - final int? parsed = int.tryParse(key.toString()); - if (parsed != null && parsed >= 0 && parsed > maxIndex) { - maxIndex = parsed; - } - } - return maxIndex + 1; - } + final int? overflowCount = Utils.overflowCount(value); + if (overflowCount != null) return overflowCount; return null; } return 1; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 1defe05..b419c8b 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -49,6 +49,13 @@ final class Utils { static bool isOverflow(dynamic obj) => obj is Map && _overflowIndex[obj] != null; + /// Returns tracked overflow length (`maxIndex + 1`) or `null` if not overflow. + @internal + static int? overflowCount(dynamic obj) { + if (!isOverflow(obj) || obj is! Map) return null; + return _getOverflowIndex(obj) + 1; + } + /// Returns the tracked max numeric index for an overflow map, or -1 if absent. static int _getOverflowIndex(Map obj) => _overflowIndex[obj] ?? -1; From 74e7975228b8e161da046437294bc277663a67cd Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:33:10 +0000 Subject: [PATCH 13/15] :white_check_mark: add tests for overflowCount utility in merge and sparse overflow scenarios --- test/unit/utils_test.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/utils_test.dart b/test/unit/utils_test.dart index 2896013..3789447 100644 --- a/test/unit/utils_test.dart +++ b/test/unit/utils_test.dart @@ -956,9 +956,11 @@ void main() { final overflow = Utils.combine(['a'], 'b', listLimit: 1) as Map; expect(Utils.isOverflow(overflow), isTrue); + expect(Utils.overflowCount(overflow), 2); final merged = Utils.merge(overflow, 'c') as Map; expect(merged, {'0': 'a', '1': 'b', '2': 'c'}); + expect(Utils.overflowCount(merged), 3); }); test('merges overflow object into primitive', () { @@ -999,6 +1001,15 @@ void main() { '1': 'b', }); expect(Utils.isOverflow(merged), isFalse); + expect(Utils.overflowCount(merged), isNull); + }); + + test('overflowCount follows tracked max index for sparse overflow maps', + () { + final sparse = Utils.markOverflow({'100': 'x'}, 100); + + expect(Utils.isOverflow(sparse), isTrue); + expect(Utils.overflowCount(sparse), 101); }); }); }); From 3db5c812bc5377ad3aef6f0939240a29d49a4110 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:37:32 +0000 Subject: [PATCH 14/15] :recycle: enforce empty-bracket handling in _parseQueryStringValues and improve list limit checks --- lib/src/extensions/decode.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/extensions/decode.dart b/lib/src/extensions/decode.dart index 6728964..dd45ee6 100644 --- a/lib/src/extensions/decode.dart +++ b/lib/src/extensions/decode.dart @@ -41,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, @@ -66,10 +66,8 @@ extension _$Decode on QS { // Guard incremental growth of an existing list as we parse additional items. if (options.throwOnLimitExceeded && + options.listLimit >= 0 && currentListLength >= options.listLimit) { - if (options.listLimit < 0) { - throw RangeError('List parsing is disabled (listLimit < 0).'); - } throw RangeError(_listLimitExceededMessage(options.listLimit)); } @@ -198,6 +196,9 @@ extension _$Decode on QS { // 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).'); + } val = [val]; } From cb40ce9eb0acc50bd5f215ddd8bcb13909c8fdcc Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 17 Feb 2026 00:37:37 +0000 Subject: [PATCH 15/15] :white_check_mark: add test for negative list limit in strict mode to ensure non-list values decode correctly --- test/unit/decode_test.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/unit/decode_test.dart b/test/unit/decode_test.dart index 1892f68..1a5831d 100644 --- a/test/unit/decode_test.dart +++ b/test/unit/decode_test.dart @@ -2048,6 +2048,28 @@ void main() { ); }); + test( + 'negative list limit strict mode still decodes non-list values without growth', + () { + expect( + QS.decode( + 'a=b', + const DecodeOptions(listLimit: -1, throwOnLimitExceeded: true), + ), + equals({'a': 'b'}), + ); + + expect( + QS.decode( + 'a[0]=b', + const DecodeOptions(listLimit: -1, throwOnLimitExceeded: true), + ), + equals({ + 'a': {'0': 'b'} + }), + ); + }); + test('applies list limit to nested lists', () { expect( () => QS.decode(