[Fixes #3683] Replace implementations in MediaTypeComparisons and

MediaTypeEncodings with memory efficient implementations using a MediaType
struct.
This commit is contained in:
javiercn 2016-01-04 18:14:10 -08:00 committed by jacalvar
parent 739f83a978
commit 2063356f24
33 changed files with 1390 additions and 338 deletions

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
public abstract class OutputFormatterCanWriteContext
{
/// <summary>
/// Gets or sets the <see cref="MediaTypeHeaderValue"/> of the content type to write to the response.
/// Gets or sets the content type to write to the response.
/// </summary>
/// <remarks>
/// An <see cref="IOutputFormatter"/> can set this value when its

View File

@ -68,14 +68,27 @@ namespace Microsoft.AspNet.Mvc
// Confirm the request's content type is more specific than a media type this action supports e.g. OK
// if client sent "text/plain" data and this action supports "text/*".
if (requestContentType != null &&
!ContentTypes.Any(contentType => MediaTypeComparisons.IsSubsetOf(contentType, requestContentType)))
if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType))
{
context.Result = new UnsupportedMediaTypeResult();
}
}
}
private bool IsSubsetOfAnyContentType(string requestMediaType)
{
var parsedRequestMediaType = new MediaType(requestMediaType);
for (var i = 0; i < ContentTypes.Count; i++)
{
var contentTypeMediaType = new MediaType(ContentTypes[i]);
if (parsedRequestMediaType.IsSubsetOf(contentTypeMediaType))
{
return true;
}
}
return false;
}
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
{
@ -111,9 +124,9 @@ namespace Microsoft.AspNet.Mvc
return !isActionWithoutConsumeConstraintPresent;
}
// Confirm the request's content type is more specific than a media type this action supports e.g. OK
// Confirm the request's content type is more specific than (a media type this action supports e.g. OK
// if client sent "text/plain" data and this action supports "text/*".
if (ContentTypes.Any(contentType => MediaTypeComparisons.IsSubsetOf(contentType, requestContentType)))
if (IsSubsetOfAnyContentType(requestContentType))
{
return true;
}
@ -181,8 +194,9 @@ namespace Microsoft.AspNet.Mvc
var contentTypes = new MediaTypeCollection();
foreach (var arg in completeArgs)
{
if (MediaTypeComparisons.MatchesAllSubtypes(arg) ||
MediaTypeComparisons.MatchesAllTypes(arg))
var mediaType = new MediaType(arg);
if (mediaType.MatchesAllSubTypes ||
mediaType.MatchesAllTypes)
{
throw new InvalidOperationException(
Resources.FormatMatchAllContentTypeIsNotAllowed(arg));

View File

@ -0,0 +1,153 @@
// 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 System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc.Formatters
{
public static class AcceptHeaderParser
{
public static IList<MediaTypeSegmentWithQuality> ParseAcceptHeader(IList<string> acceptHeaders)
{
var parsedValues = new List<MediaTypeSegmentWithQuality>();
ParseAcceptHeader(acceptHeaders, parsedValues);
return parsedValues;
}
public static void ParseAcceptHeader(IList<string> acceptHeaders, IList<MediaTypeSegmentWithQuality> parsedValues)
{
if (acceptHeaders == null)
{
throw new ArgumentNullException(nameof(acceptHeaders));
}
if (parsedValues == null)
{
throw new ArgumentNullException(nameof(parsedValues));
}
for (var i = 0; i < acceptHeaders.Count; i++)
{
var charIndex = 0;
var value = acceptHeaders[i];
while (!string.IsNullOrEmpty(value) && charIndex < value.Length)
{
MediaTypeSegmentWithQuality output;
if (TryParseValue(value, ref charIndex, out output))
{
// The entry may not contain an actual value, like Accept: application/json, , */*
if (output.MediaType.HasValue)
{
parsedValues.Add(output);
}
}
else
{
var invalidValuesError = Resources.FormatAcceptHeaderParser_ParseAcceptHeader_InvalidValues(
value.Substring(charIndex));
throw new FormatException(invalidValuesError);
}
}
}
}
private static bool TryParseValue(string value, ref int index, out MediaTypeSegmentWithQuality parsedValue)
{
parsedValue = default(MediaTypeSegmentWithQuality);
// The accept header may be added multiple times to the request/response message. E.g.
// Accept: text/xml; q=1
// Accept:
// Accept: text/plain; q=0.2
// In this case, do not fail parsing in case one of the values is the empty string.
if (string.IsNullOrEmpty(value) || (index == value.Length))
{
return true;
}
var separatorFound = false;
var currentIndex = GetNextNonEmptyOrWhitespaceIndex(value, index, out separatorFound);
if (currentIndex == value.Length)
{
index = currentIndex;
return true;
}
MediaTypeSegmentWithQuality result;
var length = GetMediaTypeWithQualityLength(value, currentIndex, out result);
if (length == 0)
{
return false;
}
currentIndex = currentIndex + length;
currentIndex = GetNextNonEmptyOrWhitespaceIndex(value, currentIndex, out separatorFound);
// If we've not reached the end of the string, then we must have a separator.
// E. g application/json, text/plain <- We must be at ',' otherwise, we've failed parsing.
if (!separatorFound && (currentIndex < value.Length))
{
return false;
}
index = currentIndex;
parsedValue = result;
return true;
}
private static int GetNextNonEmptyOrWhitespaceIndex(
string input,
int startIndex,
out bool separatorFound)
{
Debug.Assert(input != null);
Debug.Assert(startIndex <= input.Length); // it's OK if index == value.Length.
separatorFound = false;
var current = startIndex + HttpTokenParsingRules.GetWhitespaceLength(input, startIndex);
if ((current == input.Length) || (input[current] != ','))
{
return current;
}
// If we have a separator, skip the separator and all following whitespaces, and
// continue until the current character is neither a separator nor a whitespace.
separatorFound = true;
current++; // skip delimiter.
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
while ((current < input.Length) && (input[current] == ','))
{
current++; // skip delimiter.
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
}
return current;
}
private static int GetMediaTypeWithQualityLength(
string input,
int start,
out MediaTypeSegmentWithQuality result)
{
result = MediaType.CreateMediaTypeSegmentWithQuality(input, start);
if (result.MediaType.HasValue)
{
return result.MediaType.Length;
}
else
{
return 0;
}
}
}
}

View File

@ -89,13 +89,28 @@ namespace Microsoft.AspNet.Mvc.Formatters
// request's format and IApiResponseMetadataProvider-provided content types similarly to an Accept
// header and an output formatter's SupportedMediaTypes: Confirm action supports a more specific media
// type than requested e.g. OK if "text/*" requested and action supports "text/plain".
if (!supportedMediaTypes.Any(c => MediaTypeComparisons.IsSubsetOf(contentType, c)))
if (!IsSuperSetOfAnySupportedMediaType(contentType, supportedMediaTypes))
{
context.Result = new HttpNotFoundResult();
}
}
}
private bool IsSuperSetOfAnySupportedMediaType(string contentType, MediaTypeCollection supportedMediaTypes)
{
var parsedContentType = new MediaType(contentType);
for (var i = 0; i < supportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(supportedMediaTypes[i]);
if (supportedMediaType.IsSubsetOf(parsedContentType))
{
return true;
}
}
return false;
}
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
{

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.AspNet.Mvc.Formatters
{
internal enum HttpParseResult
{
Parsed,
NotParsed,
InvalidFormat,
}
}

View File

@ -0,0 +1,273 @@
// 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 System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
namespace Microsoft.AspNet.Mvc.Formatters
{
internal static class HttpTokenParsingRules
{
private static readonly bool[] TokenChars;
private const int MaxNestedCount = 5;
internal const char CR = '\r';
internal const char LF = '\n';
internal const char SP = ' ';
internal const char Tab = '\t';
internal const int MaxInt64Digits = 19;
internal const int MaxInt32Digits = 10;
// iso-8859-1, Western European (ISO)
internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding(28591);
static HttpTokenParsingRules()
{
// token = 1*<any CHAR except CTLs or separators>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
TokenChars = new bool[128]; // everything is false
for (int i = 33; i < 127; i++) // skip Space (32) & DEL (127)
{
TokenChars[i] = true;
}
// remove separators: these are not valid token characters
TokenChars[(byte)'('] = false;
TokenChars[(byte)')'] = false;
TokenChars[(byte)'<'] = false;
TokenChars[(byte)'>'] = false;
TokenChars[(byte)'@'] = false;
TokenChars[(byte)','] = false;
TokenChars[(byte)';'] = false;
TokenChars[(byte)':'] = false;
TokenChars[(byte)'\\'] = false;
TokenChars[(byte)'"'] = false;
TokenChars[(byte)'/'] = false;
TokenChars[(byte)'['] = false;
TokenChars[(byte)']'] = false;
TokenChars[(byte)'?'] = false;
TokenChars[(byte)'='] = false;
TokenChars[(byte)'{'] = false;
TokenChars[(byte)'}'] = false;
}
internal static bool IsTokenChar(char character)
{
// Must be between 'space' (32) and 'DEL' (127)
if (character > 127)
{
return false;
}
return TokenChars[character];
}
internal static int GetTokenLength(string input, int startIndex)
{
Contract.Requires(input != null);
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
if (startIndex >= input.Length)
{
return 0;
}
var current = startIndex;
while (current < input.Length)
{
if (!IsTokenChar(input[current]))
{
return current - startIndex;
}
current++;
}
return input.Length - startIndex;
}
internal static int GetWhitespaceLength(string input, int startIndex)
{
Contract.Requires(input != null);
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
if (startIndex >= input.Length)
{
return 0;
}
var current = startIndex;
char c;
while (current < input.Length)
{
c = input[current];
if ((c == SP) || (c == Tab))
{
current++;
continue;
}
if (c == CR)
{
// If we have a #13 char, it must be followed by #10 and then at least one SP or HT.
if ((current + 2 < input.Length) && (input[current + 1] == LF))
{
char spaceOrTab = input[current + 2];
if ((spaceOrTab == SP) || (spaceOrTab == Tab))
{
current += 3;
continue;
}
}
}
return current - startIndex;
}
// All characters between startIndex and the end of the string are LWS characters.
return input.Length - startIndex;
}
internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length)
{
var nestedCount = 0;
return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length);
}
// quoted-pair = "\" CHAR
// CHAR = <any US-ASCII character (octets 0 - 127)>
internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length)
{
Contract.Requires(input != null);
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) &&
(Contract.ValueAtReturn(out length) <= (input.Length - startIndex)));
length = 0;
if (input[startIndex] != '\\')
{
return HttpParseResult.NotParsed;
}
// Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char)
// If so, check whether the character is in the range 0-127. If not, it's an invalid value.
if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127))
{
return HttpParseResult.InvalidFormat;
}
// We don't care what the char next to '\' is.
length = 2;
return HttpParseResult.Parsed;
}
// TEXT = <any OCTET except CTLs, but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
//
// Since we don't really care about the content of a quoted string or comment, we're more tolerant and
// allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment).
//
// 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like
// "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested
// comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any)
// is unusual.
private static HttpParseResult GetExpressionLength(
string input,
int startIndex,
char openChar,
char closeChar,
bool supportsNesting,
ref int nestedCount,
out int length)
{
Contract.Requires(input != null);
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
Contract.Ensures((Contract.Result<HttpParseResult>() != HttpParseResult.Parsed) ||
(Contract.ValueAtReturn<int>(out length) > 0));
length = 0;
if (input[startIndex] != openChar)
{
return HttpParseResult.NotParsed;
}
var current = startIndex + 1; // Start parsing with the character next to the first open-char
while (current < input.Length)
{
// Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e.
// quoted char + closing char). Otherwise the closing char may be considered part of the quoted char.
var quotedPairLength = 0;
if ((current + 2 < input.Length) &&
(GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed))
{
// We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair,
// but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only
// allows ASCII chars after '\'; qdtext allows both '\' and >127 chars).
current = current + quotedPairLength;
continue;
}
// If we support nested expressions and we find an open-char, then parse the nested expressions.
if (supportsNesting && (input[current] == openChar))
{
nestedCount++;
try
{
// Check if we exceeded the number of nested calls.
if (nestedCount > MaxNestedCount)
{
return HttpParseResult.InvalidFormat;
}
var nestedLength = 0;
HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar,
supportsNesting, ref nestedCount, out nestedLength);
switch (nestedResult)
{
case HttpParseResult.Parsed:
current += nestedLength; // add the length of the nested expression and continue.
break;
case HttpParseResult.NotParsed:
Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " +
"parsing, because we found the open-char. So either it's a valid nested " +
"expression or it has invalid format.");
break;
case HttpParseResult.InvalidFormat:
// If the nested expression is invalid, we can't continue, so we fail with invalid format.
return HttpParseResult.InvalidFormat;
default:
Contract.Assert(false, "Unknown enum result: " + nestedResult);
break;
}
}
finally
{
nestedCount--;
}
}
if (input[current] == closeChar)
{
length = current - startIndex + 1;
return HttpParseResult.Parsed;
}
current++;
}
// We didn't see the final quote, therefore we have an invalid expression string.
return HttpParseResult.InvalidFormat;
}
}
}

View File

@ -69,10 +69,21 @@ namespace Microsoft.AspNet.Mvc.Formatters
// Confirm the request's content type is more specific than a media type this formatter supports e.g. OK if
// client sent "text/plain" data and this formatter supports "text/*".
return SupportedMediaTypes.Any(supportedMediaType =>
return IsSubsetOfAnySupportedContentType(contentType);
}
private bool IsSubsetOfAnySupportedContentType(string contentType)
{
var parsedContentType = new MediaType(contentType);
for (var i = 0; i < SupportedMediaTypes.Count; i++)
{
return MediaTypeComparisons.IsSubsetOf(supportedMediaType, contentType);
});
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (parsedContentType.IsSubsetOf(supportedMediaType))
{
return true;
}
}
return false;
}
/// <summary>
@ -120,7 +131,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
if (request.ContentType != null)
{
var encoding = MediaTypeEncoding.GetEncoding(request.ContentType);
var encoding = MediaType.GetEncoding(request.ContentType);
if (encoding != null)
{
foreach (var supportedEncoding in SupportedEncodings)

View File

@ -0,0 +1,551 @@
// 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 System.Globalization;
using System.Text;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Formatters
{
/// <summary>
/// A media type value.
/// </summary>
public struct MediaType
{
private static readonly StringSegment QualityParameter = new StringSegment("q");
private MediaTypeParameterParser _parameterParser;
/// <summary>
/// Initializes a <see cref="MediaType"/> instance.
/// </summary>
/// <param name="mediaType">The <see cref="string"/> with the media type.</param>
public MediaType(string mediaType)
: this(mediaType, 0, mediaType.Length)
{
}
/// <summary>
/// Initializes a <see cref="MediaType"/> instance.
/// </summary>
/// <param name="mediaType">The <see cref="StringSegment"/> with the media type.</param>
public MediaType(StringSegment mediaType)
: this(mediaType.Buffer, mediaType.Offset, mediaType.Length)
{
}
/// <summary>
/// Initializes a <see cref="MediaTypeParameterParser"/> instance.
/// </summary>
/// <param name="mediaType">The <see cref="string"/> with the media type.</param>
/// <param name="offset">The offset in the <paramref name="mediaType"/> where the parsing starts.</param>
/// <param name="length">The of the media type to parse if provided.</param>
public MediaType(string mediaType, int offset, int? length)
{
if (mediaType == null)
{
throw new ArgumentNullException(nameof(mediaType));
}
if (offset < 0 || offset >= mediaType.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
if (length != null && offset + length > mediaType.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
_parameterParser = default(MediaTypeParameterParser);
StringSegment type;
var typeLength = GetTypeLength(mediaType, offset, out type);
if (typeLength == 0)
{
Type = new StringSegment();
SubType = new StringSegment();
return;
}
else
{
Type = type;
}
StringSegment subType;
var subTypeLength = GetSubtypeLength(mediaType, offset + typeLength, out subType);
if (subTypeLength == 0)
{
SubType = new StringSegment();
return;
}
else
{
SubType = subType;
}
_parameterParser = new MediaTypeParameterParser(mediaType, offset + typeLength + subTypeLength, length);
}
// All GetXXXLength methods work in the same way. They expect to be on the right position for
// the token they are parsing, for example, the beginning of the media type or the delimiter
// from a previous token, like '/', ';' or '='.
// Each method consumes the delimiter token if any, the leading whitespace, then the given token
// itself, and finally the trailing whitespace.
private static int GetTypeLength(string input, int offset, out StringSegment type)
{
if (offset < 0 || offset >= input.Length)
{
type = default(StringSegment);
return 0;
}
// Parse the type, i.e. <type> in media type string "<type>/<subtype>; param1=value1; param2=value2"
var typeLength = HttpTokenParsingRules.GetTokenLength(input, offset);
if (typeLength == 0)
{
type = default(StringSegment);
return 0;
}
type = new StringSegment(input, offset, typeLength);
var current = offset + typeLength;
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
return current - offset;
}
private static int GetSubtypeLength(string input, int offset, out StringSegment subType)
{
var current = offset;
// Parse the separator between type and subtype
if (current < 0 || current >= input.Length || input[current] != '/')
{
subType = default(StringSegment);
return 0;
}
current++; // skip delimiter.
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
var subtypeLength = HttpTokenParsingRules.GetTokenLength(input, current);
if (subtypeLength == 0)
{
subType = default(StringSegment);
return 0;
}
subType = new StringSegment(input, current, subtypeLength);
current = current + subtypeLength;
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
return current - offset;
}
/// <summary>
/// Gets the type of the <see cref="MediaType"/>.
/// </summary>
public StringSegment Type { get; }
/// <summary>
/// Gets whether this <see cref="MediaType"/> matches all types.
/// </summary>
public bool MatchesAllTypes => Type.Equals("*", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets the subtype of the <see cref="MediaType"/>.
/// </summary>
public StringSegment SubType { get; private set; }
/// <summary>
/// Gets whether this <see cref="MediaType"/> matches all subtypes.
/// </summary>
public bool MatchesAllSubTypes => SubType.Equals("*", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets the <see cref="System.Text.Encoding"/> of the <see cref="MediaType"/> if it has one.
/// </summary>
public Encoding Encoding => GetEncodingFromCharset(GetParameter("charset"));
/// <summary>
/// Gets the charset parameter of the <see cref="MediaType"/> if it has one.
/// </summary>
public StringSegment Charset => GetParameter("charset");
/// <summary>
/// Determines whether the current <see cref="MediaType"/> is a subset of the <paramref name="set"/> <see cref="MediaType"/>.
/// </summary>
/// <param name="set">The set <see cref="MediaType"/>.</param>
/// <returns>
/// <code>true</code> if this <see cref="MediaType"/> is a subset of <paramref name="set"/>; otherwise<code>false</code>.
/// </returns>
public bool IsSubsetOf(MediaType set)
{
return (set.MatchesAllTypes || set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase)) &&
(set.MatchesAllSubTypes || set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase)) &&
ContainsAllParameters(set._parameterParser);
}
/// <summary>
/// Gets the parameter <paramref name="parameterName"/> of the media type.
/// </summary>
/// <param name="parameterName">The name of the parameter to retrieve.</param>
/// <returns>The <see cref="StringSegment"/>for the given <paramref name="parameterName"/> if found; otherwise<code>null</code>.</returns>
public StringSegment GetParameter(string parameterName)
{
return GetParameter(new StringSegment(parameterName));
}
/// <summary>
/// Gets the parameter <paramref name="parameterName"/> of the media type.
/// </summary>
/// <param name="parameterName">The name of the parameter to retrieve.</param>
/// <returns>The <see cref="StringSegment"/>for the given <paramref name="parameterName"/> if found; otherwise<code>null</code>.</returns>
public StringSegment GetParameter(StringSegment parameterName)
{
var parametersParser = _parameterParser;
MediaTypeParameter parameter;
while (parametersParser.ParseNextParameter(out parameter))
{
if (parameter.HasName(parameterName))
{
return parameter.Value;
}
}
return new StringSegment();
}
/// <summary>
/// Replaces the encoding of the given <paramref name="mediaType"/> with the provided
/// <paramref name="encoding"/>.
/// </summary>
/// <param name="mediaType">The media type whose encoding will be replaced.</param>
/// <param name="encoding">The encoding that will replace the encoding in the <paramref name="mediaType"/></param>
/// <returns>A media type with the replaced encoding.</returns>
public static string ReplaceEncoding(string mediaType, Encoding encoding)
{
return ReplaceEncoding(new StringSegment(mediaType), encoding);
}
/// <summary>
/// Replaces the encoding of the given <paramref name="mediaType"/> with the provided
/// <paramref name="encoding"/>.
/// </summary>
/// <param name="mediaType">The media type whose encoding will be replaced.</param>
/// <param name="encoding">The encoding that will replace the encoding in the <paramref name="mediaType"/></param>
/// <returns>A media type with the replaced encoding.</returns>
public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding)
{
var parsedMediaType = new MediaType(mediaType);
var charset = parsedMediaType.GetParameter("charset");
if (charset.HasValue &&
charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase))
{
return mediaType.Value;
}
if (!charset.HasValue)
{
return CreateMediaTypeWithEncoding(mediaType, encoding);
}
var charsetOffset = charset.Offset - mediaType.Offset;
var restOffset = charsetOffset + charset.Length;
var restLength = mediaType.Length - restOffset;
var finalLength = charsetOffset + encoding.WebName.Length + restLength;
var builder = new StringBuilder(mediaType.Buffer, mediaType.Offset, charsetOffset, finalLength);
builder.Append(encoding.WebName);
builder.Append(mediaType.Buffer, restOffset, restLength);
return builder.ToString();
}
public static Encoding GetEncoding(string mediaType)
{
return GetEncoding(new StringSegment(mediaType));
}
public static Encoding GetEncoding(StringSegment mediaType)
{
var parsedMediaType = new MediaType(mediaType);
return parsedMediaType.Encoding;
}
/// <summary>
/// Creates an <see cref="MediaTypeSegmentWithQuality"/> containing the media type in <paramref name="mediaType"/>
/// and its associated quality.
/// </summary>
/// <param name="mediaType">The media type to parse.</param>
/// <param name="start">The position at which the parsing starts.</param>
/// <returns>The parsed media type with its associated quality.</returns>
public static MediaTypeSegmentWithQuality CreateMediaTypeSegmentWithQuality(string mediaType, int start)
{
var parsedMediaType = new MediaType(mediaType, start, length: null);
var parser = parsedMediaType._parameterParser;
double quality = 1.0d;
MediaTypeParameter parameter;
while (parser.ParseNextParameter(out parameter))
{
if (parameter.HasName(QualityParameter))
{
quality = double.Parse(
parameter.Value.Value, NumberStyles.AllowDecimalPoint,
NumberFormatInfo.InvariantInfo);
}
}
// We check if the parsed media type has value at this stage when we have iterated
// over all the parameters and we know if the parsing was sucessful.
if (!parser.ParsingFailed)
{
return new MediaTypeSegmentWithQuality(
new StringSegment(mediaType, start, parser.CurrentOffset - start),
quality);
}
else
{
return default(MediaTypeSegmentWithQuality);
}
}
private static Encoding GetEncodingFromCharset(StringSegment charset)
{
if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
{
// This is an optimization for utf-8 that prevents the Substring caused by
// charset.Value
return Encoding.UTF8;
}
try
{
// charset.Value might be an invalid encoding name as in charset=invalid.
// For that reason, we catch the exception thrown by Encoding.GetEncoding
// and return null instead.
return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
}
catch (Exception)
{
return null;
}
}
private static string CreateMediaTypeWithEncoding(StringSegment mediaType, Encoding encoding)
{
return $"{mediaType.Value}; charset={encoding.WebName}";
}
private bool ContainsAllParameters(MediaTypeParameterParser setParameters)
{
var parameterFound = true;
MediaTypeParameter setParameter;
while (setParameters.ParseNextParameter(out setParameter) && parameterFound)
{
if (setParameter.HasName("q"))
{
// "q" and later parameters are not involved in media type matching. Quoting the RFC: The first
// "q" parameter (if any) separates the media-range parameter(s) from the accept-params.
break;
}
// Copy the parser as we need to iterate multiple times over it.
// We can do this because it's a struct
var subSetParameters = _parameterParser;
parameterFound = false;
MediaTypeParameter subSetParameter;
while (subSetParameters.ParseNextParameter(out subSetParameter) && !parameterFound)
{
parameterFound = subSetParameter.Equals(setParameter);
}
}
return parameterFound;
}
private struct MediaTypeParameterParser
{
private string _mediaTypeBuffer;
private int? _length;
public MediaTypeParameterParser(string mediaTypeBuffer, int offset, int? length)
{
_mediaTypeBuffer = mediaTypeBuffer;
_length = length;
CurrentOffset = offset;
ParsingFailed = false;
}
public int CurrentOffset { get; private set; }
public bool ParsingFailed { get; private set; }
public bool ParseNextParameter(out MediaTypeParameter result)
{
if (_mediaTypeBuffer == null)
{
result = default(MediaTypeParameter);
return false;
}
var parameterLength = GetParameterLength(_mediaTypeBuffer, CurrentOffset, out result);
CurrentOffset = CurrentOffset + parameterLength;
if (parameterLength == 0)
{
ParsingFailed = _length != null && CurrentOffset < _length;
return false;
}
return true;
}
private static int GetParameterLength(string input, int startIndex, out MediaTypeParameter parsedValue)
{
if (OffsetIsOutOfRange(startIndex, input.Length) ||
input[startIndex] != ';')
{
parsedValue = default(MediaTypeParameter);
return 0;
}
StringSegment name;
var nameLength = GetNameLength(input, startIndex, out name);
var current = startIndex + nameLength;
if (nameLength == 0 || OffsetIsOutOfRange(current, input.Length) || input[current] != '=')
{
parsedValue = default(MediaTypeParameter);
return 0;
}
StringSegment value;
var valueLength = GetValueLength(input, current, out value);
parsedValue = new MediaTypeParameter(name, value);
current = current + valueLength;
return current - startIndex;
}
private static int GetNameLength(string input, int startIndex, out StringSegment name)
{
var current = startIndex;
current++; // skip ';'
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
var nameLength = HttpTokenParsingRules.GetTokenLength(input, current);
if (nameLength == 0)
{
name = default(StringSegment);
return 0;
}
name = new StringSegment(input, current, nameLength);
current = current + nameLength;
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
return current - startIndex;
}
private static int GetValueLength(string input, int startIndex, out StringSegment value)
{
var current = startIndex;
current++; // skip '='.
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
var valueLength = HttpTokenParsingRules.GetTokenLength(input, current);
if (valueLength == 0)
{
// A value can either be a token or a quoted string. Check if it is a quoted string.
if (HttpTokenParsingRules.GetQuotedStringLength(input, current, out valueLength) != HttpParseResult.Parsed)
{
// We have an invalid value. Reset the name and return.
value = default(StringSegment);
return 0;
}
}
value = new StringSegment(input, current, valueLength);
current = current + valueLength;
current = current + HttpTokenParsingRules.GetWhitespaceLength(input, current);
return current - startIndex;
}
private static bool OffsetIsOutOfRange(int offset, int length)
{
return offset < 0 || offset >= length;
}
}
private struct MediaTypeParameter : IEquatable<MediaTypeParameter>
{
public static readonly StringSegment Type = new StringSegment("type");
public static readonly StringSegment Subtype = new StringSegment("subtype");
public MediaTypeParameter(StringSegment name, StringSegment value)
{
Name = name;
Value = value;
}
public StringSegment Name { get; }
public StringSegment Value { get; }
public bool HasName(string name)
{
return HasName(new StringSegment(name));
}
public bool HasName(StringSegment name)
{
return Name.Equals(name, StringComparison.OrdinalIgnoreCase);
}
public bool Equals(MediaTypeParameter other)
{
return HasName(other.Name) &&
Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
return obj is MediaTypeParameter && Equals((MediaTypeParameter)obj);
}
public override int GetHashCode()
{
HashCodeCombiner hashCode = HashCodeCombiner.Start();
hashCode.Add(Name.Value);
hashCode.Add(Value.Value);
return hashCode;
}
public override string ToString() => $"{Name}={Value}";
}
}
}

View File

@ -1,120 +0,0 @@
// 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 Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc.Formatters
{
/// <summary>
/// Different types of tests against media type values.
/// </summary>
public static class MediaTypeComparisons
{
/// <summary>
/// Determines if the <paramref name="subset" /> media type is a subset of the <paramref name="set" /> media type
/// without taking into account the quality parameter.
/// </summary>
/// <param name="set">The more general media type.</param>
/// <param name="subset">The more specific media type.</param>
/// <returns><code>true</code> if <paramref name="set" /> is a more general media type than <paramref name="subset"/>;
/// otherwise <code>false</code>.</returns>
public static bool IsSubsetOf(StringSegment set, string subset)
{
return IsSubsetOf(set, new StringSegment(subset));
}
/// <summary>
/// Determines if the <paramref name="subset" /> media type is a subset of the <paramref name="set" /> media type
/// without taking into account the quality parameter.
/// </summary>
/// <param name="set">The more general media type.</param>
/// <param name="subset">The more specific media type.</param>
/// <returns><code>true</code> if <paramref name="set" /> is a more general media type than <paramref name="subset"/>;
/// otherwise <code>false</code>.</returns>
public static bool IsSubsetOf(string set, string subset)
{
return IsSubsetOf(new StringSegment(set), new StringSegment(subset));
}
/// <summary>
/// Determines if the <paramref name="subset" /> media type is a subset of the <paramref name="set" /> media type.
/// Two media types are compatible if one is a subset of the other ignoring any charset
/// parameter.
/// </summary>
/// <param name="set">The more general media type.</param>
/// <param name="subset">The more specific media type.</param>
/// <param name="ignoreQuality">Whether or not we should skip checking the quality parameter.</param>
/// <returns><code>true</code> if <paramref name="set" /> is a more general media type than <paramref name="subset"/>;
/// otherwise <code>false</code>.</returns>
public static bool IsSubsetOf(StringSegment set, StringSegment subset)
{
if (!set.HasValue || !subset.HasValue)
{
return false;
}
MediaTypeHeaderValue setMediaType;
MediaTypeHeaderValue subSetMediaType;
return MediaTypeHeaderValue.TryParse(set.Value, out setMediaType) &&
MediaTypeHeaderValue.TryParse(subset.Value, out subSetMediaType) &&
subSetMediaType.IsSubsetOf(setMediaType);
}
/// <summary>
/// Determines if the type of a given <paramref name="mediaType" /> matches all types, E.g, */*.
/// </summary>
/// <param name="mediaType">The media type to check</param>
/// <returns><code>true</code> if the <paramref name="mediaType" /> matches all subtypes; otherwise <code>false</code>.</returns>
public static bool MatchesAllTypes(string mediaType)
{
return MatchesAllTypes(new StringSegment(mediaType));
}
/// <summary>
/// Determines if the type of a given <paramref name="mediaType" /> matches all types, E.g, */*.
/// </summary>
/// <param name="mediaType">The media type to check</param>
/// <returns><code>true</code> if the <paramref name="mediaType" /> matches all subtypes; otherwise <code>false</code>.</returns>
public static bool MatchesAllTypes(StringSegment mediaType)
{
if (!mediaType.HasValue)
{
return false;
}
MediaTypeHeaderValue parsedMediaType;
return MediaTypeHeaderValue.TryParse(mediaType.Value, out parsedMediaType) &&
parsedMediaType.MatchesAllTypes;
}
/// <summary>
/// Determines if the given <paramref name="mediaType" /> matches all subtypes, E.g, text/*.
/// </summary>
/// <param name="mediaType">The media type to check</param>
/// <returns><code>true</code> if the <paramref name="mediaType" /> matches all subtypes; otherwise <code>false</code>.</returns>
public static bool MatchesAllSubtypes(string mediaType)
{
return MatchesAllSubtypes(new StringSegment(mediaType));
}
/// <summary>
/// Determines if the given <paramref name="mediaType" /> matches all subtypes, E.g, text/*.
/// </summary>
/// <param name="mediaType">The media type to check</param>
/// <returns><code>true</code> if the <paramref name="mediaType" /> matches all subtypes; otherwise <code>false</code>.</returns>
public static bool MatchesAllSubtypes(StringSegment mediaType)
{
if (!mediaType.HasValue)
{
return false;
}
MediaTypeHeaderValue parsedMediaType;
return MediaTypeHeaderValue.TryParse(mediaType.Value, out parsedMediaType) &&
parsedMediaType.MatchesAllSubTypes;
}
}
}

View File

@ -1,104 +0,0 @@
using System;
using System.Text;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc.Formatters
{
/// <summary>
/// A set of operations to manipulate the encoding of a media type value.
/// </summary>
public class MediaTypeEncoding
{
/// <summary>
/// Gets the <see cref="Encoding"/> for the given <see cref="mediaType"/> if it exists.
/// </summary>
/// <param name="mediaType">The media type from which to get the charset parameter.</param>
/// <returns>The <see cref="Encoding"/> of the media type if it exists; otherwise <code>null</code>.</returns>
public static Encoding GetEncoding(StringSegment mediaType)
{
var charset = GetCharsetParameter(mediaType);
return GetEncodingFromCharset(charset);
}
/// <summary>
/// Gets the <see cref="Encoding"/> for the given <see cref="mediaType"/> if it exists.
/// </summary>
/// <param name="mediaType">The media type from which to get the charset parameter.</param>
/// <returns>The <see cref="Encoding"/> of the media type if it exists or a <see cref="StringSegment"/> without value if not.</returns>
public static Encoding GetEncoding(string mediaType)
{
var charset = GetCharsetParameter(new StringSegment(mediaType));
return GetEncodingFromCharset(charset);
}
/// <summary>
/// Gets the charset parameter of the given <paramref name="mediaType"/> if it exists.
/// </summary>
/// <param name="mediaType">The media type from which to get the charset parameter.</param>
/// <returns>The charset of the media type if it exists or a <see cref="StringSegment"/> without value if not.</returns>
public static StringSegment GetCharsetParameter(StringSegment mediaType)
{
MediaTypeHeaderValue parsedMediaType;
if (MediaTypeHeaderValue.TryParse(mediaType.Value, out parsedMediaType))
{
return new StringSegment(parsedMediaType.Charset);
}
return new StringSegment();
}
/// <summary>
/// Replaces the encoding of the given <paramref name="mediaType"/> with the provided
/// <paramref name="encoding"/>.
/// </summary>
/// <param name="mediaType">The media type whose encoding will be replaced.</param>
/// <param name="encoding">The encoding that will replace the encoding in the <paramref name="mediaType"/></param>
/// <returns>A media type with the replaced encoding.</returns>
public static string ReplaceEncoding(string mediaType, Encoding encoding)
{
return ReplaceEncoding(new StringSegment(mediaType), encoding);
}
/// <summary>
/// Replaces the encoding of the given <paramref name="mediaType"/> with the provided
/// <paramref name="encoding"/>.
/// </summary>
/// <param name="mediaType">The media type whose encoding will be replaced.</param>
/// <param name="encoding">The encoding that will replace the encoding in the <paramref name="mediaType"/></param>
/// <returns>A media type with the replaced encoding.</returns>
public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding)
{
var parsedMediaType = MediaTypeHeaderValue.Parse(mediaType.Value);
if (string.Equals(parsedMediaType.Encoding?.WebName, encoding?.WebName, StringComparison.OrdinalIgnoreCase))
{
return mediaType.Value;
}
parsedMediaType.Encoding = encoding;
return parsedMediaType.ToString();
}
private static Encoding GetEncodingFromCharset(StringSegment charset)
{
if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
{
// This is an optimization for utf-8 that prevents the Substring caused by
// charset.Value
return Encoding.UTF8;
}
try
{
// charset.Value might be an invalid encoding name as in charset=invalid.
// For that reason, we catch the exception thrown by Encoding.GetEncoding
// and return null instead.
return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
}
catch (Exception)
{
return null;
}
}
}
}

View File

@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
var cache = new Dictionary<string, string>();
foreach (var mediaType in SupportedMediaTypes)
{
cache.Add(mediaType, MediaTypeEncoding.ReplaceEncoding(mediaType, Encoding.UTF8));
cache.Add(mediaType, MediaType.ReplaceEncoding(mediaType, Encoding.UTF8));
}
// Safe race condition, worst case scenario we initialize the field multiple times with dictionaries containing
@ -93,11 +93,14 @@ namespace Microsoft.AspNet.Mvc.Formatters
{
List<string> mediaTypes = null;
var parsedContentType = new MediaType(contentType);
// Confirm this formatter supports a more specific media type than requested e.g. OK if "text/*"
// requested and formatter supports "text/plain". Treat contentType like it came from an Accept header.
foreach (var mediaType in SupportedMediaTypes)
{
if (MediaTypeComparisons.IsSubsetOf(new StringSegment(contentType), mediaType))
var parsedMediaType = new MediaType(mediaType);
if (parsedMediaType.IsSubsetOf(parsedContentType))
{
if (mediaTypes == null)
{
@ -135,13 +138,14 @@ namespace Microsoft.AspNet.Mvc.Formatters
if (context.ContentType.HasValue)
{
var contentTypeEncoding = MediaTypeEncoding.GetCharsetParameter(context.ContentType);
if (contentTypeEncoding.HasValue)
var parsedContentType = new MediaType(context.ContentType);
var contentTypeCharset = parsedContentType.Charset;
if (contentTypeCharset.HasValue)
{
for (var i = 0; i < SupportedEncodings.Count; i++)
{
var supportedEncoding = SupportedEncodings[i];
if (contentTypeEncoding.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase))
if (contentTypeCharset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase))
{
// This is supported.
return SupportedEncodings[i];
@ -186,11 +190,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
// Confirm this formatter supports a more specific media type than requested e.g. OK if "text/*"
// requested and formatter supports "text/plain". contentType is typically what we got in an Accept
// header.
var contentType = context.ContentType;
var parsedContentType = new MediaType(context.ContentType);
for (var i = 0; i < SupportedMediaTypes.Count; i++)
{
var supportedMediaType = SupportedMediaTypes[i];
if (MediaTypeComparisons.IsSubsetOf(contentType, supportedMediaType))
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (supportedMediaType.IsSubsetOf(parsedContentType))
{
context.ContentType = new StringSegment(SupportedMediaTypes[i]);
return true;
@ -272,11 +276,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
{
if (string.Equals(encoding.WebName, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) &&
OutputMediaTypeCache.ContainsKey(mediaType))
{
return OutputMediaTypeCache[mediaType];
}
{
return OutputMediaTypeCache[mediaType];
}
return MediaTypeEncoding.ReplaceEncoding(mediaType, encoding);
return MediaType.ReplaceEncoding(mediaType, encoding);
}
private Encoding MatchAcceptCharacterEncoding(IList<StringWithQualityHeaderValue> acceptCharsetHeaders)

View File

@ -55,10 +55,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
var response = context.HttpContext.Response;
return response.WriteAsync(
valueAsString,
MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8);
return response.WriteAsync(valueAsString, MediaType.GetEncoding(response.ContentType) ?? Encoding.UTF8);
}
}
}

View File

@ -256,19 +256,11 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
HttpRequest request)
{
var result = new List<MediaTypeSegmentWithQuality>();
var parsedHeaders = request.GetTypedHeaders().Accept;
for (var i = 0; i < parsedHeaders?.Count; i++)
AcceptHeaderParser.ParseAcceptHeader(request.Headers[HeaderNames.Accept], result);
for (int i = 0; i < result.Count; i++)
{
result.Add(new MediaTypeSegmentWithQuality(
new StringSegment(parsedHeaders[i].ToString()),
parsedHeaders[i].Quality ?? 1.0));
}
for (var i = 0; i < result.Count; i++)
{
if (!RespectBrowserAcceptHeader &&
MediaTypeComparisons.MatchesAllTypes(result[i].MediaType) &&
MediaTypeComparisons.MatchesAllSubtypes(result[i].MediaType))
var mediaType = new MediaType(result[i].MediaType);
if (!RespectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes)
{
result.Clear();
return result;
@ -292,9 +284,11 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
return true;
}
var parsedMediaType = new MediaType(mediaType);
for (int i = 0; i < acceptableMediaTypes.Count; i++)
{
if (MediaTypeComparisons.IsSubsetOf(mediaType, acceptableMediaTypes[i]))
var acceptableMediaType = new MediaType(acceptableMediaTypes[i]);
if (acceptableMediaType.IsSubsetOf(parsedMediaType))
{
return true;
}
@ -378,7 +372,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
{
var mediaType = sortedAcceptHeaders[i];
formatterContext.ContentType = mediaType.MediaType;
for(var j = 0;j < formatters.Count;j++)
for (var j = 0; j < formatters.Count; j++)
{
var formatter = formatters[j];
if (formatter.CanWriteResult(formatterContext))
@ -450,8 +444,9 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
for (var i = 0; i < contentTypes.Count; i++)
{
var contentType = contentTypes[i];
if (MediaTypeComparisons.MatchesAllTypes(contentType) ||
MediaTypeComparisons.MatchesAllSubtypes(contentType))
var parsedContentType = new MediaType(contentType);
if (parsedContentType.MatchesAllTypes ||
parsedContentType.MatchesAllSubTypes)
{
var message = Resources.FormatObjectResult_MatchAllContentType(
contentType,

View File

@ -37,14 +37,14 @@ namespace Microsoft.AspNet.Mvc.Internal
{
Debug.Assert(defaultContentType != null);
var defaultContentTypeEncoding = MediaTypeEncoding.GetEncoding(defaultContentType);
var defaultContentTypeEncoding = MediaType.GetEncoding(defaultContentType);
Debug.Assert(defaultContentTypeEncoding != null);
// 1. User sets the ContentType property on the action result
if (actionResultContentType != null)
{
resolvedContentType = actionResultContentType;
var actionResultEncoding = MediaTypeEncoding.GetEncoding(actionResultContentType);
var actionResultEncoding = MediaType.GetEncoding(actionResultContentType);
resolvedContentTypeEncoding = actionResultEncoding ?? defaultContentTypeEncoding;
return;
}
@ -52,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.Internal
// 2. User sets the ContentType property on the http response directly
if (!string.IsNullOrEmpty(httpResponseContentType))
{
var mediaTypeEncoding = MediaTypeEncoding.GetEncoding(httpResponseContentType);
var mediaTypeEncoding = MediaType.GetEncoding(httpResponseContentType);
if (mediaTypeEncoding != null)
{
resolvedContentType = httpResponseContentType;

View File

@ -92,15 +92,15 @@ namespace Microsoft.AspNet.Mvc
var contentTypes = new MediaTypeCollection();
foreach (var arg in completeArgs)
{
var contentType = arg;
if (MediaTypeComparisons.MatchesAllSubtypes(contentType)||
MediaTypeComparisons.MatchesAllTypes(contentType))
var contentType = new MediaType(arg);
if (contentType.MatchesAllTypes ||
contentType.MatchesAllSubTypes)
{
throw new InvalidOperationException(
Resources.FormatMatchAllContentTypeIsNotAllowed(arg));
}
contentTypes.Add(contentType);
contentTypes.Add(arg);
}
return contentTypes;

View File

@ -1050,6 +1050,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("FormatFormatterMappings_GetMediaTypeMappingForFormat_InvalidFormat"), p0);
}
/// <summary>
/// "Invalid values '{0}'."
/// </summary>
internal static string AcceptHeaderParser_ParseAcceptHeader_InvalidValues
{
get { return GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"); }
}
/// <summary>
/// "Invalid values '{0}'."
/// </summary>
internal static string FormatAcceptHeaderParser_ParseAcceptHeader_InvalidValues(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -322,4 +322,7 @@
<data name="FormatFormatterMappings_GetMediaTypeMappingForFormat_InvalidFormat" xml:space="preserve">
<value>The argument '{0}' is invalid. Empty or null formats are not supported.</value>
</data>
<data name="AcceptHeaderParser_ParseAcceptHeader_InvalidValues" xml:space="preserve">
<value>"Invalid values '{0}'."</value>
</data>
</root>

View File

@ -26,6 +26,10 @@
"type": "build"
},
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.PropertyActivator.Sources": {
"version": "1.0.0-*",
"type": "build"

View File

@ -140,7 +140,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
var response = context.HttpContext.Response;
var selectedEncoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8;
var selectedEncoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8;
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
{

View File

@ -188,7 +188,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
var writerSettings = WriterSettings.Clone();
writerSettings.Encoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8;
writerSettings.Encoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8;
// Wrap the object only if there is a wrapping type.
var value = context.Object;

View File

@ -165,7 +165,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
var response = context.HttpContext.Response;
var writerSettings = WriterSettings.Clone();
writerSettings.Encoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8;
writerSettings.Encoding = MediaType.GetEncoding(context.ContentType) ?? Encoding.UTF8;
// Wrap the object only if there is a wrapping type.
var value = context.Object;

View File

@ -977,7 +977,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Assert
Assert.IsType<ContentResult>(actualContentResult);
Assert.Equal("TestContent", actualContentResult.Content);
Assert.Null(MediaTypeEncoding.GetEncoding(actualContentResult.ContentType));
Assert.Null(MediaType.GetEncoding(actualContentResult.ContentType));
Assert.Equal("text/plain", actualContentResult.ContentType.ToString());
}
@ -993,7 +993,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Assert
Assert.IsType<ContentResult>(actualContentResult);
Assert.Equal("TestContent", actualContentResult.Content);
Assert.Same(Encoding.UTF8, MediaTypeEncoding.GetEncoding(actualContentResult.ContentType));
Assert.Same(Encoding.UTF8, MediaType.GetEncoding(actualContentResult.ContentType));
Assert.Equal("text/plain; charset=utf-8", actualContentResult.ContentType.ToString());
}
@ -1026,7 +1026,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Assert.NotNull(contentResult.ContentType);
Assert.Equal(contentType, contentResult.ContentType.ToString());
// The default encoding of ContentResult is used when this result is executed.
Assert.Null(MediaTypeEncoding.GetEncoding(contentResult.ContentType));
Assert.Null(MediaType.GetEncoding(contentResult.ContentType));
}
[Fact]

View File

@ -0,0 +1,69 @@
// 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.Collections.Generic;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNet.Mvc.Formatters
{
public class AcceptHeaderParserTest
{
[Fact]
public void ParseAcceptHeader_ParsesSimpleHeader()
{
// Arrange
var header = "application/json";
var expected = new List<MediaTypeSegmentWithQuality>
{
new MediaTypeSegmentWithQuality(new StringSegment("application/json"),1.0)
};
// Act
var parsed = AcceptHeaderParser.ParseAcceptHeader(new List<string> { header });
// Assert
Assert.Equal(expected, parsed);
}
[Fact]
public void ParseAcceptHeader_ParsesSimpleHeaderWithMultipleValues()
{
// Arrange
var header = "application/json, application/xml;q=0.8";
var expected = new List<MediaTypeSegmentWithQuality>
{
new MediaTypeSegmentWithQuality(new StringSegment("application/json"),1.0),
new MediaTypeSegmentWithQuality(new StringSegment("application/xml;q=0.8"),0.8)
};
// Act
var parsed = AcceptHeaderParser.ParseAcceptHeader(new List<string> { header });
// Assert
Assert.Equal(expected, parsed);
foreach (var mediaType in parsed)
{
Assert.Same(header, mediaType.MediaType.Buffer);
}
}
[Fact]
public void ParseAcceptHeader_ParsesMultipleHeaderValues()
{
// Arrange
var expected = new List<MediaTypeSegmentWithQuality>
{
new MediaTypeSegmentWithQuality(new StringSegment("application/json"),1.0),
new MediaTypeSegmentWithQuality(new StringSegment("application/xml;q=0.8"),0.8)
};
// Act
var parsed = AcceptHeaderParser.ParseAcceptHeader(
new List<string> { "application/json", "", "application/xml;q=0.8" });
// Assert
Assert.Equal(expected, parsed);
}
}
}

View File

@ -9,57 +9,6 @@ namespace Microsoft.AspNet.Mvc.Formatters
{
public class MediaTypeComparisonsTest
{
[Theory]
[InlineData("application/json", "application/json", true)]
[InlineData("application/json", "application/json;charset=utf-8", true)]
[InlineData("application/json;charset=utf-8", "application/json", false)]
[InlineData("application/json;q=0.8", "application/json;q=0.9", true)]
[InlineData("application/json;q=0.8;charset=utf-7", "application/json;charset=utf-8;q=0.9", true)]
[InlineData("application/json;format=indent;charset=utf-8", "application/json", false)]
[InlineData("application/json", "application/json;format=indent;charset=utf-8", true)]
[InlineData("application/json;format=indent;charset=utf-8", "application/json;format=indent;charset=utf-8", true)]
[InlineData("application/json;charset=utf-8;format=indent", "application/json;format=indent;charset=utf-8", true)]
public void IsSubsetOf(string set, string subset, bool expectedResult)
{
// Arrange & Act
var result = MediaTypeComparisons.IsSubsetOf(
new StringSegment(set),
new StringSegment(subset));
// Assert
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", false)]
[InlineData("text/plain", false)]
public void MatchesAllTypes(string value, bool expectedResult)
{
// Arrange
var mediaType = new StringSegment(value);
// Act
var result = MediaTypeComparisons.MatchesAllTypes(mediaType);
// Assert
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", true)]
[InlineData("text/plain", false)]
public void MatchesAllSubtypes(string value, bool expectedResult)
{
// Arrange
var mediaType = new StringSegment(value);
// Act
var result = MediaTypeComparisons.MatchesAllSubtypes(mediaType);
// Assert
Assert.Equal(expectedResult, result);
}
}
}

View File

@ -0,0 +1,153 @@
// 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.Linq;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNet.Mvc.Formatters
{
public class MediaTypeTest
{
[Theory]
[InlineData("application/json")]
[InlineData("application /json")]
public void CanParse_ParameterlessMediaTypes(string mediaType)
{
// Arrange & Act
var result = new MediaType(mediaType, 0, mediaType.Length);
// Assert
Assert.Equal(new StringSegment("application"), result.Type);
Assert.Equal(new StringSegment("json"), result.SubType);
}
[Theory]
[InlineData("application/json;format=pretty;charset=utf-8;q=0.8")]
[InlineData("application/json;format=pretty;charset=utf-8; q=0.8 ")]
[InlineData("application/json;format=pretty;charset=utf-8 ; q=0.8 ")]
[InlineData("application/json;format=pretty; charset=utf-8 ; q=0.8 ")]
[InlineData("application/json;format=pretty ; charset=utf-8 ; q=0.8 ")]
[InlineData("application/json; format=pretty ; charset=utf-8 ; q=0.8 ")]
[InlineData("application/json; format=pretty ; charset=utf-8 ; q= 0.8 ")]
[InlineData("application/json; format=pretty ; charset=utf-8 ; q = 0.8 ")]
public void CanParse_MediaTypesWithParameters(string mediaType)
{
// Arrange & Act
var result = new MediaType(mediaType, 0, mediaType.Length);
// Assert
Assert.Equal(new StringSegment("application"), result.Type);
Assert.Equal(new StringSegment("json"), result.SubType);
Assert.Equal(new StringSegment("pretty"), result.GetParameter("format"));
Assert.Equal(new StringSegment("0.8"), result.GetParameter("q"));
Assert.Equal(new StringSegment("utf-8"), result.GetParameter("charset"));
}
[Theory]
[InlineData("application/json;charset=utf-8")]
[InlineData("application/json;format=indent;q=0.8;charset=utf-8")]
[InlineData("application/json;format=indent;charset=utf-8;q=0.8")]
[InlineData("application/json;charset=utf-8;format=indent;q=0.8")]
public void GetParameter_ReturnsParameter_IfParameterIsInMediaType(string mediaType)
{
// Arrange
var expectedParameter = new StringSegment("utf-8");
var parsedMediaType = new MediaType(mediaType, 0, mediaType.Length);
// Act
var result = parsedMediaType.GetParameter("charset");
// Assert
Assert.NotNull(result);
Assert.Equal(expectedParameter, result);
}
[Fact]
public void GetParameter_IsCaseInsensitive()
{
// Arrange
var mediaType = "application/json;charset=utf-8";
var expectedParameter = new StringSegment("utf-8");
var parsedMediaType = new MediaType(mediaType);
// Act
var result = parsedMediaType.GetParameter("CHARSET");
// Assert
Assert.NotNull(result);
Assert.Equal(expectedParameter, result);
}
[Theory]
[InlineData("application/json", "application/json", true)]
[InlineData("application/json", "application/json;charset=utf-8", true)]
[InlineData("application/json;charset=utf-8", "application/json", false)]
[InlineData("application/json;q=0.8", "application/json;q=0.9", true)]
[InlineData("application/json;q=0.8;charset=utf-7", "application/json;charset=utf-8;q=0.9", true)]
[InlineData("application/json;format=indent;charset=utf-8", "application/json", false)]
[InlineData("application/json", "application/json;format=indent;charset=utf-8", true)]
[InlineData("application/json;format=indent;charset=utf-8", "application/json;format=indent;charset=utf-8", true)]
[InlineData("application/json;charset=utf-8;format=indent", "application/json;format=indent;charset=utf-8", true)]
public void IsSubsetOf(string set, string subset, bool expectedResult)
{
// Arrange
var setMediaType = new MediaType(set);
var subSetMediaType = new MediaType(subset);
// Act
var result = subSetMediaType.IsSubsetOf(setMediaType);
// Assert
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", false)]
[InlineData("text/plain", false)]
public void MatchesAllTypes(string value, bool expectedResult)
{
// Arrange
var mediaType = new MediaType(value);
// Act
var result = mediaType.MatchesAllTypes;
// Assert
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", true)]
[InlineData("text/plain", false)]
public void MatchesAllSubtypes(string value, bool expectedResult)
{
// Arrange
var mediaType = new MediaType(value);
// Act
var result = mediaType.MatchesAllSubTypes;
// Assert
Assert.Equal(expectedResult, result);
}
[Fact]
public void GetParameter_ReturnsNull_IfParameterIsNotInMediaType()
{
var mediaType = "application/json;charset=utf-8;format=indent;q=0.8";
var parsedMediaType = new MediaType(mediaType, 0, mediaType.Length);
// Act
var result = parsedMediaType.GetParameter("other");
// Assert
Assert.False(result.HasValue);
}
}
}

View File

@ -138,7 +138,6 @@ namespace Microsoft.AspNet.Mvc.Internal
out resolvedContentTypeEncoding);
// Assert
Assert.Equal(expectedContentType, resolvedContentType);
Assert.Equal(Encoding.UTF8, resolvedContentTypeEncoding);
}

View File

@ -404,6 +404,50 @@ END:VCARD
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
}
[Fact]
public async Task InvalidResponseContentType_WithNotMatchingAcceptHeader_Returns406()
{
// Arrange
var targetUri = "http://localhost/InvalidContentType/SetResponseContentTypeJson";
var request = new HttpRequestMessage(HttpMethod.Get, targetUri);
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/custom1"));
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
}
[Fact]
public async Task InvalidResponseContentType_WithMatchingAcceptHeader_Returns406()
{
// Arrange
var targetUri = "http://localhost/InvalidContentType/SetResponseContentTypeJson";
var request = new HttpRequestMessage(HttpMethod.Get, targetUri);
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
}
[Fact]
public async Task InvalidResponseContentType_WithoutAcceptHeader_Returns406()
{
// Arrange
var targetUri = "http://localhost/InvalidContentType/SetResponseContentTypeJson";
var request = new HttpRequestMessage(HttpMethod.Get, targetUri);
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
}
[Fact]
public async Task ProducesAttribute_And_FormatFilterAttribute_Conflicting()
{

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Mvc
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal(content, contentResult.Content);
Assert.Equal("text/asp; charset=us-ascii", contentResult.ContentType.ToString());
Assert.Equal(encoding, MediaTypeEncoding.GetEncoding(contentResult.ContentType));
Assert.Equal(encoding, MediaType.GetEncoding(contentResult.ContentType));
}
[Theory]

View File

@ -282,7 +282,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
// Check if the original instance provided by the user has not changed.
// Since we do not have access to the new instance created within the view executor,
// check if at least the content is the same.
Assert.Null(MediaTypeEncoding.GetEncoding(contentType));
Assert.Null(MediaType.GetEncoding(contentType));
}
[Fact]

View File

@ -265,7 +265,7 @@ namespace System.Web.Http
var jsonResult = Assert.IsType<JsonResult>(result);
Assert.Same(product, jsonResult.Value);
Assert.Same(Encoding.UTF8, MediaTypeEncoding.GetEncoding(jsonResult.ContentType));
Assert.Same(Encoding.UTF8, MediaType.GetEncoding(jsonResult.ContentType));
}
[Fact]

View File

@ -0,0 +1,14 @@
using Microsoft.AspNet.Mvc;
namespace BasicWebSite.Controllers.ContentNegotiation
{
public class InvalidContentTypeController : Controller
{
[HttpGet("InvalidContentType/SetResponseContentTypeJson")]
public IActionResult SetResponseContentTypeJson()
{
HttpContext.Response.ContentType = "json";
return Ok(0);
}
}
}

View File

@ -38,7 +38,7 @@ namespace BasicWebSite.Formatters
builder.AppendLine();
builder.AppendLine("END:VCARD");
var selectedEncoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8;
var selectedEncoding = new MediaType(context.ContentType).Encoding ?? Encoding.UTF8;
await context.HttpContext.Response.WriteAsync(
builder.ToString(),

View File

@ -41,7 +41,7 @@ namespace BasicWebSite.Formatters
builder.AppendLine();
builder.AppendLine("END:VCARD");
var selectedEncoding = MediaTypeEncoding.GetEncoding(context.ContentType) ?? Encoding.UTF8;
var selectedEncoding = new MediaType(context.ContentType).Encoding ?? Encoding.UTF8;
await context.HttpContext.Response.WriteAsync(
builder.ToString(),