diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 937e3f7181..0d193b4e61 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -625,7 +625,7 @@ namespace Microsoft.Net.Http.Headers { input = RemoveQuotes(input); - // First pass to calculate the size of the InplaceStringBuilder + // First pass to calculate the size of the string var backSlashCount = CountBackslashesForDecodingQuotedString(input); if (backSlashCount == 0) @@ -633,26 +633,32 @@ namespace Microsoft.Net.Http.Headers return input; } - var stringBuilder = new InplaceStringBuilder(input.Length - backSlashCount); - - for (var i = 0; i < input.Length; i++) + return string.Create(input.Length - backSlashCount, input, (span, segment) => { - if (i < input.Length - 1 && input[i] == '\\') + var spanIndex = 0; + var spanLength = span.Length; + for (var i = 0; i < segment.Length && (uint)spanIndex < (uint)spanLength; i++) { - // If there is an backslash character as the last character in the string, - // we will assume that it should be included literally in the unescaped string - // Ex: "hello\\" => "hello\\" - // Also, if a sender adds a quoted pair like '\\''n', - // we will assume it is over escaping and just add a n to the string. - // Ex: "he\\llo" => "hello" - stringBuilder.Append(input[i + 1]); - i++; - continue; - } - stringBuilder.Append(input[i]); - } + int nextIndex = i + 1; + if ((uint)nextIndex < (uint)segment.Length && segment[i] == '\\') + { + // If there is an backslash character as the last character in the string, + // we will assume that it should be included literally in the unescaped string + // Ex: "hello\\" => "hello\\" + // Also, if a sender adds a quoted pair like '\\''n', + // we will assume it is over escaping and just add a n to the string. + // Ex: "he\\llo" => "hello" + span[spanIndex] = segment[nextIndex]; + i++; + } + else + { + span[spanIndex] = segment[i]; + } - return stringBuilder.ToString(); + spanIndex++; + } + }); } private static int CountBackslashesForDecodingQuotedString(StringSegment input) @@ -696,25 +702,27 @@ namespace Microsoft.Net.Http.Headers // By calling this, we know that the string requires quotes around it to be a valid token. var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input); - var stringBuilder = new InplaceStringBuilder(input.Length + backSlashCount + 2); // 2 for quotes - stringBuilder.Append('\"'); + // 2 for quotes + return string.Create(input.Length + backSlashCount + 2, input, (span, segment) => { + // Helps to elide the bounds check for span[0] + span[span.Length - 1] = span[0] = '\"'; - for (var i = 0; i < input.Length; i++) - { - if (input[i] == '\\' || input[i] == '\"') + var spanIndex = 1; + for (var i = 0; i < segment.Length; i++) { - stringBuilder.Append('\\'); + if (segment[i] == '\\' || segment[i] == '\"') + { + span[spanIndex++] = '\\'; + } + else if ((segment[i] <= 0x1F || segment[i] == 0x7F) && segment[i] != 0x09) + { + // Control characters are not allowed in a quoted-string, which include all characters + // below 0x1F (except for 0x09 (TAB)) and 0x7F. + throw new FormatException($"Invalid control character '{segment[i]}' in input."); + } + span[spanIndex++] = segment[i]; } - else if ((input[i] <= 0x1F || input[i] == 0x7F) && input[i] != 0x09) - { - // Control characters are not allowed in a quoted-string, which include all characters - // below 0x1F (except for 0x09 (TAB)) and 0x7F. - throw new FormatException($"Invalid control character '{input[i]}' in input."); - } - stringBuilder.Append(input[i]); - } - stringBuilder.Append('\"'); - return stringBuilder.ToString(); + }); } private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input) diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs index 979844613e..8529593486 100644 --- a/src/Http/Headers/src/SetCookieHeaderValue.cs +++ b/src/Http/Headers/src/SetCookieHeaderValue.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Text; using Microsoft.Extensions.Primitives; @@ -24,7 +25,8 @@ namespace Microsoft.Net.Http.Headers private const string HttpOnlyToken = "httponly"; private const string SeparatorToken = "; "; private const string EqualsToken = "="; - private const string DefaultPath = "/"; // TODO: Used? + private const int ExpiresDateLength = 29; + private const string ExpiresDateFormat = "r"; private static readonly HttpHeaderParser SingleValueParser = new GenericHeaderParser(false, GetSetCookieLength); @@ -99,14 +101,11 @@ namespace Microsoft.Net.Http.Headers { var length = _name.Length + EqualsToken.Length + _value.Length; - string expires = null; string maxAge = null; - string sameSite = null; if (Expires.HasValue) { - expires = HeaderUtilities.FormatDate(Expires.GetValueOrDefault()); - length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length; + length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + ExpiresDateLength; } if (MaxAge.HasValue) @@ -132,7 +131,7 @@ namespace Microsoft.Net.Http.Headers if (SameSite != SameSiteMode.None) { - sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken; + var sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken; length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; } @@ -141,61 +140,75 @@ namespace Microsoft.Net.Http.Headers length += SeparatorToken.Length + HttpOnlyToken.Length; } - var sb = new InplaceStringBuilder(length); - - sb.Append(_name); - sb.Append(EqualsToken); - sb.Append(_value); - - if (expires != null) + return string.Create(length, (this, maxAge), (span, tuple) => { - AppendSegment(ref sb, ExpiresToken, expires); - } + var (headerValue, maxAgeValue) = tuple; - if (maxAge != null) - { - AppendSegment(ref sb, MaxAgeToken, maxAge); - } + Append(ref span, headerValue._name); + Append(ref span, EqualsToken); + Append(ref span, headerValue._value); - if (Domain != null) - { - AppendSegment(ref sb, DomainToken, Domain); - } + if (headerValue.Expires is DateTimeOffset expiresValue) + { + Append(ref span, SeparatorToken); + Append(ref span, ExpiresToken); + Append(ref span, EqualsToken); - if (Path != null) - { - AppendSegment(ref sb, PathToken, Path); - } + var formatted = expiresValue.TryFormat(span, out var charsWritten, ExpiresDateFormat); + span = span.Slice(charsWritten); - if (Secure) - { - AppendSegment(ref sb, SecureToken, null); - } + Debug.Assert(formatted); + } - if (SameSite != SameSiteMode.None) - { - AppendSegment(ref sb, SameSiteToken, sameSite); - } + if (maxAgeValue != null) + { + AppendSegment(ref span, MaxAgeToken, maxAgeValue); + } - if (HttpOnly) - { - AppendSegment(ref sb, HttpOnlyToken, null); - } + if (headerValue.Domain != null) + { + AppendSegment(ref span, DomainToken, headerValue.Domain); + } - return sb.ToString(); + if (headerValue.Path != null) + { + AppendSegment(ref span, PathToken, headerValue.Path); + } + + if (headerValue.Secure) + { + AppendSegment(ref span, SecureToken, null); + } + + if (headerValue.SameSite != SameSiteMode.None) + { + AppendSegment(ref span, SameSiteToken, headerValue.SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken); + } + + if (headerValue.HttpOnly) + { + AppendSegment(ref span, HttpOnlyToken, null); + } + }); } - private static void AppendSegment(ref InplaceStringBuilder builder, StringSegment name, StringSegment value) + private static void AppendSegment(ref Span span, StringSegment name, StringSegment value) { - builder.Append(SeparatorToken); - builder.Append(name); + Append(ref span, SeparatorToken); + Append(ref span, name.AsSpan()); if (value != null) { - builder.Append(EqualsToken); - builder.Append(value); + Append(ref span, EqualsToken); + Append(ref span, value.AsSpan()); } } + private static void Append(ref Span span, ReadOnlySpan other) + { + other.CopyTo(span); + span = span.Slice(other.Length); + } + /// /// Append string representation of this to given /// . @@ -452,14 +465,14 @@ namespace Microsoft.Net.Http.Headers result.HttpOnly = true; } // extension-av = - else - { + else + { // TODO: skiping it for now to avoid parsing failure? Store it in a list? // = (no spaces) if (!ReadEqualsSign(input, ref offset)) { return 0; - } + } ReadToSemicolonOrEnd(input, ref offset); } } diff --git a/src/Http/Http/perf/HeaderUtilitiesBenchmark.cs b/src/Http/Http/perf/HeaderUtilitiesBenchmark.cs new file mode 100644 index 0000000000..566ae6e987 --- /dev/null +++ b/src/Http/Http/perf/HeaderUtilitiesBenchmark.cs @@ -0,0 +1,24 @@ +// 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 BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + public class HeaderUtilitiesBenchmark + { + [Benchmark] + public StringSegment UnescapeAsQuotedString() + { + return HeaderUtilities.UnescapeAsQuotedString("\"hello\\\"foo\\\\bar\\\\baz\\\\\""); + } + + [Benchmark] + public StringSegment EscapeAsQuotedString() + { + return HeaderUtilities.EscapeAsQuotedString("\"hello\\\"foo\\\\bar\\\\baz\\\\\""); + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/ViewPath.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/ViewPath.cs index 640dc69432..652d74229c 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/ViewPath.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/ViewPath.cs @@ -1,8 +1,6 @@ // 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 Microsoft.Extensions.Primitives; - namespace Microsoft.AspNetCore.Mvc.Razor { internal static class ViewPath @@ -23,23 +21,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor length++; } - var builder = new InplaceStringBuilder(length); - if (addLeadingSlash) + return string.Create(length, (path, addLeadingSlash), (span, tuple) => { - builder.Append('/'); - } + var (pathValue, addLeadingSlashValue) = tuple; + var spanIndex = 0; - for (var i = 0; i < path.Length; i++) - { - var ch = path[i]; - if (ch == '\\') + if (addLeadingSlashValue) { - ch = '/'; + span[spanIndex++] = '/'; } - builder.Append(ch); - } - return builder.ToString(); + foreach (var ch in pathValue) + { + span[spanIndex++] = ch == '\\' ? '/' : ch; + } + }); } } } diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs index 603a0225dc..b2650063b7 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs @@ -144,12 +144,18 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Debug.Assert(!string.IsNullOrEmpty(viewEnginePath)); Debug.Assert(viewEnginePath.StartsWith("/", StringComparison.Ordinal)); - var builder = new InplaceStringBuilder(1 + areaName.Length + viewEnginePath.Length); - builder.Append('/'); - builder.Append(areaName); - builder.Append(viewEnginePath); + return string.Create(1 + areaName.Length + viewEnginePath.Length, (areaName, viewEnginePath), (span, tuple) => + { + var (areaNameValue, viewEnginePathValue) = tuple; - return builder.ToString(); + span[0] = '/'; + span = span.Slice(1); + + areaNameValue.AsSpan().CopyTo(span); + span = span.Slice(areaNameValue.Length); + + viewEnginePathValue.AsSpan().CopyTo(span); + }); } private static SelectorModel CreateSelectorModel(string prefix, string routeTemplate)