Remove InplaceStringBuilder usages (#6163)

This commit is contained in:
Pavel Krymets 2019-01-02 15:50:19 -08:00 committed by GitHub
parent 3477daf3c4
commit 0ae6cc8e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 101 deletions

View File

@ -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)

View File

@ -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<SetCookieHeaderValue> SingleValueParser
= new GenericHeaderParser<SetCookieHeaderValue>(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<char> 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<char> span, ReadOnlySpan<char> other)
{
other.CopyTo(span);
span = span.Slice(other.Length);
}
/// <summary>
/// Append string representation of this <see cref="SetCookieHeaderValue"/> to given
/// <paramref name="builder"/>.
@ -452,14 +465,14 @@ namespace Microsoft.Net.Http.Headers
result.HttpOnly = true;
}
// extension-av = <any CHAR except CTLs or ";">
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);
}
}

View File

@ -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\\\\\"");
}
}
}

View File

@ -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;
}
});
}
}
}

View File

@ -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)