1// Copyright (c) Microsoft Corporation. All rights reserved.2// Licensed under the MIT License.34// Copied from https://github.com/aspnet/AspNetCore/tree/master/src/Http/Headers/src56using System;7using System.Collections.Generic;8using System.Diagnostics.Contracts;9using System.Globalization;10using System.Linq;11using System.Text;1213#pragma warning disable IDE0008 // Use explicit type14#pragma warning disable IDE0017 // initialization can be simplified15#pragma warning disable IDE0019 // Use pattern matching16#pragma warning disable IDE0032 // Use auto property17#pragma warning disable IDE0034 // Default expression can be simplified18#pragma warning disable IDE0054 // Use compound assignment19#pragma warning disable IDE0059 // Unnecessary assignment20#pragma warning disable IDE1006 // Missing s_ prefix2122namespace Azure.Core.Http.Multipart23{24 /// <summary>25 /// Representation of the media type header. See <see href="https://tools.ietf.org/html/rfc6838">Media Type Specific26 /// </summary>27 internal class MediaTypeHeaderValue28 {29 private const string BoundaryString = "boundary";30 private const string CharsetString = "charset";31 private const string MatchesAllString = "*/*";32 private const string QualityString = "q";33 private const string WildcardString = "*";3435 private const char ForwardSlashCharacter = '/';36 private const char PeriodCharacter = '.';37 private const char PlusCharacter = '+';3839 private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter };4041 private static readonly HttpHeaderParser<MediaTypeHeaderValue> SingleValueParser42 = new GenericHeaderParser<MediaTypeHeaderValue>(false, GetMediaTypeLength);43 private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser44 = new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength);4546 // Use a collection instead of a dictionary since we may have multiple parameters with the same name.47 private ObjectCollection<NameValueHeaderValue> _parameters;48 private StringSegment _mediaType;49 private bool _isReadOnly;5051 private MediaTypeHeaderValue()52 {53 // Used by the parser to create a new instance of this type.54 }5556 /// <summary>57 /// Initializes a <see cref="MediaTypeHeaderValue"/> instance.58 /// </summary>59 /// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type.60 /// The text provided must be a single media type without parameters. </param>61 public MediaTypeHeaderValue(StringSegment mediaType)62 {63 CheckMediaTypeFormat(mediaType, nameof(mediaType));64 _mediaType = mediaType;65 }6667 /// <summary>68 /// Initializes a <see cref="MediaTypeHeaderValue"/> instance.69 /// </summary>70 /// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type.71 /// The text provided must be a single media type without parameters. </param>72 /// <param name="quality">The <see cref="double"/> with the quality of the media type.</param>73 public MediaTypeHeaderValue(StringSegment mediaType, double quality)74 : this(mediaType)75 {76 Quality = quality;77 }7879 /// <summary>80 /// Gets or sets the value of the charset parameter. Returns <see cref="StringSegment.Empty"/>81 /// if there is no charset.82 /// </summary>83 public StringSegment Charset84 {85 get86 {87 return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value.Value;88 }89 set90 {91 HeaderUtilities.ThrowIfReadOnly(IsReadOnly);92 // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from93 // setting a non-existing charset.94 var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString);95 if (StringSegment.IsNullOrEmpty(value))96 {97 // Remove charset parameter98 if (charsetParameter != null)99 {100 Parameters.Remove(charsetParameter);101 }102 }103 else104 {105 if (charsetParameter != null)106 {107 charsetParameter.Value = value;108 }109 else110 {111 Parameters.Add(new NameValueHeaderValue(CharsetString, value));112 }113 }114 }115 }116117 /// <summary>118 /// Gets or sets the value of the Encoding parameter. Setting the Encoding will set119 /// the <see cref="Charset"/> to <see cref="Encoding.WebName"/>.120 /// </summary>121 public Encoding Encoding122 {123 get124 {125 var charset = Charset;126 if (!StringSegment.IsNullOrEmpty(charset))127 {128 try129 {130 return Encoding.GetEncoding(charset.Value);131 }132 catch (ArgumentException)133 {134 // Invalid or not supported135 }136 }137 return null;138 }139 set140 {141 HeaderUtilities.ThrowIfReadOnly(IsReadOnly);142 if (value == null)143 {144 Charset = null;145 }146 else147 {148 Charset = value.WebName;149 }150 }151 }152153 /// <summary>154 /// Gets or sets the value of the boundary parameter. Returns <see cref="StringSegment.Empty"/>155 /// if there is no boundary.156 /// </summary>157 public StringSegment Boundary158 {159 get160 {161 return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value ?? default(StringSegment);162 }163 set164 {165 HeaderUtilities.ThrowIfReadOnly(IsReadOnly);166 var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString);167 if (StringSegment.IsNullOrEmpty(value))168 {169 // Remove charset parameter170 if (boundaryParameter != null)171 {172 Parameters.Remove(boundaryParameter);173 }174 }175 else176 {177 if (boundaryParameter != null)178 {179 boundaryParameter.Value = value;180 }181 else182 {183 Parameters.Add(new NameValueHeaderValue(BoundaryString, value));184 }185 }186 }187 }188189 /// <summary>190 /// Gets or sets the media type's parameters. Returns an empty <see cref="IList{T}"/>191 /// if there are no parameters.192 /// </summary>193 public IList<NameValueHeaderValue> Parameters194 {195 get196 {197 if (_parameters == null)198 {199 if (IsReadOnly)200 {201 _parameters = ObjectCollection<NameValueHeaderValue>.EmptyReadOnlyCollection;202 }203 else204 {205 _parameters = new ObjectCollection<NameValueHeaderValue>();206 }207 }208 return _parameters;209 }210 }211212 /// <summary>213 /// Gets or sets the value of the quality parameter. Returns null214 /// if there is no quality.215 /// </summary>216 public double? Quality217 {218 get { return HeaderUtilities.GetQuality(_parameters); }219 set220 {221 HeaderUtilities.ThrowIfReadOnly(IsReadOnly);222 HeaderUtilities.SetQuality(Parameters, value);223 }224 }225226 /// <summary>227 /// Gets or sets the value of the media type. Returns <see cref="StringSegment.Empty"/>228 /// if there is no media type.229 /// </summary>230 /// <example>231 /// For the media type <c>"application/json"</c>, the property gives the value232 /// <c>"application/json"</c>.233 /// </example>234 public StringSegment MediaType235 {236 get { return _mediaType; }237 set238 {239 HeaderUtilities.ThrowIfReadOnly(IsReadOnly);240 CheckMediaTypeFormat(value, nameof(value));241 _mediaType = value;242 }243 }244245 /// <summary>246 /// Gets the type of the <see cref="MediaTypeHeaderValue"/>.247 /// </summary>248 /// <example>249 /// For the media type <c>"application/json"</c>, the property gives the value <c>"application"</c>.250 /// </example>251 /// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2">Naming Requirements</see> for more 252 public StringSegment Type253 {254 get255 {256 return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter));257 }258 }259260 /// <summary>261 /// Gets the subtype of the <see cref="MediaTypeHeaderValue"/>.262 /// </summary>263 /// <example>264 /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value265 /// <c>"vnd.example+json"</c>.266 /// </example>267 /// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2">Naming Requirements</see> for more 268 public StringSegment SubType269 {270 get271 {272 return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1);273 }274 }275276 /// <summary>277 /// Gets subtype of the <see cref="MediaTypeHeaderValue"/>, excluding any structured syntax suffix. Returns <see278 /// if there is no subtype without suffix.279 /// </summary>280 /// <example>281 /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value282 /// <c>"vnd.example"</c>.283 /// </example>284 public StringSegment SubTypeWithoutSuffix285 {286 get287 {288 var subType = SubType;289 var startOfSuffix = subType.LastIndexOf(PlusCharacter);290 if (startOfSuffix == -1)291 {292 return subType;293 }294 else295 {296 return subType.Subsegment(0, startOfSuffix);297 }298 }299 }300301 /// <summary>302 /// Gets the structured syntax suffix of the <see cref="MediaTypeHeaderValue"/> if it has one.303 /// See <see href="https://tools.ietf.org/html/rfc6838#section-4.8">The RFC documentation on structured syntaxes304 /// </summary>305 /// <example>306 /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value307 /// <c>"json"</c>.308 /// </example>309 public StringSegment Suffix310 {311 get312 {313 var subType = SubType;314 var startOfSuffix = subType.LastIndexOf(PlusCharacter);315 if (startOfSuffix == -1)316 {317 return default(StringSegment);318 }319 else320 {321 return subType.Subsegment(startOfSuffix + 1);322 }323 }324 }325326 /// <summary>327 /// Get a <see cref="IList{T}"/> of facets of the <see cref="MediaTypeHeaderValue"/>. Facets are a328 /// period separated list of StringSegments in the <see cref="SubTypeWithoutSuffix"/>.329 /// See <see href="https://tools.ietf.org/html/rfc6838#section-3">The RFC documentation on facets.</see>330 /// </summary>331 /// <example>332 /// For the media type <c>"application/vnd.example+json"</c>, the property gives the value:333 /// <c>{"vnd", "example"}</c>334 /// </example>335 public IEnumerable<StringSegment> Facets336 {337 get338 {339 return SubTypeWithoutSuffix.Split(PeriodCharacterArray);340 }341 }342343 /// <summary>344 /// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all types.345 /// </summary>346 public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal);347348 /// <summary>349 /// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes.350 /// </summary>351 /// <example>352 /// For the media type <c>"application/*"</c>, this property is <c>true</c>.353 /// </example>354 /// <example>355 /// For the media type <c>"application/json"</c>, this property is <c>false</c>.356 /// </example>357 public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal);358359 /// <summary>360 /// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes, ignoring any structured syntax su361 /// </summary>362 /// <example>363 /// For the media type <c>"application/*+json"</c>, this property is <c>true</c>.364 /// </example>365 /// <example>366 /// For the media type <c>"application/vnd.example+json"</c>, this property is <c>false</c>.367 /// </example>368 public bool MatchesAllSubTypesWithoutSuffix =>369 SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase);370371 /// <summary>372 /// Gets whether the <see cref="MediaTypeHeaderValue"/> is readonly.373 /// </summary>374 public bool IsReadOnly375 {376 get { return _isReadOnly; }377 }378379 /// <summary>380 /// Gets a value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of381 /// <paramref name="otherMediaType"/>. A "subset" is defined as the same or a more specific media type382 /// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept.383 /// </summary>384 /// <param name="otherMediaType">The <see cref="MediaTypeHeaderValue"/> to compare.</param>385 /// <returns>386 /// A value indicating whether this <see cref="MediaTypeHeaderValue"/> is a subset of387 /// <paramref name="otherMediaType"/>.388 /// </returns>389 /// <remarks>390 /// For example "multipart/mixed; boundary=1234" is a subset of "multipart/mixed; boundary=1234",391 /// "multipart/mixed", "multipart/*", and "*/*" but not "multipart/mixed; boundary=2345" or392 /// "multipart/message; boundary=1234".393 /// </remarks>394 public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)395 {396 if (otherMediaType == null)397 {398 return false;399 }400401 // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*".402 return MatchesType(otherMediaType) &&403 MatchesSubtype(otherMediaType) &&404 MatchesParameters(otherMediaType);405 }406407 /// <summary>408 /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,409 /// while avoiding the cost of re-validating the components.410 /// </summary>411 /// <returns>A deep copy.</returns>412 public MediaTypeHeaderValue Copy()413 {414 var other = new MediaTypeHeaderValue();415 other._mediaType = _mediaType;416417 if (_parameters != null)418 {419 other._parameters = new ObjectCollection<NameValueHeaderValue>(420 _parameters.Select(item => item.Copy()));421 }422 return other;423 }424425 /// <summary>426 /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,427 /// while avoiding the cost of re-validating the components. This copy is read-only.428 /// </summary>429 /// <returns>A deep, read-only, copy.</returns>430 public MediaTypeHeaderValue CopyAsReadOnly()431 {432 if (IsReadOnly)433 {434 return this;435 }436437 var other = new MediaTypeHeaderValue();438 other._mediaType = _mediaType;439 if (_parameters != null)440 {441 other._parameters = new ObjectCollection<NameValueHeaderValue>(442 _parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true);443 }444 other._isReadOnly = true;445 return other;446 }447448 public override string ToString()449 {450 var builder = new StringBuilder();451 builder.Append(_mediaType.AsSpan());452 NameValueHeaderValue.ToString(_parameters, separator: ';', leadingSeparator: true, destination: builder);453 return builder.ToString();454 }455456 public override bool Equals(object obj)457 {458 var other = obj as MediaTypeHeaderValue;459460 if (other == null)461 {462 return false;463 }464465 return _mediaType.Equals(other._mediaType, StringComparison.OrdinalIgnoreCase) &&466 HeaderUtilities.AreEqualCollections(_parameters, other._parameters);467 }468469 public override int GetHashCode()470 {471 // The media-type string is case-insensitive.472 return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_p473 }474475 /// <summary>476 /// Takes a media type and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters.477 /// </summary>478 /// <param name="input">The <see cref="StringSegment"/> with the media type.</param>479 /// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>480 public static MediaTypeHeaderValue Parse(StringSegment input)481 {482 var index = 0;483 return SingleValueParser.ParseValue(input, ref index);484 }485486 /// <summary>487 /// Takes a media type, which can include parameters, and parses it into the <see cref="MediaTypeHeaderValue" />488 /// </summary>489 /// <param name="input">The <see cref="StringSegment"/> with the media type. The media type constructed here mus490 /// <param name="parsedValue">The parsed <see cref="MediaTypeHeaderValue"/></param>491 /// <returns>True if the value was successfully parsed.</returns>492 public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue)493 {494 var index = 0;495 return SingleValueParser.TryParseValue(input, ref index, out parsedValue);496 }497498 /// <summary>499 /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderVal500 /// </summary>501 /// <param name="inputs">A list of media types</param>502 /// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>503 public static IList<MediaTypeHeaderValue> ParseList(IList<string> inputs)504 {505 return MultipleValueParser.ParseValues(inputs);506 }507508 /// <summary>509 /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderVal510 /// Throws if there is invalid data in a string.511 /// </summary>512 /// <param name="inputs">A list of media types</param>513 /// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>514 public static IList<MediaTypeHeaderValue> ParseStrictList(IList<string> inputs)515 {516 return MultipleValueParser.ParseStrictValues(inputs);517 }518519 /// <summary>520 /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderVal521 /// </summary>522 /// <param name="inputs">A list of media types</param>523 /// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param>524 /// <returns>True if the value was successfully parsed.</returns>525 public static bool TryParseList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)526 {527 return MultipleValueParser.TryParseValues(inputs, out parsedValues);528 }529530 /// <summary>531 /// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderVal532 /// </summary>533 /// <param name="inputs">A list of media types</param>534 /// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param>535 /// <returns>True if the value was successfully parsed.</returns>536 public static bool TryParseStrictList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)537 {538 return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);539 }540541 private static int GetMediaTypeLength(StringSegment input, int startIndex, out MediaTypeHeaderValue parsedValue)542 {543 Contract.Requires(startIndex >= 0);544545 parsedValue = null;546547 if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))548 {549 return 0;550 }551552 // Caller must remove leading whitespace. If not, we'll return 0.553 var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out var mediaType554555 if (mediaTypeLength == 0)556 {557 return 0;558 }559560 var current = startIndex + mediaTypeLength;561 current = current + HttpRuleParser.GetWhitespaceLength(input, current);562 MediaTypeHeaderValue mediaTypeHeader = null;563564 // If we're not done and we have a parameter delimiter, then we have a list of parameters.565 if ((current < input.Length) && (input[current] == ';'))566 {567 mediaTypeHeader = new MediaTypeHeaderValue();568 mediaTypeHeader._mediaType = mediaType;569570 current++; // skip delimiter.571 var parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',572 mediaTypeHeader.Parameters);573574 parsedValue = mediaTypeHeader;575 return current + parameterLength - startIndex;576 }577578 // We have a media type without parameters.579 mediaTypeHeader = new MediaTypeHeaderValue();580 mediaTypeHeader._mediaType = mediaType;581 parsedValue = mediaTypeHeader;582 return current - startIndex;583 }584585 private static int GetMediaTypeExpressionLength(StringSegment input, int startIndex, out StringSegment mediaType586 {587 Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length));588589 // This method just parses the "type/subtype" string, it does not parse parameters.590 mediaType = null;591592 // Parse the type, i.e. <type> in media type string "<type>/<subtype>; param1=value1; param2=value2"593 var typeLength = HttpRuleParser.GetTokenLength(input, startIndex);594595 if (typeLength == 0)596 {597 return 0;598 }599600 var current = startIndex + typeLength;601 current = current + HttpRuleParser.GetWhitespaceLength(input, current);602603 // Parse the separator between type and subtype604 if ((current >= input.Length) || (input[current] != '/'))605 {606 return 0;607 }608 current++; // skip delimiter.609 current = current + HttpRuleParser.GetWhitespaceLength(input, current);610611 // Parse the subtype, i.e. <subtype> in media type string "<type>/<subtype>; param1=value1; param2=value2"612 var subtypeLength = HttpRuleParser.GetTokenLength(input, current);613614 if (subtypeLength == 0)615 {616 return 0;617 }618619 // If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using620 // one Substring call. Otherwise get substrings for <type> and <subtype> and combine them.621 var mediaTypeLength = current + subtypeLength - startIndex;622 if (typeLength + subtypeLength + 1 == mediaTypeLength)623 {624 mediaType = input.Subsegment(startIndex, mediaTypeLength);625 }626 else627 {628 mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, s629 }630631 return mediaTypeLength;632 }633634 private static void CheckMediaTypeFormat(StringSegment mediaType, string parameterName)635 {636 if (StringSegment.IsNullOrEmpty(mediaType))637 {638 throw new ArgumentException("An empty string is not allowed.", parameterName);639 }640641 // When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed.642 // Also no LWS between type and subtype is allowed.643 var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out var tempMediaType);644 if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length))645 {646 throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", media647 }648 }649650 private bool MatchesType(MediaTypeHeaderValue set)651 {652 return set.MatchesAllTypes ||653 set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase);654 }655656 private bool MatchesSubtype(MediaTypeHeaderValue set)657 {658 if (set.MatchesAllSubTypes)659 {660 return true;661 }662663 if (set.Suffix.HasValue)664 {665 if (Suffix.HasValue)666 {667 return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set);668 }669 else670 {671 return false;672 }673 }674 else675 {676 // If this subtype or suffix matches the subtype of the set,677 // it is considered a subtype.678 // Ex: application/json > application/val+json679 return MatchesEitherSubtypeOrSuffix(set);680 }681 }682683 private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set)684 {685 return set.MatchesAllSubTypesWithoutSuffix ||686 set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase);687 }688689 private bool MatchesEitherSubtypeOrSuffix(MediaTypeHeaderValue set)690 {691 return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase) ||692 set.SubType.Equals(Suffix, StringComparison.OrdinalIgnoreCase);693 }694695 private bool MatchesParameters(MediaTypeHeaderValue set)696 {697 if (set._parameters != null && set._parameters.Count != 0)698 {699 // Make sure all parameters in the potential superset are included locally. Fine to have additional700 // parameters locally; they make this one more specific.701 foreach (var parameter in set._parameters)702 {703 if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase))704 {705 // A parameter named "*" has no effect on media type matching, as it is only used as an indicati706 // that the entire media type string should be treated as a wildcard.707 continue;708 }709710 if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase))711 {712 // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first713 // "q" parameter (if any) separates the media-range parameter(s) from the accept-params.714 break;715 }716717 var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name);718 if (localParameter == null)719 {720 // Not found.721 return false;722 }723724 if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)725 {726 return false;727 }728 }729 }730 return true;731 }732733 private bool MatchesSubtypeSuffix(MediaTypeHeaderValue set)734 {735 // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*")736 // because there's no clear use case for it.737 return set.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase);738 }739 }740}ncG1vNJzZmiZqqq%2Fpr%2FDpJirrJmbrqTA0meZpaeSY7CwvsRnrqKmlKTEtHrNnqtomaqqv6Z50p2iZp6fp3qvsdNoeqiclVp%2FcY%2FOr5yrmZeafILG1KucZ4ukpL%2Bis8RneaWnkqh7g63TnJ%2BYhZWZtqKg2KmcgZ2RmbKzosClrJ5mmKm6rQ%3D%3D