Update `MediaTypeHeaderValue.IsSubsetOf()` to perform consistent checks

- aspnet/Mvc#3138 part 1/2
  - check parameters with same polarity as type and subtype
    - ignore quality factors
  - bug was obscured because MVC has no formatters supporting wildcard media types

nits:
- add doc comments
- spelling
- correct typo in a `project.json` file
This commit is contained in:
Doug Bunting 2015-10-14 12:07:07 -07:00
parent a0b29e3f2b
commit 0581bcf008
3 changed files with 66 additions and 26 deletions

View File

@ -219,6 +219,21 @@ namespace Microsoft.Net.Http.Headers
get { return _isReadOnly; }
}
/// <summary>
/// Gets a value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of
/// <paramref name="otherMediaType"/>. A "subset" is defined as the same or a more specific media type
/// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept.
/// </summary>
/// <param name="otherMediaType">The <see cref="MediaTypeHeaderValue"/> to compare.</param>
/// <returns>
/// A value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of
/// <paramref name="otherMediaType"/>.
/// </returns>
/// <remarks>
/// For example "multipart/mixed; boundary=1234" is a subset of "multipart/mixed; boundary=1234",
/// "multipart/mixed", "multipart/*", and "*/*" but not "multipart/mixed; boundary=2345" or
/// "multipart/message; boundary=1234".
/// </remarks>
public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)
{
if (otherMediaType == null)
@ -226,6 +241,7 @@ namespace Microsoft.Net.Http.Headers
return false;
}
// "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*".
if (!Type.Equals(otherMediaType.Type, StringComparison.OrdinalIgnoreCase))
{
if (!otherMediaType.MatchesAllTypes)
@ -241,22 +257,29 @@ namespace Microsoft.Net.Http.Headers
}
}
if (Parameters != null)
// "text/plain; charset=utf-8; level=1" is a subset of "text/plain; charset=utf-8". In turn
// "text/plain; charset=utf-8" is a subset of "text/plain".
if (otherMediaType._parameters != null && otherMediaType._parameters.Count != 0)
{
if (Parameters.Count != 0 && (otherMediaType.Parameters == null || otherMediaType.Parameters.Count == 0))
// Make sure all parameters in the potential superset are included locally. Fine to have additional
// parameters locally; they make this one more specific.
foreach (var parameter in otherMediaType._parameters)
{
return false;
}
// Make sure all parameters listed locally are listed in the other one. The other one may have additional parameters.
foreach (var param in _parameters)
{
var otherParam = NameValueHeaderValue.Find(otherMediaType._parameters, param.Name);
if (otherParam == null)
if (string.Equals(parameter.Name, "q", StringComparison.OrdinalIgnoreCase))
{
// "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;
}
var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name);
if (localParameter == null)
{
// Not found.
return false;
}
if (!string.Equals(param.Value, otherParam.Value, StringComparison.OrdinalIgnoreCase))
if (!string.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@ -364,7 +387,7 @@ namespace Microsoft.Net.Http.Headers
return 0;
}
// Caller must remove leading whitespaces. If not, we'll return 0.
// Caller must remove leading whitespace. If not, we'll return 0.
string mediaType = null;
var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out mediaType);
@ -432,7 +455,7 @@ namespace Microsoft.Net.Http.Headers
return 0;
}
// If there are no whitespaces between <type> and <subtype> in <type>/<subtype> get the media type using
// If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using
// one Substring call. Otherwise get substrings for <type> and <subtype> and combine them.
var mediatTypeLength = current + subtypeLength - startIndex;
if (typeLength + subtypeLength + 1 == mediatTypeLength)
@ -454,8 +477,8 @@ namespace Microsoft.Net.Http.Headers
throw new ArgumentException("An empty string is not allowed.", parameterName);
}
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
// Also no LWS between type and subtype are allowed.
// When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed.
// Also no LWS between type and subtype is allowed.
string tempMediaType;
var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out tempMediaType);
if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length))

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
@ -11,9 +11,11 @@
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -542,13 +542,16 @@ namespace Microsoft.Net.Http.Headers
[Theory]
[InlineData("*/*;", "*/*")]
[InlineData("text/*;", "text/*")]
[InlineData("text/*", "text/*")]
[InlineData("text/*;", "*/*")]
[InlineData("text/plain;", "text/plain")]
[InlineData("*/*;", "*/*;charset=utf-8;")]
[InlineData("text/*;", "*/*;charset=utf-8;")]
[InlineData("text/plain;", "*/*;charset=utf-8;")]
[InlineData("text/plain;", "text/*;charset=utf-8;")]
[InlineData("text/plain;", "text/plain;charset=utf-8;")]
[InlineData("text/plain", "text/*")]
[InlineData("text/plain;", "*/*")]
[InlineData("*/*;missingparam=4", "*/*")]
[InlineData("text/*;missingparam=4;", "*/*;")]
[InlineData("text/plain;missingparam=4", "*/*;")]
[InlineData("text/plain;missingparam=4", "text/*")]
[InlineData("text/plain;charset=utf-8", "text/plain;charset=utf-8")]
[InlineData("text/plain;version=v1", "Text/plain;Version=v1")]
[InlineData("text/plain;version=v1", "tExT/plain;version=V1")]
[InlineData("text/plain;version=v1", "TEXT/PLAIN;VERSION=V1")]
@ -558,26 +561,38 @@ namespace Microsoft.Net.Http.Headers
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")]
public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2)
{
// Arrange
var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1);
var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2);
// Act
var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2);
// Assert
Assert.True(isSubset);
}
[Theory]
[InlineData("application/html", "text/*")]
[InlineData("application/json", "application/html")]
[InlineData("text/plain;version=v1", "text/plain;version=")]
[InlineData("*/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/plain;missingparam=4;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/plain;missingparam=4;", "text/*;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/plain;missingparam=4;", "*/*;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")]
[InlineData("*/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;missingparam=4;")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;missingparam=4;")]
public void IsSubsetOf_NegativeCases(string mediaType1, string mediaType2)
{
// Arrange
var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1);
var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2);
// Act
var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2);
// Assert
Assert.False(isSubset);
}