From 7440d5d29c3814d94c83cdbbea04ee8179e03e64 Mon Sep 17 00:00:00 2001 From: Yves57 Date: Fri, 17 Feb 2017 22:44:38 +0100 Subject: [PATCH] Remove allocations and improve Header Quality Values parsing performance --- .../HeaderUtilities.cs | 95 ++++++++++++++++++- .../HttpHeaderParser.cs | 11 ++- .../StringWithQualityHeaderValue.cs | 16 +--- .../StringWithQualityHeaderValueTest.cs | 7 ++ 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs index c6580a56fc..64cb5d5705 100644 --- a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -13,6 +13,7 @@ namespace Microsoft.Net.Http.Headers public static class HeaderUtilities { private static readonly int _int64MaxStringLength = 19; + private static readonly int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1 private const string QualityName = "q"; internal const string BytesUnit = "bytes"; @@ -62,9 +63,8 @@ namespace Microsoft.Net.Http.Headers { // Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal // separator is considered invalid (even if the current culture would allow it). - double qualityValue; - if (double.TryParse(qualityParameter.Value, NumberStyles.AllowDecimalPoint, - NumberFormatInfo.InvariantInfo, out qualityValue)) + if (TryParseQualityDouble(qualityParameter.Value, 0, out var qualityValue, out var length)) + { return qualityValue; } @@ -480,6 +480,95 @@ namespace Microsoft.Net.Http.Headers } } + // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) + // See https://tools.ietf.org/html/rfc7231#section-5.3.1 + // Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails). + internal static bool TryParseQualityDouble(string input, int startIndex, out double quality, out int length) + { + quality = 0; + length = 0; + + var inputLength = input.Length; + var current = startIndex; + var limit = startIndex + _qualityValueMaxCharCount; + + var intPart = 0; + var decPart = 0; + var decPow = 1; + + if (current >= inputLength) + { + return false; + } + + var ch = input[current]; + + if (ch >= '0' && ch <= '1') // Only values between 0 and 1 are accepted, according to RFC + { + intPart = ch - '0'; + current++; + } + else + { + // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the + // form "0.123". + return false; + } + + if (current < inputLength) + { + ch = input[current]; + + if (ch >= '0' && ch <= '9') + { + // The RFC accepts only one digit before the dot + return false; + } + + if (ch == '.') + { + current++; + + while (current < inputLength) + { + ch = input[current]; + if (ch >= '0' && ch <= '9') + { + if (current >= limit) + { + return false; + } + + decPart = decPart * 10 + ch - '0'; + decPow *= 10; + current++; + } + else + { + break; + } + } + } + } + + if (decPart != 0) + { + quality = intPart + decPart / (double)decPow; + } + else + { + quality = intPart; + } + + if (quality < 0 || quality > 1) + { + return false; + } + + length = current - startIndex; + return true; + } + /// /// Converts the non-negative 64-bit numeric value to its equivalent string representation. /// diff --git a/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs b/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs index 4fe977ff19..383a304dcf 100644 --- a/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs +++ b/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs @@ -61,13 +61,14 @@ namespace Microsoft.Net.Http.Headers // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller // can ignore the value. parsedValues = null; - var results = new List(); + List results = null; if (values == null) { return false; } - foreach (var value in values) + for (var i = 0; i < values.Count; i++) { + var value = values[i]; int index = 0; while (!string.IsNullOrEmpty(value) && index < value.Length) @@ -78,6 +79,10 @@ namespace Microsoft.Net.Http.Headers // The entry may not contain an actual value, like " , " if (output != null) { + if (results == null) + { + results = new List(); // Allocate it only when used + } results.Add(output); } } @@ -92,7 +97,7 @@ namespace Microsoft.Net.Http.Headers } } } - if (results.Count > 0) + if (results != null) { parsedValues = results; return true; diff --git a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs index 0521c1a000..13ac4fb15c 100644 --- a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs @@ -204,21 +204,7 @@ namespace Microsoft.Net.Http.Headers return false; } - int qualityLength = HttpRuleParser.GetNumberLength(input, current, true); - - if (qualityLength == 0) - { - return false; - } - - double quality; - if (!double.TryParse(input.Substring(current, qualityLength), NumberStyles.AllowDecimalPoint, - NumberFormatInfo.InvariantInfo, out quality)) - { - return false; - } - - if ((quality < 0) || (quality > 1)) + if (!HeaderUtilities.TryParseQualityDouble(input, current, out var quality, out var qualityLength)) { return false; } diff --git a/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs index 2771614c2f..49ee58b93e 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs @@ -121,6 +121,8 @@ namespace Microsoft.Net.Http.Headers CheckValidParse(" t", new StringWithQualityHeaderValue("t")); CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0)); CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1)); + CheckValidParse("t;q=1.000", new StringWithQualityHeaderValue("t", 1)); + CheckValidParse("t;q=0.12345678", new StringWithQualityHeaderValue("t", 0.12345678)); CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0)); CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5")); CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8)); @@ -154,6 +156,11 @@ namespace Microsoft.Net.Http.Headers [InlineData("t;q=a")] [InlineData("t;qa")] [InlineData("t;q1")] + [InlineData("integer_part_too_long;q=01")] + [InlineData("integer_part_too_long;q=01.0")] + [InlineData("decimal_part_too_long;q=0.123456789")] + [InlineData("decimal_part_too_long;q=0.123456789 ")] + [InlineData("no_integer_part;q=.1")] public void Parse_SetOfInvalidValueStrings_Throws(string input) { Assert.Throws(() => StringWithQualityHeaderValue.Parse(input));