diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs index 7ceb8728..b7a2253a 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -321,7 +321,7 @@ private void SetDate(string parameter, DateTimeOffset? date) else { // Must always be quoted - var dateString = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", HttpRuleParser.DateToString(date.Value)); + var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true); if (dateParameter != null) { dateParameter.Value = dateString; diff --git a/src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs b/src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs new file mode 100644 index 00000000..06893155 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal static class DateTimeFormatter + { + private static readonly DateTimeFormatInfo FormatInfo = CultureInfo.InvariantCulture.DateTimeFormat; + + private static readonly string[] MonthNames = FormatInfo.AbbreviatedMonthNames; + private static readonly string[] DayNames = FormatInfo.AbbreviatedDayNames; + + private static readonly int Rfc1123DateLength = "ddd, dd MMM yyyy HH:mm:ss GMT".Length; + private static readonly int QuotedRfc1123DateLength = Rfc1123DateLength + 2; + + // ASCII numbers are in the range 48 - 57. + private const int AsciiNumberOffset = 0x30; + + private const string Gmt = "GMT"; + private const char Comma = ','; + private const char Space = ' '; + private const char Colon = ':'; + private const char Quote = '"'; + + public static string ToRfc1123String(this DateTimeOffset dateTime) + { + return ToRfc1123String(dateTime, false); + } + + public static string ToRfc1123String(this DateTimeOffset dateTime, bool quoted) + { + var universal = dateTime.UtcDateTime; + + var length = quoted ? QuotedRfc1123DateLength : Rfc1123DateLength; + var target = new InplaceStringBuilder(length); + + if (quoted) + { + target.Append(Quote); + } + + target.Append(DayNames[(int)universal.DayOfWeek]); + target.Append(Comma); + target.Append(Space); + AppendNumber(ref target, universal.Day); + target.Append(Space); + target.Append(MonthNames[universal.Month - 1]); + target.Append(Space); + AppendYear(ref target, universal.Year); + target.Append(Space); + AppendTimeOfDay(ref target, universal.TimeOfDay); + target.Append(Space); + target.Append(Gmt); + + if (quoted) + { + target.Append(Quote); + } + + return target.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendYear(ref InplaceStringBuilder target, int year) + { + target.Append(GetAsciiChar(year / 1000)); + target.Append(GetAsciiChar(year % 1000 / 100)); + target.Append(GetAsciiChar(year % 100 / 10)); + target.Append(GetAsciiChar(year % 10)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendTimeOfDay(ref InplaceStringBuilder target, TimeSpan timeOfDay) + { + AppendNumber(ref target, timeOfDay.Hours); + target.Append(Colon); + AppendNumber(ref target, timeOfDay.Minutes); + target.Append(Colon); + AppendNumber(ref target, timeOfDay.Seconds); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendNumber(ref InplaceStringBuilder target, int number) + { + target.Append(GetAsciiChar(number / 10)); + target.Append(GetAsciiChar(number % 10)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static char GetAsciiChar(int value) + { + return (char)(AsciiNumberOffset + value); + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs index eee2ea9c..786b54dd 100644 --- a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -220,7 +220,12 @@ public static bool TryParseDate(string input, out DateTimeOffset result) public static string FormatDate(DateTimeOffset dateTime) { - return HttpRuleParser.DateToString(dateTime); + return FormatDate(dateTime, false); + } + + public static string FormatDate(DateTimeOffset dateTime, bool quoted) + { + return dateTime.ToRfc1123String(quoted); } public static string RemoveQuotes(string input) diff --git a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs index e30465bd..63783599 100644 --- a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs +++ b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs @@ -233,12 +233,6 @@ internal static HttpParseResult GetQuotedPairLength(string input, int startIndex return HttpParseResult.Parsed; } - internal static string DateToString(DateTimeOffset dateTime) - { - // Format according to RFC1123; 'r' uses invariant info (DateTimeFormatInfo.InvariantInfo) - return dateTime.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture); - } - internal static bool TryStringToDate(string input, out DateTimeOffset result) { // Try the various date formats in the order listed above. diff --git a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs index c564722d..4960e4be 100644 --- a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs @@ -53,7 +53,7 @@ public override string ToString() { if (_entityTag == null) { - return HttpRuleParser.DateToString(_lastModified.Value); + return HeaderUtilities.FormatDate(_lastModified.Value); } return _entityTag.ToString(); } diff --git a/src/Microsoft.Net.Http.Headers/project.json b/src/Microsoft.Net.Http.Headers/project.json index 4f3a4ed8..703cf515 100644 --- a/src/Microsoft.Net.Http.Headers/project.json +++ b/src/Microsoft.Net.Http.Headers/project.json @@ -19,14 +19,12 @@ "xmlDoc": true }, "dependencies": { - "NETStandard.Library": "1.6.1-*" + "Microsoft.Extensions.Primitives": "1.1.0-*", + "NETStandard.Library": "1.6.1-*", + "System.Buffers": "4.3.0-*", + "System.Diagnostics.Contracts": "4.3.0-*" }, "frameworks": { - "netstandard1.1": { - "dependencies": { - "System.Buffers": "4.3.0-*", - "System.Diagnostics.Contracts": "4.3.0-*" - } - } + "netstandard1.1": {} } } \ No newline at end of file diff --git a/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs b/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs new file mode 100644 index 00000000..ccae6e57 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public static class HeaderUtilitiesTest + { + private const string Rfc1123Format = "r"; + + [Theory] + [MemberData(nameof(TestValues))] + public static void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) + { + var formatted = dateTime.ToString(Rfc1123Format); + var expected = quoted ? $"\"{formatted}\"" : formatted; + var actual = HeaderUtilities.FormatDate(dateTime, quoted); + + Assert.Equal(expected, actual); + } + + public static TheoryData TestValues + { + get + { + var data = new TheoryData(); + + var now = DateTimeOffset.Now; + + foreach (var quoted in new[] { true, false }) + { + for (var i = 0; i < 60; i++) + { + data.Add(now.AddSeconds(i), quoted); + data.Add(now.AddMinutes(i), quoted); + data.Add(now.AddDays(i), quoted); + data.Add(now.AddMonths(i), quoted); + data.Add(now.AddYears(i), quoted); + } + } + + return data; + } + } + } +}