[Fixes #3683] Replace implementations in MediaTypeComparisons and
MediaTypeEncodings with memory efficient implementations using a MediaType struct.
This commit is contained in:
parent
739f83a978
commit
2063356f24
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,7 +138,6 @@ namespace Microsoft.AspNet.Mvc.Internal
|
|||
out resolvedContentTypeEncoding);
|
||||
|
||||
// Assert
|
||||
|
||||
Assert.Equal(expectedContentType, resolvedContentType);
|
||||
Assert.Equal(Encoding.UTF8, resolvedContentTypeEncoding);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue