From aeebeadc001f340d0a81ec159d7bb7bc69f81033 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Jan 2026 23:13:35 +0000 Subject: [PATCH 1/5] fix: implement array limit handling to prevent DoS via memory exhaustion --- QsNet.Tests/DecodeTests.cs | 141 +++++++++++++++++++++++++++++--- QsNet.Tests/UtilsTests.cs | 147 ++++++++++++++++++++++++++++++++++ QsNet/Internal/Decoder.cs | 23 ++++-- QsNet/Internal/Utils.cs | 160 ++++++++++++++++++++++++++++++++++++- 4 files changed, 448 insertions(+), 23 deletions(-) diff --git a/QsNet.Tests/DecodeTests.cs b/QsNet.Tests/DecodeTests.cs index 37a3b4a..4a0b2d8 100644 --- a/QsNet.Tests/DecodeTests.cs +++ b/QsNet.Tests/DecodeTests.cs @@ -752,7 +752,7 @@ public void Decode_ParsesMixOfSimpleAndExplicitLists() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", "c" } + ["a"] = new Dictionary { ["0"] = "b", ["1"] = "c" } } ); @@ -779,7 +779,7 @@ public void Decode_ParsesMixOfSimpleAndExplicitLists() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", "c" } + ["a"] = new Dictionary { ["0"] = "b", ["1"] = "c" } } ); @@ -1295,7 +1295,13 @@ public void Decode_AllowsEmptyStringsInLists() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", null, "c", "" } + ["a"] = new Dictionary + { + ["0"] = "b", + ["1"] = null, + ["2"] = "c", + ["3"] = "" + } } ); @@ -1319,7 +1325,13 @@ public void Decode_AllowsEmptyStringsInLists() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", "", "c", null } + ["a"] = new Dictionary + { + ["0"] = "b", + ["1"] = "", + ["2"] = "c", + ["3"] = null + } } ); @@ -2560,7 +2572,83 @@ public void Decode_ListLimit_HandlesListLimitOfZeroCorrectly() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "1", "2" } + ["a"] = new Dictionary { ["0"] = "1", ["1"] = "2" } + } + ); + } + + [Fact] + public void Decode_ListLimit_RespectsImplicitArrayLimit() + { + var values = Enumerable.Repeat("x", 105).ToArray(); + var attack = "a[]=" + string.Join("&a[]=", values); + var result = Qs.Decode(attack, new DecodeOptions { ListLimit = 100 }); + + result.Should().ContainKey("a"); + result["a"].Should().BeAssignableTo(); + ((IDictionary)result["a"]!).Count.Should().Be(105); + } + + [Fact] + public void Decode_ListLimit_BoundaryConditions() + { + Qs.Decode("a[]=1&a[]=2&a[]=3", new DecodeOptions { ListLimit = 3 }) + .Should() + .BeEquivalentTo( + new Dictionary + { + ["a"] = new List { "1", "2", "3" } + } + ); + + Qs.Decode("a[]=1&a[]=2&a[]=3&a[]=4", new DecodeOptions { ListLimit = 3 }) + .Should() + .BeEquivalentTo( + new Dictionary + { + ["a"] = new Dictionary + { + ["0"] = "1", + ["1"] = "2", + ["2"] = "3", + ["3"] = "4" + } + } + ); + + Qs.Decode("a[]=1&a[]=2", new DecodeOptions { ListLimit = 1 }) + .Should() + .BeEquivalentTo( + new Dictionary + { + ["a"] = new Dictionary { ["0"] = "1", ["1"] = "2" } + } + ); + } + + [Fact] + public void Decode_ListLimit_ConvertsDuplicateValuesWhenLimitExceeded() + { + Qs.Decode("a=b&a=c&a=d", new DecodeOptions { ListLimit = 20 }) + .Should() + .BeEquivalentTo( + new Dictionary + { + ["a"] = new List { "b", "c", "d" } + } + ); + + Qs.Decode("a=b&a=c&a=d", new DecodeOptions { ListLimit = 2 }) + .Should() + .BeEquivalentTo( + new Dictionary + { + ["a"] = new Dictionary + { + ["0"] = "b", + ["1"] = "c", + ["2"] = "d" + } } ); } @@ -2915,7 +3003,7 @@ public void ShouldParseAMixOfSimpleAndExplicitArrays() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", "c" } + ["a"] = new Dictionary { ["0"] = "b", ["1"] = "c" } } ); @@ -2933,7 +3021,7 @@ public void ShouldParseAMixOfSimpleAndExplicitArrays() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", "c" } + ["a"] = new Dictionary { ["0"] = "b", ["1"] = "c" } } ); } @@ -3423,7 +3511,13 @@ public void ShouldAllowForEmptyStringsInArrays() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", null, "c", "" } + ["a"] = new Dictionary + { + ["0"] = "b", + ["1"] = null, + ["2"] = "c", + ["3"] = "" + } } ); @@ -3441,7 +3535,13 @@ public void ShouldAllowForEmptyStringsInArrays() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "b", "", "c", null } + ["a"] = new Dictionary + { + ["0"] = "b", + ["1"] = "", + ["2"] = "c", + ["3"] = null + } } ); @@ -3450,7 +3550,12 @@ public void ShouldAllowForEmptyStringsInArrays() .BeEquivalentTo( new Dictionary { - ["a"] = new List { "", "b", "c" } + ["a"] = new Dictionary + { + ["0"] = "", + ["1"] = "b", + ["2"] = "c" + } } ); } @@ -4178,9 +4283,19 @@ public void Decode_CommaSplit_NoTruncationWhenSumExceedsLimit_AndThrowOff() var result = Qs.Decode("a=1,2&a=3,4,5", opts); var dict = Assert.IsType>(result); - var list = Assert.IsType>(dict["a"]); - // With ThrowOnLimitExceeded = false, no truncation occurs; full concatenation is allowed - list.Select(x => x?.ToString()).Should().Equal("1", "2", "3", "4", "5"); + var list = Assert.IsType>(dict["a"]); + // With ThrowOnLimitExceeded = false, values are preserved but list limit still converts to a map. + list.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "1", + ["1"] = "2", + ["2"] = "3", + ["3"] = "4", + ["4"] = "5" + } + ); } [Fact] diff --git a/QsNet.Tests/UtilsTests.cs b/QsNet.Tests/UtilsTests.cs index 2a5e94d..451ff29 100644 --- a/QsNet.Tests/UtilsTests.cs +++ b/QsNet.Tests/UtilsTests.cs @@ -1233,6 +1233,153 @@ public void Combine_ListAndScalarPreservesOrder() .BeEquivalentTo(new List { 1, 2, 3 }); } + [Fact] + public void Combine_WithListLimit_UnderAndOverLimit() + { + var under = Utils.CombineWithLimit( + new List { "a", "b" }, + "c", + new DecodeOptions { ListLimit = 10 } + ); + under.Should().BeOfType>(); + under.Should().BeEquivalentTo(new List { "a", "b", "c" }); + + var atLimit = Utils.CombineWithLimit( + new List { "a", "b" }, + "c", + new DecodeOptions { ListLimit = 3 } + ); + atLimit.Should().BeOfType>(); + atLimit.Should().BeEquivalentTo(new List { "a", "b", "c" }); + + var over = Utils.CombineWithLimit( + new List { "a", "b", "c" }, + "d", + new DecodeOptions { ListLimit = 3 } + ); + over.Should().BeOfType>(); + over.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["2"] = "c", + ["3"] = "d" + } + ); + Utils.IsOverflow(over).Should().BeTrue(); + } + + [Fact] + public void Combine_WithListLimit_ZeroConvertsToMap() + { + var combined = Utils.CombineWithLimit( + new List(), + "a", + new DecodeOptions { ListLimit = 0 } + ); + combined.Should().BeOfType>(); + combined.Should().BeEquivalentTo(new Dictionary { ["0"] = "a" }); + } + + [Fact] + public void Combine_WithOverflowObject_AppendsAtNextIndex() + { + var overflow = Utils.CombineWithLimit( + new List { "a" }, + "b", + new DecodeOptions { ListLimit = 1 } + ); + Utils.IsOverflow(overflow).Should().BeTrue(); + + var combined = Utils.CombineWithLimit(overflow, "c", new DecodeOptions { ListLimit = 10 }); + combined.Should().BeSameAs(overflow); + combined.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["2"] = "c" + } + ); + } + + [Fact] + public void Combine_WithPlainMap_DoesNotUseOverflowBehavior() + { + var plain = new Dictionary { ["0"] = "a", ["1"] = "b" }; + Utils.IsOverflow(plain).Should().BeFalse(); + + var combined = Utils.CombineWithLimit(plain, "c", new DecodeOptions { ListLimit = 10 }); + combined.Should().BeEquivalentTo(new List { plain, "c" }); + } + + [Fact] + public void Merge_WithOverflowObject_AppendsAtNextIndex() + { + var overflow = Utils.CombineWithLimit( + new List { "a" }, + "b", + new DecodeOptions { ListLimit = 1 } + ); + Utils.IsOverflow(overflow).Should().BeTrue(); + + var merged = Utils.Merge(overflow, "c"); + merged.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["2"] = "c" + } + ); + Utils.IsOverflow(merged).Should().BeTrue(); + } + + [Fact] + public void Merge_WithPlainMap_UsesValueAsKey() + { + var obj = new Dictionary { ["0"] = "a", ["1"] = "b" }; + Utils.IsOverflow(obj).Should().BeFalse(); + + var merged = Utils.Merge(obj, "c"); + merged.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["c"] = true + } + ); + } + + [Fact] + public void Merge_OverflowObjectIntoPrimitive_ShiftsIndices() + { + var overflow = Utils.CombineWithLimit( + new List { "b" }, + "c", + new DecodeOptions { ListLimit = 1 } + ); + Utils.IsOverflow(overflow).Should().BeTrue(); + + var merged = Utils.Merge("a", overflow); + merged.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["2"] = "c" + } + ); + Utils.IsOverflow(merged).Should().BeTrue(); + } + [Fact] public void InterpretNumericEntities_ReturnsInputUnchangedWhenThereAreNoEntities() { diff --git a/QsNet/Internal/Decoder.cs b/QsNet/Internal/Decoder.cs index b6091dc..fc18985 100644 --- a/QsNet/Internal/Decoder.cs +++ b/QsNet/Internal/Decoder.cs @@ -253,7 +253,7 @@ int currentListLength switch (options.Duplicates) { case Duplicates.Combine: - obj[key] = Utils.Combine(existingVal, value); + obj[key] = Utils.CombineWithLimit(existingVal, value, options); break; case Duplicates.Last: obj[key] = value; @@ -352,13 +352,20 @@ bool valuesParsed } else { - if ( - options.AllowEmptyLists - && (leaf?.Equals("") == true || (options.StrictNullHandling && leaf == null)) - ) - obj = new List(); + if (Utils.IsOverflow(leaf)) + { + obj = leaf; + } else - obj = Utils.Combine(new List(), leaf); + { + if ( + options.AllowEmptyLists + && (leaf?.Equals("") == true || (options.StrictNullHandling && leaf == null)) + ) + obj = new List(); + else + obj = Utils.CombineWithLimit(new List(), leaf, options); + } } } else @@ -758,4 +765,4 @@ private static string JoinAsCommaSeparatedStrings(IEnumerable enumerable) return sb?.ToString() ?? string.Empty; } -} \ No newline at end of file +} diff --git a/QsNet/Internal/Utils.cs b/QsNet/Internal/Utils.cs index e52d4b6..f01195c 100644 --- a/QsNet/Internal/Utils.cs +++ b/QsNet/Internal/Utils.cs @@ -27,6 +27,13 @@ internal static partial class Utils /// private const int SegmentLimit = 1024; + private sealed class OverflowState + { + public int MaxIndex { get; set; } + } + + private static readonly ConditionalWeakTable OverflowTable = new(); + /// /// A regex to match percent-encoded characters in the format %XX. /// @@ -179,6 +186,37 @@ private static Regex MyRegex1() case IDictionary targetMap: { var mutable = ToDictionary(targetMap); + var targetMapOverflow = IsOverflow(targetMap); + if (targetMapOverflow && !ReferenceEquals(mutable, targetMap)) + SetOverflowMaxIndex(mutable, GetOverflowMaxIndex(targetMap)); + + if (targetMapOverflow) + { + var targetMaxIndex = GetOverflowMaxIndex(mutable); + switch (source) + { + case IEnumerable srcIter: + { + var i = targetMaxIndex + 1; + foreach (var item in srcIter) + { + if (item is not Undefined) + mutable[i.ToString(CultureInfo.InvariantCulture)] = item; + i++; + } + + SetOverflowMaxIndex(mutable, i - 1); + return mutable; + } + case Undefined: + return mutable; + } + + var nextIndex = targetMaxIndex + 1; + mutable[nextIndex.ToString(CultureInfo.InvariantCulture)] = source; + SetOverflowMaxIndex(mutable, nextIndex); + return mutable; + } switch (source) { @@ -217,11 +255,19 @@ private static Regex MyRegex1() // Source IS a map var sourceMap = (IDictionary)source; // iterate the original map + var sourceOverflow = IsOverflow(sourceMap); Dictionary mergeTarget; + var initialMaxIndex = -1; + var targetOverflow = false; switch (target) { case IDictionary tmap: mergeTarget = ToDictionary(tmap); + targetOverflow = IsOverflow(tmap); + if (targetOverflow && !ReferenceEquals(mergeTarget, tmap)) + SetOverflowMaxIndex(mergeTarget, GetOverflowMaxIndex(tmap)); + if (targetOverflow) + initialMaxIndex = GetOverflowMaxIndex(mergeTarget); break; case IEnumerable tEnum: @@ -237,23 +283,54 @@ private static Regex MyRegex1() } mergeTarget = dict; + if (i > 0) + initialMaxIndex = i - 1; break; } default: { if (target is null or Undefined) - return NormalizeForTarget((IDictionary)source); + { + var normalized = NormalizeForTarget(sourceMap); + if (sourceOverflow && normalized is IDictionary normalizedMap) + SetOverflowMaxIndex(normalizedMap, GetOverflowMaxIndex(sourceMap)); + return normalized; + } + + if (sourceOverflow) + { + var result = new Dictionary(sourceMap.Count + 1) + { + ["0"] = target + }; + foreach (DictionaryEntry entry in sourceMap) + { + if (TryGetArrayIndex(entry.Key, out var idx)) + result[(idx + 1).ToString(CultureInfo.InvariantCulture)] = entry.Value; + else + result[entry.Key] = entry.Value; + } + + var sourceMaxIndex = GetOverflowMaxIndex(sourceMap); + SetOverflowMaxIndex(result, sourceMaxIndex >= 0 ? sourceMaxIndex + 1 : 0); + return result; + } var list = new List { target, - ToObjectKeyedDictionary((IDictionary)source) + ToObjectKeyedDictionary(sourceMap) }; return list; } } + var trackOverflow = targetOverflow || sourceOverflow; + var maxIndex = trackOverflow ? initialMaxIndex : -1; + if (trackOverflow && sourceOverflow) + maxIndex = Math.Max(maxIndex, GetOverflowMaxIndex(sourceMap)); + foreach (DictionaryEntry entry in sourceMap) { var key = entry.Key; @@ -262,8 +339,14 @@ private static Regex MyRegex1() mergeTarget[key] = mergeTarget.TryGetValue(key, out var existing) ? Merge(existing, value, options) : value; + + if (trackOverflow && TryGetArrayIndex(key, out var idx) && idx > maxIndex) + maxIndex = idx; } + if (trackOverflow) + SetOverflowMaxIndex(mergeTarget, maxIndex); + return mergeTarget; } @@ -722,6 +805,79 @@ void AddOne(object? x) } } + internal static bool IsOverflow(object? obj) + { + return obj is not null && OverflowTable.TryGetValue(obj, out _); + } + + private static int GetOverflowMaxIndex(object obj) + { + return OverflowTable.TryGetValue(obj, out var state) ? state.MaxIndex : -1; + } + + private static void SetOverflowMaxIndex(object obj, int maxIndex) + { + OverflowTable.GetOrCreateValue(obj).MaxIndex = maxIndex; + } + + private static object MarkOverflow(object obj, int maxIndex) + { + SetOverflowMaxIndex(obj, maxIndex); + return obj; + } + + private static bool TryGetArrayIndex(object key, out int index) + { + switch (key) + { + case int i when i >= 0: + index = i; + return true; + case string s + when int.TryParse( + s, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var parsed + ) + && parsed >= 0 + && parsed.ToString(CultureInfo.InvariantCulture) == s: + index = parsed; + return true; + default: + index = -1; + return false; + } + } + + internal static object CombineWithLimit(object? a, object? b, DecodeOptions options) + { + if (options.ListLimit < 0) + return Combine(a, b); + + if (IsOverflow(a)) + { + var target = (IDictionary)a!; + var nextIndex = GetOverflowMaxIndex(target) + 1; + target[nextIndex.ToString(CultureInfo.InvariantCulture)] = b; + SetOverflowMaxIndex(target, nextIndex); + return target; + } + + var combined = Combine(a, b); + return combined.Count > options.ListLimit + ? MarkOverflow(ListToIndexMap(combined), combined.Count - 1) + : combined; + } + + private static Dictionary ListToIndexMap(List list) + { + var map = new Dictionary(list.Count); + for (var i = 0; i < list.Count; i++) + map[i.ToString(CultureInfo.InvariantCulture)] = list[i]; + return map; + } + /// /// Applies a function to a value or each element in an IEnumerable. /// From a885786b8990c62fa3b14f1f390d868efd4a29a8 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Jan 2026 23:25:27 +0000 Subject: [PATCH 2/5] fix: enhance overflow handling in Merge method to respect existing numeric keys --- QsNet.Tests/UtilsTests.cs | 24 ++++++++++++++++++++++++ QsNet/Internal/Utils.cs | 20 +++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/QsNet.Tests/UtilsTests.cs b/QsNet.Tests/UtilsTests.cs index 451ff29..e6e08f0 100644 --- a/QsNet.Tests/UtilsTests.cs +++ b/QsNet.Tests/UtilsTests.cs @@ -1380,6 +1380,30 @@ public void Merge_OverflowObjectIntoPrimitive_ShiftsIndices() Utils.IsOverflow(merged).Should().BeTrue(); } + [Fact] + public void Merge_OverflowRespectsExistingNumericKeys() + { + var options = new DecodeOptions { ListLimit = 1 }; + var overflow = Utils.CombineWithLimit(new List { "a" }, "b", options); + Utils.IsOverflow(overflow).Should().BeTrue(); + + var target = new Dictionary { ["5"] = "x" }; + var merged = Utils.Merge(target, overflow); + Utils.IsOverflow(merged).Should().BeTrue(); + + var appended = Utils.CombineWithLimit(merged, "c", options); + appended.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["5"] = "x", + ["6"] = "c" + } + ); + } + [Fact] public void InterpretNumericEntities_ReturnsInputUnchangedWhenThereAreNoEntities() { diff --git a/QsNet/Internal/Utils.cs b/QsNet/Internal/Utils.cs index f01195c..1f5444f 100644 --- a/QsNet/Internal/Utils.cs +++ b/QsNet/Internal/Utils.cs @@ -328,8 +328,13 @@ private static Regex MyRegex1() var trackOverflow = targetOverflow || sourceOverflow; var maxIndex = trackOverflow ? initialMaxIndex : -1; - if (trackOverflow && sourceOverflow) - maxIndex = Math.Max(maxIndex, GetOverflowMaxIndex(sourceMap)); + if (trackOverflow) + { + if (!targetOverflow) + maxIndex = Math.Max(maxIndex, GetMaxIndexFromMap(mergeTarget)); + if (sourceOverflow) + maxIndex = Math.Max(maxIndex, GetOverflowMaxIndex(sourceMap)); + } foreach (DictionaryEntry entry in sourceMap) { @@ -850,6 +855,15 @@ out var parsed } } + private static int GetMaxIndexFromMap(IDictionary map) + { + var maxIndex = -1; + foreach (DictionaryEntry entry in map) + if (TryGetArrayIndex(entry.Key, out var idx) && idx > maxIndex) + maxIndex = idx; + return maxIndex; + } + internal static object CombineWithLimit(object? a, object? b, DecodeOptions options) { if (options.ListLimit < 0) @@ -1402,4 +1416,4 @@ public int GetHashCode(object obj) { return RuntimeHelpers.GetHashCode(obj); } -} \ No newline at end of file +} From 8e54046a47fbfa39d25945294d1c7083bc642a3c Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Jan 2026 23:26:52 +0000 Subject: [PATCH 3/5] fix: update qs dependency version to ^6.14.1 --- QsNet.Comparison/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QsNet.Comparison/js/package.json b/QsNet.Comparison/js/package.json index bc38bc5..43215c2 100644 --- a/QsNet.Comparison/js/package.json +++ b/QsNet.Comparison/js/package.json @@ -5,6 +5,6 @@ "author": "Klemen Tusar", "license": "BSD-3-Clause", "dependencies": { - "qs": "^6.14.0" + "qs": "^6.14.1" } } From 6e03483ba50d1274ce5ea75349fda7c8b62fc7fd Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Jan 2026 23:41:30 +0000 Subject: [PATCH 4/5] fix: add tests for overflow handling in Merge method with integer keys --- QsNet.Tests/UtilsTests.cs | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/QsNet.Tests/UtilsTests.cs b/QsNet.Tests/UtilsTests.cs index e6e08f0..9beee35 100644 --- a/QsNet.Tests/UtilsTests.cs +++ b/QsNet.Tests/UtilsTests.cs @@ -1404,6 +1404,52 @@ public void Merge_OverflowRespectsExistingNumericKeys() ); } + [Fact] + public void Merge_OverflowTracksIntegerKeys() + { + var options = new DecodeOptions { ListLimit = 1 }; + var overflow = Utils.CombineWithLimit(new List { "a" }, "b", options); + + var target = new Dictionary { [5] = "x" }; + var merged = Utils.Merge(target, overflow); + Utils.IsOverflow(merged).Should().BeTrue(); + + var appended = Utils.CombineWithLimit(merged, "c", options); + appended.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + [5] = "x", + ["6"] = "c" + } + ); + } + + [Fact] + public void Merge_OverflowIgnoresNonCanonicalStringIndices() + { + var options = new DecodeOptions { ListLimit = 1 }; + var overflow = Utils.CombineWithLimit(new List { "a" }, "b", options); + + var target = new Dictionary { ["010"] = "x" }; + var merged = Utils.Merge(target, overflow); + Utils.IsOverflow(merged).Should().BeTrue(); + + var appended = Utils.CombineWithLimit(merged, "c", options); + appended.Should() + .BeEquivalentTo( + new Dictionary + { + ["0"] = "a", + ["1"] = "b", + ["010"] = "x", + ["2"] = "c" + } + ); + } + [Fact] public void InterpretNumericEntities_ReturnsInputUnchangedWhenThereAreNoEntities() { From 26db1f9caa8ee1464820cf8c997b2d58c8bcd5e9 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Jan 2026 23:44:04 +0000 Subject: [PATCH 5/5] fix: refactor Decode test for improved readability and maintainability --- QsNet.Tests/DecodeTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/QsNet.Tests/DecodeTests.cs b/QsNet.Tests/DecodeTests.cs index 4a0b2d8..16bbe91 100644 --- a/QsNet.Tests/DecodeTests.cs +++ b/QsNet.Tests/DecodeTests.cs @@ -1446,7 +1446,8 @@ public void Decode_ParsesBuffersCorrectly() [Fact] public void Decode_ParsesJqueryParamStrings() { - const string encoded = "filter%5B0%5D%5B%5D=int1&filter%5B0%5D%5B%5D=%3D&filter%5B0%5D%5B%5D=77&filter%5B%5D=and&filter%5B2%5D%5B%5D=int2&filter%5B2%5D%5B%5D=%3D&filter%5B2%5D%5B%5D=8"; + const string encoded = + "filter%5B0%5D%5B%5D=int1&filter%5B0%5D%5B%5D=%3D&filter%5B0%5D%5B%5D=77&filter%5B%5D=and&filter%5B2%5D%5B%5D=int2&filter%5B2%5D%5B%5D=%3D&filter%5B2%5D%5B%5D=8"; var expected = new Dictionary { ["filter"] = new List @@ -2585,8 +2586,8 @@ public void Decode_ListLimit_RespectsImplicitArrayLimit() var result = Qs.Decode(attack, new DecodeOptions { ListLimit = 100 }); result.Should().ContainKey("a"); - result["a"].Should().BeAssignableTo(); - ((IDictionary)result["a"]!).Count.Should().Be(105); + result["a"].Should().BeAssignableTo() + .Which.Count.Should().Be(105); } [Fact] @@ -4936,4 +4937,4 @@ public void Decode_CommaSplit_ThrowsWhenSumExceedsLimitAndThrowOn() private static partial Regex MyRegex(); #endregion -} +} \ No newline at end of file