Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Faster response Content-Length parsing. #1166

Merged
merged 1 commit into from
Oct 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,41 @@ public static void ValidateHeaderCharacters(string headerCharacters)
}
}

public static long ParseContentLength(StringValues value)
public static unsafe long ParseContentLength(StringValues value)
{
try
{
return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture);
}
catch (FormatException ex)
var input = value.ToString();
var parsed = 0L;

fixed (char* ptr = input)
{
throw new InvalidOperationException("Content-Length value must be an integral number.", ex);
var ch = (ushort*)ptr;
var end = ch + input.Length;

if (ch == end)
{
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
Expand Down Expand Up @@ -149,75 +150,88 @@ public void ThrowsWhenClearingHeadersAfterReadOnlyIsSet()
Assert.Throws<InvalidOperationException>(() => dictionary.Clear());
}

[Fact]
public void ThrowsWhenAddingContentLengthWithNonNumericValue()
[Theory]
[MemberData(nameof(BadContentLengths))]
public void ThrowsWhenAddingContentLengthWithNonNumericValue(string contentLength)
{
var headers = new FrameResponseHeaders();
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]
public void ThrowsWhenSettingContentLengthToNonNumericValue()
[Theory]
[MemberData(nameof(BadContentLengths))]
public void ThrowsWhenSettingContentLengthToNonNumericValue(string contentLength)
{
var headers = new FrameResponseHeaders();
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]
public void ThrowsWhenSettingRawContentLengthToNonNumericValue()
[Theory]
[MemberData(nameof(BadContentLengths))]
public void ThrowsWhenSettingRawContentLengthToNonNumericValue(string contentLength)
{
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]
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue()
[Theory]
[MemberData(nameof(BadContentLengths))]
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue(string contentLength)
{
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]
public void ContentLengthValueCanBeReadAsLongAfterAddingHeader()
[Theory]
[MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterAddingHeader(string contentLength)
{
var headers = new FrameResponseHeaders();
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]
public void ContentLengthValueCanBeReadAsLongAfterSettingHeader()
[Theory]
[MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterSettingHeader(string contentLength)
{
var headers = new FrameResponseHeaders();
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]
public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader()
[Theory]
[MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader(string contentLength)
{
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]
public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader()
[Theory]
[MemberData(nameof(GoodContentLengths))]
public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader(string contentLength)
{
var headers = new FrameResponseHeaders();
headers.HeaderContentLength = "42";
headers.HeaderContentLength = contentLength;

Assert.Equal(42, headers.HeaderContentLengthValue);
Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue);
}

[Fact]
Expand All @@ -243,5 +257,33 @@ public void ContentLengthValueClearedWhenHeadersCleared()

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>
{
"",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we throw in some invalid characters other than space? E.g. "!" and "ü".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"ü" will be rejected by ValidateHeaderCharacters(). But yeah I'll add a few more cases.

" ",
" 42",
"42 ",
"bad",
"!",
"!42",
"42!",
"42,000",
"42.000",
};
}
}
}