Faster response Content-Length parsing.

This commit is contained in:
Cesar Blum Silveira 2016-10-13 15:05:54 -07:00
parent 78584799a4
commit fff0adeaaf
2 changed files with 103 additions and 36 deletions

View File

@ -233,16 +233,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
} }
public static long ParseContentLength(StringValues value) public static unsafe long ParseContentLength(StringValues value)
{ {
try var input = value.ToString();
var parsed = 0L;
fixed (char* ptr = input)
{ {
return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture); var ch = (ushort*)ptr;
} var end = ch + input.Length;
catch (FormatException ex)
{ if (ch == end)
throw new InvalidOperationException("Content-Length value must be an integral number.", ex); {
ThrowInvalidContentLengthException(value);
}
ushort digit = 0;
while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9)
{
parsed *= 10;
parsed += digit;
ch++;
}
if (ch != end)
{
ThrowInvalidContentLengthException(value);
}
} }
return parsed;
}
private static void ThrowInvalidContentLengthException(string value)
{
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");
} }
private static void ThrowInvalidHeaderCharacter(char ch) private static void ThrowInvalidHeaderCharacter(char ch)

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
@ -149,75 +150,88 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Throws<InvalidOperationException>(() => dictionary.Clear()); Assert.Throws<InvalidOperationException>(() => dictionary.Clear());
} }
[Fact] [Theory]
public void ThrowsWhenAddingContentLengthWithNonNumericValue() [MemberData(nameof(BadContentLengths))]
public void ThrowsWhenAddingContentLengthWithNonNumericValue(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers; var dictionary = (IDictionary<string, StringValues>)headers;
Assert.Throws<InvalidOperationException>(() => dictionary.Add("Content-Length", new[] { "bad" })); var exception = Assert.Throws<InvalidOperationException>(() => dictionary.Add("Content-Length", new[] { contentLength }));
Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message);
} }
[Fact] [Theory]
public void ThrowsWhenSettingContentLengthToNonNumericValue() [MemberData(nameof(BadContentLengths))]
public void ThrowsWhenSettingContentLengthToNonNumericValue(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers; var dictionary = (IDictionary<string, StringValues>)headers;
Assert.Throws<InvalidOperationException>(() => ((IHeaderDictionary)headers)["Content-Length"] = "bad"); var exception = Assert.Throws<InvalidOperationException>(() => ((IHeaderDictionary)headers)["Content-Length"] = contentLength);
Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message);
} }
[Fact] [Theory]
public void ThrowsWhenSettingRawContentLengthToNonNumericValue() [MemberData(nameof(BadContentLengths))]
public void ThrowsWhenSettingRawContentLengthToNonNumericValue(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
Assert.Throws<InvalidOperationException>(() => headers.SetRawContentLength("bad", Encoding.ASCII.GetBytes("bad"))); var exception = Assert.Throws<InvalidOperationException>(() => headers.SetRawContentLength(contentLength, Encoding.ASCII.GetBytes(contentLength)));
Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message);
} }
[Fact] [Theory]
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue() [MemberData(nameof(BadContentLengths))]
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
Assert.Throws<InvalidOperationException>(() => headers.HeaderContentLength = "bad");
var exception = Assert.Throws<InvalidOperationException>(() => headers.HeaderContentLength = contentLength);
Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message);
} }
[Fact] [Theory]
public void ContentLengthValueCanBeReadAsLongAfterAddingHeader() [MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterAddingHeader(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers; var dictionary = (IDictionary<string, StringValues>)headers;
dictionary.Add("Content-Length", "42"); dictionary.Add("Content-Length", contentLength);
Assert.Equal(42, headers.HeaderContentLengthValue); Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
} }
[Fact] [Theory]
public void ContentLengthValueCanBeReadAsLongAfterSettingHeader() [MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterSettingHeader(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers; var dictionary = (IDictionary<string, StringValues>)headers;
dictionary["Content-Length"] = "42"; dictionary["Content-Length"] = contentLength;
Assert.Equal(42, headers.HeaderContentLengthValue); Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
} }
[Fact] [Theory]
public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader() [MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
headers.SetRawContentLength("42", Encoding.ASCII.GetBytes("42")); headers.SetRawContentLength(contentLength, Encoding.ASCII.GetBytes(contentLength));
Assert.Equal(42, headers.HeaderContentLengthValue); Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
} }
[Fact] [Theory]
public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader() [MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader(string contentLength)
{ {
var headers = new FrameResponseHeaders(); var headers = new FrameResponseHeaders();
headers.HeaderContentLength = "42"; headers.HeaderContentLength = contentLength;
Assert.Equal(42, headers.HeaderContentLengthValue); Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
} }
[Fact] [Fact]
@ -243,5 +257,33 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(null, headers.HeaderContentLengthValue); Assert.Equal(null, headers.HeaderContentLengthValue);
} }
private static long ParseLong(string value)
{
return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture);
}
public static TheoryData<string> GoodContentLengths => new TheoryData<string>
{
"0",
"00",
"042",
"42",
long.MaxValue.ToString(CultureInfo.InvariantCulture)
};
public static TheoryData<string> BadContentLengths => new TheoryData<string>
{
"",
" ",
" 42",
"42 ",
"bad",
"!",
"!42",
"42!",
"42,000",
"42.000",
};
} }
} }