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); 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); var backSlashCount = CountBackslashesForDecodingQuotedString(input);
if (backSlashCount == 0) if (backSlashCount == 0)
@ -633,26 +633,32 @@ namespace Microsoft.Net.Http.Headers
return input; return input;
} }
var stringBuilder = new InplaceStringBuilder(input.Length - backSlashCount); return string.Create(input.Length - backSlashCount, input, (span, segment) =>
for (var i = 0; i < input.Length; i++)
{ {
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, int nextIndex = i + 1;
// we will assume that it should be included literally in the unescaped string if ((uint)nextIndex < (uint)segment.Length && segment[i] == '\\')
// Ex: "hello\\" => "hello\\" {
// Also, if a sender adds a quoted pair like '\\''n', // If there is an backslash character as the last character in the string,
// we will assume it is over escaping and just add a n to the string. // we will assume that it should be included literally in the unescaped string
// Ex: "he\\llo" => "hello" // Ex: "hello\\" => "hello\\"
stringBuilder.Append(input[i + 1]); // Also, if a sender adds a quoted pair like '\\''n',
i++; // we will assume it is over escaping and just add a n to the string.
continue; // Ex: "he\\llo" => "hello"
} span[spanIndex] = segment[nextIndex];
stringBuilder.Append(input[i]); i++;
} }
else
{
span[spanIndex] = segment[i];
}
return stringBuilder.ToString(); spanIndex++;
}
});
} }
private static int CountBackslashesForDecodingQuotedString(StringSegment input) 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. // By calling this, we know that the string requires quotes around it to be a valid token.
var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input); var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input);
var stringBuilder = new InplaceStringBuilder(input.Length + backSlashCount + 2); // 2 for quotes // 2 for quotes
stringBuilder.Append('\"'); 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++) var spanIndex = 1;
{ for (var i = 0; i < segment.Length; i++)
if (input[i] == '\\' || input[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) private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input)

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Text; using System.Text;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -24,7 +25,8 @@ namespace Microsoft.Net.Http.Headers
private const string HttpOnlyToken = "httponly"; private const string HttpOnlyToken = "httponly";
private const string SeparatorToken = "; "; private const string SeparatorToken = "; ";
private const string EqualsToken = "="; 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 private static readonly HttpHeaderParser<SetCookieHeaderValue> SingleValueParser
= new GenericHeaderParser<SetCookieHeaderValue>(false, GetSetCookieLength); = new GenericHeaderParser<SetCookieHeaderValue>(false, GetSetCookieLength);
@ -99,14 +101,11 @@ namespace Microsoft.Net.Http.Headers
{ {
var length = _name.Length + EqualsToken.Length + _value.Length; var length = _name.Length + EqualsToken.Length + _value.Length;
string expires = null;
string maxAge = null; string maxAge = null;
string sameSite = null;
if (Expires.HasValue) if (Expires.HasValue)
{ {
expires = HeaderUtilities.FormatDate(Expires.GetValueOrDefault()); length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + ExpiresDateLength;
length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length;
} }
if (MaxAge.HasValue) if (MaxAge.HasValue)
@ -132,7 +131,7 @@ namespace Microsoft.Net.Http.Headers
if (SameSite != SameSiteMode.None) 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; length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
} }
@ -141,61 +140,75 @@ namespace Microsoft.Net.Http.Headers
length += SeparatorToken.Length + HttpOnlyToken.Length; length += SeparatorToken.Length + HttpOnlyToken.Length;
} }
var sb = new InplaceStringBuilder(length); return string.Create(length, (this, maxAge), (span, tuple) =>
sb.Append(_name);
sb.Append(EqualsToken);
sb.Append(_value);
if (expires != null)
{ {
AppendSegment(ref sb, ExpiresToken, expires); var (headerValue, maxAgeValue) = tuple;
}
if (maxAge != null) Append(ref span, headerValue._name);
{ Append(ref span, EqualsToken);
AppendSegment(ref sb, MaxAgeToken, maxAge); Append(ref span, headerValue._value);
}
if (Domain != null) if (headerValue.Expires is DateTimeOffset expiresValue)
{ {
AppendSegment(ref sb, DomainToken, Domain); Append(ref span, SeparatorToken);
} Append(ref span, ExpiresToken);
Append(ref span, EqualsToken);
if (Path != null) var formatted = expiresValue.TryFormat(span, out var charsWritten, ExpiresDateFormat);
{ span = span.Slice(charsWritten);
AppendSegment(ref sb, PathToken, Path);
}
if (Secure) Debug.Assert(formatted);
{ }
AppendSegment(ref sb, SecureToken, null);
}
if (SameSite != SameSiteMode.None) if (maxAgeValue != null)
{ {
AppendSegment(ref sb, SameSiteToken, sameSite); AppendSegment(ref span, MaxAgeToken, maxAgeValue);
} }
if (HttpOnly) if (headerValue.Domain != null)
{ {
AppendSegment(ref sb, HttpOnlyToken, 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); Append(ref span, SeparatorToken);
builder.Append(name); Append(ref span, name.AsSpan());
if (value != null) if (value != null)
{ {
builder.Append(EqualsToken); Append(ref span, EqualsToken);
builder.Append(value); 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> /// <summary>
/// Append string representation of this <see cref="SetCookieHeaderValue"/> to given /// Append string representation of this <see cref="SetCookieHeaderValue"/> to given
/// <paramref name="builder"/>. /// <paramref name="builder"/>.
@ -452,14 +465,14 @@ namespace Microsoft.Net.Http.Headers
result.HttpOnly = true; result.HttpOnly = true;
} }
// extension-av = <any CHAR except CTLs or ";"> // extension-av = <any CHAR except CTLs or ";">
else else
{ {
// TODO: skiping it for now to avoid parsing failure? Store it in a list? // TODO: skiping it for now to avoid parsing failure? Store it in a list?
// = (no spaces) // = (no spaces)
if (!ReadEqualsSign(input, ref offset)) if (!ReadEqualsSign(input, ref offset))
{ {
return 0; return 0;
} }
ReadToSemicolonOrEnd(input, ref offset); 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. // 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. // 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 namespace Microsoft.AspNetCore.Mvc.Razor
{ {
internal static class ViewPath internal static class ViewPath
@ -23,23 +21,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor
length++; length++;
} }
var builder = new InplaceStringBuilder(length); return string.Create(length, (path, addLeadingSlash), (span, tuple) =>
if (addLeadingSlash)
{ {
builder.Append('/'); var (pathValue, addLeadingSlashValue) = tuple;
} var spanIndex = 0;
for (var i = 0; i < path.Length; i++) if (addLeadingSlashValue)
{
var ch = path[i];
if (ch == '\\')
{ {
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(!string.IsNullOrEmpty(viewEnginePath));
Debug.Assert(viewEnginePath.StartsWith("/", StringComparison.Ordinal)); Debug.Assert(viewEnginePath.StartsWith("/", StringComparison.Ordinal));
var builder = new InplaceStringBuilder(1 + areaName.Length + viewEnginePath.Length); return string.Create(1 + areaName.Length + viewEnginePath.Length, (areaName, viewEnginePath), (span, tuple) =>
builder.Append('/'); {
builder.Append(areaName); var (areaNameValue, viewEnginePathValue) = tuple;
builder.Append(viewEnginePath);
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) private static SelectorModel CreateSelectorModel(string prefix, string routeTemplate)