diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs b/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs index 4b4b24f7..3ac23d9c 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs @@ -98,7 +98,7 @@ public int? Port { return null; } - + return p; } } diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs index 7cac98cf..fb6af992 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs @@ -68,10 +68,10 @@ public static void SetHeaderJoined(IHeaderDictionary headers, string key, String // Quote items that contain comas and are not already quoted. private static string QuoteIfNeeded(string value) { - if (!string.IsNullOrWhiteSpace(value) && - value.Contains(',') && + if (!string.IsNullOrWhiteSpace(value) && + value.Contains(',') && (value[0] != '"' || value[value.Length - 1] != '"')) - { + { return $"\"{value}\""; } return value; @@ -79,7 +79,7 @@ private static string QuoteIfNeeded(string value) private static string DeQuote(string value) { - if (!string.IsNullOrWhiteSpace(value) && + if (!string.IsNullOrWhiteSpace(value) && (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')) { value = value.Substring(1, value.Length - 2); diff --git a/src/Microsoft.AspNetCore.Http/Internal/ParsingHelpers.cs b/src/Microsoft.AspNetCore.Http/Internal/ParsingHelpers.cs index 5e406d85..57b1a7a9 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/ParsingHelpers.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/ParsingHelpers.cs @@ -411,12 +411,11 @@ private static string DeQuote(string value) throw new ArgumentNullException(nameof(headers)); } - const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite; long value; var rawValue = headers[HeaderNames.ContentLength]; if (rawValue.Count == 1 && !string.IsNullOrWhiteSpace(rawValue[0]) && - long.TryParse(rawValue[0], styles, CultureInfo.InvariantCulture, out value)) + HeaderUtilities.TryParseInt64(new StringSegment(rawValue[0]).Trim(), out value)) { return value; } @@ -433,7 +432,7 @@ public static void SetContentLength(IHeaderDictionary headers, long? value) if (value.HasValue) { - headers[HeaderNames.ContentLength] = value.Value.ToString(CultureInfo.InvariantCulture); + headers[HeaderNames.ContentLength] = HeaderUtilities.FormatInt64(value.Value); } else { diff --git a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs index 7ad65ac3..c0d28bdc 100644 --- a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs @@ -11,18 +11,18 @@ namespace Microsoft.Net.Http.Headers { public class CacheControlHeaderValue { - private const string MaxAgeString = "max-age"; - private const string MaxStaleString = "max-stale"; - private const string MinFreshString = "min-fresh"; - private const string MustRevalidateString = "must-revalidate"; - private const string NoCacheString = "no-cache"; - private const string NoStoreString = "no-store"; - private const string NoTransformString = "no-transform"; - private const string OnlyIfCachedString = "only-if-cached"; - private const string PrivateString = "private"; - private const string ProxyRevalidateString = "proxy-revalidate"; - private const string PublicString = "public"; - private const string SharedMaxAgeString = "s-maxage"; + public static readonly string PublicString = "public"; + public static readonly string PrivateString = "private"; + public static readonly string MaxAgeString = "max-age"; + public static readonly string SharedMaxAgeString = "s-maxage"; + public static readonly string NoCacheString = "no-cache"; + public static readonly string NoStoreString = "no-store"; + public static readonly string MaxStaleString = "max-stale"; + public static readonly string MinFreshString = "min-fresh"; + public static readonly string NoTransformString = "no-transform"; + public static readonly string OnlyIfCachedString = "only-if-cached"; + public static readonly string MustRevalidateString = "must-revalidate"; + public static readonly string ProxyRevalidateString = "proxy-revalidate"; // The Cache-Control header is special: It is a header supporting a list of values, but we represent the list // as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is @@ -394,63 +394,120 @@ private static bool TrySetCacheControlValues( CacheControlHeaderValue cc, List nameValueList) { - foreach (NameValueHeaderValue nameValue in nameValueList) + for (var i = 0; i < nameValueList.Count; i++) { + var nameValue = nameValueList[i]; + var name = nameValue.Name; var success = true; - string name = nameValue.Name.ToLowerInvariant(); - switch (name) + switch (name.Length) { - case NoCacheString: - success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders); - break; - - case NoStoreString: - success = TrySetTokenOnlyValue(nameValue, ref cc._noStore); - break; - - case MaxAgeString: - success = TrySetTimeSpan(nameValue, ref cc._maxAge); - break; - - case MaxStaleString: - success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit)); - if (success) + case 6: + if (string.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase)) { - cc._maxStale = true; + success = TrySetTokenOnlyValue(nameValue, ref cc._public); + } + else + { + goto default; } break; - case MinFreshString: - success = TrySetTimeSpan(nameValue, ref cc._minFresh); - break; - - case NoTransformString: - success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform); + case 7: + if (string.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._maxAge); + } + else if(string.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders); + } + else + { + goto default; + } break; - case OnlyIfCachedString: - success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached); + case 8: + if (string.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders); + } + else if (string.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._noStore); + } + else if (string.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge); + } + else + { + goto default; + } break; - case PublicString: - success = TrySetTokenOnlyValue(nameValue, ref cc._public); + case 9: + if (string.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase)) + { + success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit)); + if (success) + { + cc._maxStale = true; + } + } + else if (string.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._minFresh); + } + else + { + goto default; + } break; - case PrivateString: - success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders); + case 12: + if (string.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform); + } + else + { + goto default; + } break; - case MustRevalidateString: - success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate); + case 14: + if (string.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached); + } + else + { + goto default; + } break; - case ProxyRevalidateString: - success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate); + case 15: + if (string.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate); + } + else + { + goto default; + } break; - case SharedMaxAgeString: - success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge); + case 16: + if (string.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate); + } + else + { + goto default; + } break; default: diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs index b7a2253a..e3542480 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -104,13 +104,13 @@ public long? Size get { var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); - ulong value; + long value; if (sizeParameter != null) { - string sizeString = sizeParameter.Value; - if (UInt64.TryParse(sizeString, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + var sizeString = sizeParameter.Value; + if (HeaderUtilities.TryParseInt64(sizeString, out value)) { - return (long)value; + return value; } } return null; diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs index 786b54dd..4dd1081b 100644 --- a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -5,11 +5,13 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { public static class HeaderUtilities { + private static readonly int _int64MaxStringLength = 20; private const string QualityName = "q"; internal const string BytesUnit = "bytes"; @@ -198,19 +200,322 @@ internal static int GetNextNonEmptyOrWhitespaceIndex( return current; } + /// + /// Try to find a target header value among the set of given header values and parse it as a + /// . + /// + /// + /// The containing the set of header values to search. + /// + /// + /// The target header value to look for. + /// + /// + /// When this method returns, contains the parsed , if the parsing succeeded, or + /// null if the parsing failed. The conversion fails if the was not + /// found or could not be parsed as a . This parameter is passed uninitialized; + /// any value originally supplied in result will be overwritten. + /// + /// + /// true if is found and successfully parsed; otherwise, + /// false. + /// + // e.g. { "headerValue=10, targetHeaderValue=30" } + public static bool TryParseSeconds(StringValues headerValues, string targetValue, out TimeSpan? value) + { + if (StringValues.IsNullOrEmpty(headerValues) || string.IsNullOrEmpty(targetValue)) + { + value = null; + return false; + } + + for (var i = 0; i < headerValues.Count; i++) + { + // Trim leading white space + var current = HttpRuleParser.GetWhitespaceLength(headerValues[i], 0); + + while (current < headerValues[i].Length) + { + long seconds; + var tokenLength = HttpRuleParser.GetTokenLength(headerValues[i], current); + if (tokenLength == targetValue.Length + && string.Compare(headerValues[i], current, targetValue, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0 + && TryParseInt64FromHeaderValue(current + tokenLength, headerValues[i], out seconds)) + { + // Token matches target value and seconds were parsed + value = TimeSpan.FromSeconds(seconds); + return true; + } + else + { + // Skip until the next potential name + current += tokenLength; + current += HttpRuleParser.GetWhitespaceLength(headerValues[i], current); + + // Skip the value if present + if (current < headerValues[i].Length && headerValues[i][current] == '=') + { + current++; // skip '=' + current += NameValueHeaderValue.GetValueLength(headerValues[i], current); + current += HttpRuleParser.GetWhitespaceLength(headerValues[i], current); + } + + // Skip the delimiter + if (current < headerValues[i].Length && headerValues[i][current] == ',') + { + current++; // skip ',' + current += HttpRuleParser.GetWhitespaceLength(headerValues[i], current); + } + } + } + } + value = null; + return false; + } + + /// + /// Check if a target directive exists among the set of given cache control directives. + /// + /// + /// The containing the set of cache control directives. + /// + /// + /// The target cache control directives to look for. + /// + /// + /// true if is contained in ; + /// otherwise, false. + /// + public static bool ContainsCacheDirective(StringValues cacheControlDirectives, string targetDirectives) + { + if (StringValues.IsNullOrEmpty(cacheControlDirectives) || string.IsNullOrEmpty(targetDirectives)) + { + return false; + } + + + for (var i = 0; i < cacheControlDirectives.Count; i++) + { + // Trim leading white space + var current = HttpRuleParser.GetWhitespaceLength(cacheControlDirectives[i], 0); + + while (current < cacheControlDirectives[i].Length) + { + var tokenLength = HttpRuleParser.GetTokenLength(cacheControlDirectives[i], current); + if (tokenLength == targetDirectives.Length + && string.Compare(cacheControlDirectives[i], current, targetDirectives, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0) + { + // Token matches target value + return true; + } + else + { + // Skip until the next potential name + current += tokenLength; + current += HttpRuleParser.GetWhitespaceLength(cacheControlDirectives[i], current); + + // Skip the value if present + if (current < cacheControlDirectives[i].Length && cacheControlDirectives[i][current] == '=') + { + current++; // skip '=' + current += NameValueHeaderValue.GetValueLength(cacheControlDirectives[i], current); + current += HttpRuleParser.GetWhitespaceLength(cacheControlDirectives[i], current); + } + + // Skip the delimiter + if (current < cacheControlDirectives[i].Length && cacheControlDirectives[i][current] == ',') + { + current++; // skip ',' + current += HttpRuleParser.GetWhitespaceLength(cacheControlDirectives[i], current); + } + } + } + } + + return false; + } + + private static unsafe bool TryParseInt64FromHeaderValue(int startIndex, string headerValue, out long result) + { + // Trim leading whitespace + startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + + // Match and skip '=', it also can't be the last character in the headerValue + if (startIndex >= headerValue.Length - 1 || headerValue[startIndex] != '=') + { + result = 0; + return false; + } + startIndex++; + + // Trim trailing whitespace + startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + + // Try parse the number + if (TryParseInt64(new StringSegment(headerValue, startIndex, HttpRuleParser.GetNumberLength(headerValue, startIndex, false)), out result)) + { + return true; + } + + result = 0; + return false; + } + internal static bool TryParseInt32(string value, out int result) { - return int.TryParse(value, NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + return TryParseInt32(new StringSegment(value), out result); } + /// + /// Try to convert a string representation of a positive number to its 64-bit signed integer equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// + /// + /// A string containing a number to convert. + /// + /// + /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained + /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if + /// the string is null or String.Empty, is not of the correct format, is negative, or represents a number + /// greater than Int64.MaxValue. This parameter is passed uninitialized; any value originally supplied in + /// result will be overwritten. + /// + /// true if parsing succeeded; otherwise, false. public static bool TryParseInt64(string value, out long result) { - return long.TryParse(value, NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + return TryParseInt64(new StringSegment(value), out result); + } + + internal static unsafe bool TryParseInt32(StringSegment value, out int result) + { + if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) + { + result = 0; + return false; + } + + result = 0; + fixed (char* ptr = value.Buffer) + { + var ch = (ushort*)ptr + value.Offset; + var end = ch + value.Length; + + ushort digit = 0; + while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) + { + // Check for overflow + if ((result = result * 10 + digit) < 0) + { + result = 0; + return false; + } + + ch++; + } + + if (ch != end) + { + result = 0; + return false; + } + return true; + } + } + + /// + /// Try to convert a representation of a positive number to its 64-bit signed + /// integer equivalent. A return value indicates whether the conversion succeeded or failed. + /// + /// + /// A containing a number to convert. + /// + /// + /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained + /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if + /// the is null or String.Empty, is not of the correct format, is negative, or + /// represents a number greater than Int64.MaxValue. This parameter is passed uninitialized; any value + /// originally supplied in result will be overwritten. + /// + /// true if parsing succeeded; otherwise, false. + public static unsafe bool TryParseInt64(StringSegment value, out long result) + { + if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) + { + result = 0; + return false; + } + + result = 0; + fixed (char* ptr = value.Buffer) + { + var ch = (ushort*)ptr + value.Offset; + var end = ch + value.Length; + + ushort digit = 0; + while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) + { + // Check for overflow + if ((result = result * 10 + digit) < 0) + { + result = 0; + return false; + } + + ch++; + } + + if (ch != end) + { + result = 0; + return false; + } + return true; + } } - public static string FormatInt64(long value) + /// + /// Converts the signed 64-bit numeric value to its equivalent string representation. + /// + /// + /// The number to convert. + /// + /// + /// The string representation of the value of this instance, consisting of a minus sign if the value is + /// negative, and a sequence of digits ranging from 0 to 9 with no leading zeroes. + /// + public unsafe static string FormatInt64(long value) { - return value.ToString(CultureInfo.InvariantCulture); + var position = _int64MaxStringLength; + var negative = false; + + if (value < 0) + { + // Not possible to compute absolute value of MinValue, return the exact string instead. + if (value == long.MinValue) + { + return "-9223372036854775808"; + } + negative = true; + value = -value; + } + + char* charBuffer = stackalloc char[_int64MaxStringLength]; + + do + { + // Consider using Math.DivRem() if available + var quotient = value / 10; + charBuffer[--position] = (char)(0x30 + (value - quotient * 10)); // 0x30 = '0' + value = quotient; + } + while (value != 0); + + if (negative) + { + charBuffer[--position] = '-'; + } + + return new string(charBuffer, position, _int64MaxStringLength - position); } public static bool TryParseDate(string input, out DateTimeOffset result) diff --git a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs index 63783599..835a2f35 100644 --- a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs +++ b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs @@ -233,18 +233,11 @@ internal static HttpParseResult GetQuotedPairLength(string input, int startIndex return HttpParseResult.Parsed; } - internal static bool TryStringToDate(string input, out DateTimeOffset result) - { - // Try the various date formats in the order listed above. - // We should accept a wide verity of common formats, but only output RFC 1123 style dates. - if (DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result)) - { - return true; - } - - return false; - } + // Try the various date formats in the order listed above. + // We should accept a wide verity of common formats, but only output RFC 1123 style dates. + internal static bool TryStringToDate(string input, out DateTimeOffset result) => + DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); // TEXT = // LWS = [CRLF] 1*( SP | HT ) diff --git a/src/Microsoft.Net.Http.Headers/Properties/AssemblyInfo.cs b/src/Microsoft.Net.Http.Headers/Properties/AssemblyInfo.cs index 76feceef..538f508d 100644 --- a/src/Microsoft.Net.Http.Headers/Properties/AssemblyInfo.cs +++ b/src/Microsoft.Net.Http.Headers/Properties/AssemblyInfo.cs @@ -3,9 +3,11 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; [assembly: AssemblyMetadata("Serviceable", "True")] [assembly: NeutralResourcesLanguage("en-us")] [assembly: AssemblyCompany("Microsoft Corporation.")] [assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] [assembly: AssemblyProduct("Microsoft ASP.NET Core")] +[assembly: InternalsVisibleTo("Microsoft.Net.Http.Headers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.Net.Http.Headers/project.json b/src/Microsoft.Net.Http.Headers/project.json index 2423b806..f6dbda4e 100644 --- a/src/Microsoft.Net.Http.Headers/project.json +++ b/src/Microsoft.Net.Http.Headers/project.json @@ -11,6 +11,7 @@ ] }, "buildOptions": { + "allowUnsafe": true, "warningsAsErrors": true, "keyFile": "../../tools/Key.snk", "nowarn": [ diff --git a/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs b/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs index ccae6e57..cde811a0 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs @@ -2,17 +2,19 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.Net.Http.Headers { - public static class HeaderUtilitiesTest + public class HeaderUtilitiesTest { private const string Rfc1123Format = "r"; [Theory] [MemberData(nameof(TestValues))] - public static void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) + public void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) { var formatted = dateTime.ToString(Rfc1123Format); var expected = quoted ? $"\"{formatted}\"" : formatted; @@ -44,5 +46,136 @@ public static TheoryData TestValues return data; } } + + [Theory] + [InlineData("h=1", "h", 1)] + [InlineData("directive1=3, directive2=10", "directive1", 3)] + [InlineData("directive1 =45, directive2=80", "directive1", 45)] + [InlineData("directive1= 89 , directive2=22", "directive1", 89)] + [InlineData("directive1= 89 , directive2= 42", "directive2", 42)] + [InlineData("directive1= 89 , directive= 42", "directive", 42)] + public void TryParseSeconds_Succeeds(string headerValues, string targetValue, int expectedValue) + { + TimeSpan? value; + Assert.True(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); + Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); + } + + [Theory] + [InlineData("", "")] + [InlineData(null, null)] + [InlineData("h=", "h")] + [InlineData("directive1=, directive2=10", "directive1")] + [InlineData("directive1 , directive2=80", "directive1")] + [InlineData("h=10", "directive")] + [InlineData("directive1", "directive")] + [InlineData("h=directive", "directive")] + [InlineData("directive1, directive2=80", "directive")] + public void TryParseSeconds_Fails(string headerValues, string targetValue) + { + TimeSpan? value; + Assert.False(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(-1)] + [InlineData(1234567890)] + [InlineData(-1234567890)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + [InlineData(long.MinValue + 1)] + public void FormatInt64_MatchesToString(long value) + { + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), HeaderUtilities.FormatInt64(value)); + } + + [Theory] + [InlineData("h", "h", true)] + [InlineData("h=", "h", true)] + [InlineData("h=1", "h", true)] + [InlineData("H", "h", true)] + [InlineData("H=", "h", true)] + [InlineData("H=1", "h", true)] + [InlineData("h", "H", true)] + [InlineData("h=", "H", true)] + [InlineData("h=1", "H", true)] + [InlineData("directive1, directive=10", "directive1", true)] + [InlineData("directive1=, directive=10", "directive1", true)] + [InlineData("directive1=3, directive=10", "directive1", true)] + [InlineData("directive1 , directive=80", "directive1", true)] + [InlineData(" directive1, directive=80", "directive1", true)] + [InlineData("directive1 =45, directive=80", "directive1", true)] + [InlineData("directive1= 89 , directive=22", "directive1", true)] + [InlineData("directive1, directive", "directive", true)] + [InlineData("directive1, directive=", "directive", true)] + [InlineData("directive1, directive=10", "directive", true)] + [InlineData("directive1=3, directive", "directive", true)] + [InlineData("directive1=3, directive=", "directive", true)] + [InlineData("directive1=3, directive=10", "directive", true)] + [InlineData("directive1= 89 , directive= 42", "directive", true)] + [InlineData("directive1= 89 , directive = 42", "directive", true)] + [InlineData(null, null, false)] + [InlineData(null, "", false)] + [InlineData("", null, false)] + [InlineData("", "", false)] + [InlineData("h=10", "directive", false)] + [InlineData("directive1", "directive", false)] + [InlineData("h=directive", "directive", false)] + [InlineData("directive1, directive2=80", "directive", false)] + public void ContainsCacheDirective_MatchesExactValue(string headerValues, string targetValue, bool contains) + { + Assert.Equal(contains, HeaderUtilities.ContainsCacheDirective(new StringValues(headerValues), targetValue)); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("-1")] + [InlineData("a")] + [InlineData("1.1")] + [InlineData("9223372036854775808")] // long.MaxValue + 1 + public void TryParseInt64_Fails(string valueString) + { + long value = 1; + Assert.False(HeaderUtilities.TryParseInt64(valueString, out value)); + Assert.Equal(0, value); + } + + [Theory] + [InlineData("0", 0)] + [InlineData("9223372036854775807", 9223372036854775807)] // long.MaxValue + public void TryParseInt64_Succeeds(string valueString, long expected) + { + long value = 1; + Assert.True(HeaderUtilities.TryParseInt64(valueString, out value)); + Assert.Equal(expected, value); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("-1")] + [InlineData("a")] + [InlineData("1.1")] + [InlineData("1,000")] + [InlineData("2147483648")] // int.MaxValue + 1 + public void TryParseInt32_Fails(string valueString) + { + int value = 1; + Assert.False(HeaderUtilities.TryParseInt32(valueString, out value)); + Assert.Equal(0, value); + } + + [Theory] + [InlineData("0", 0)] + [InlineData("2147483647", 2147483647)] // int.MaxValue + public void TryParseInt32_Succeeds(string valueString, long expected) + { + int value = 1; + Assert.True(HeaderUtilities.TryParseInt32(valueString, out value)); + Assert.Equal(expected, value); + } } } diff --git a/test/Microsoft.Net.Http.Headers.Tests/project.json b/test/Microsoft.Net.Http.Headers.Tests/project.json index 74f2ccc3..ca176824 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/project.json +++ b/test/Microsoft.Net.Http.Headers.Tests/project.json @@ -1,10 +1,13 @@ { - "version": "1.1.0-*", "dependencies": { "dotnet-test-xunit": "2.2.0-*", "Microsoft.Net.Http.Headers": "1.2.0-*", "xunit": "2.2.0-*" }, + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, "frameworks": { "netcoreapp1.1": { "dependencies": {