| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Collections.Generic; |
| | | 5 | | using System.Diagnostics; |
| | | 6 | | using System.Diagnostics.CodeAnalysis; |
| | | 7 | | using System.Runtime.CompilerServices; |
| | | 8 | | using System.Text; |
| | | 9 | | |
| | | 10 | | namespace System.Net.Http.Headers |
| | | 11 | | { |
| | | 12 | | /// <summary>Represents a media type used in a Content-Type header as defined in the RFC 2616.</summary> |
| | | 13 | | /// <remarks> |
| | | 14 | | /// The <see cref="MediaTypeHeaderValue"/> class provides support for the media type used in a Content-Type header |
| | | 15 | | /// as defined in RFC 2616 by the IETF. An example of a media-type would be "text/plain; charset=iso-8859-5". |
| | | 16 | | /// </remarks> |
| | | 17 | | public class MediaTypeHeaderValue : ICloneable |
| | | 18 | | { |
| | | 19 | | /// <summary>The name of the charset header value.</summary> |
| | | 20 | | private const string CharSetName = "charset"; |
| | | 21 | | |
| | | 22 | | /// <summary>The lazily-initialized parameters of the header value.</summary> |
| | | 23 | | private UnvalidatedObjectCollection<NameValueHeaderValue>? _parameters; |
| | | 24 | | /// <summary>The media type.</summary> |
| | | 25 | | private string? _mediaType; |
| | | 26 | | |
| | | 27 | | /// <summary>Gets or sets the character set.</summary> |
| | | 28 | | /// <value>The character set.</value> |
| | | 29 | | public string? CharSet |
| | | 30 | | { |
| | 0 | 31 | | get => NameValueHeaderValue.Find(_parameters, CharSetName)?.Value; |
| | | 32 | | set |
| | 0 | 33 | | { |
| | | 34 | | // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from |
| | | 35 | | // setting a non-existing charset. |
| | 0 | 36 | | NameValueHeaderValue? charSetParameter = NameValueHeaderValue.Find(_parameters, CharSetName); |
| | 0 | 37 | | if (string.IsNullOrEmpty(value)) |
| | 0 | 38 | | { |
| | | 39 | | // Remove charset parameter |
| | 0 | 40 | | if (charSetParameter != null) |
| | 0 | 41 | | { |
| | 0 | 42 | | _parameters!.Remove(charSetParameter); |
| | 0 | 43 | | } |
| | 0 | 44 | | } |
| | | 45 | | else |
| | 0 | 46 | | { |
| | 0 | 47 | | if (charSetParameter != null) |
| | 0 | 48 | | { |
| | 0 | 49 | | charSetParameter.Value = value; |
| | 0 | 50 | | } |
| | | 51 | | else |
| | 0 | 52 | | { |
| | 0 | 53 | | Parameters.Add(new NameValueHeaderValue(CharSetName, value)); |
| | 0 | 54 | | } |
| | 0 | 55 | | } |
| | 0 | 56 | | } |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | /// <summary>Gets the media-type header value parameters.</summary> |
| | | 60 | | /// <value>The media-type header value parameters.</value> |
| | 872 | 61 | | public ICollection<NameValueHeaderValue> Parameters => _parameters ??= new UnvalidatedObjectCollection<NameValue |
| | | 62 | | |
| | | 63 | | /// <summary>Gets or sets the media-type header value.</summary> |
| | | 64 | | /// <value>The media-type header value.</value> |
| | | 65 | | [DisallowNull] |
| | | 66 | | public string? MediaType |
| | | 67 | | { |
| | 0 | 68 | | get { return _mediaType; } |
| | | 69 | | set |
| | 0 | 70 | | { |
| | 0 | 71 | | CheckMediaTypeFormat(value); |
| | 0 | 72 | | _mediaType = value; |
| | 0 | 73 | | } |
| | | 74 | | } |
| | | 75 | | |
| | | 76 | | /// <summary>Used by the parser to create a new instance of this type.</summary> |
| | 3907 | 77 | | internal MediaTypeHeaderValue() |
| | 3907 | 78 | | { |
| | 3907 | 79 | | } |
| | | 80 | | |
| | | 81 | | /// <summary>Initializes a new instance of the <see cref="MediaTypeHeaderValue"/> class.</summary> |
| | | 82 | | /// <param name="source">A <see cref="MediaTypeHeaderValue"/> object used to initialize the new instance.</param |
| | 0 | 83 | | protected MediaTypeHeaderValue(MediaTypeHeaderValue source) |
| | 0 | 84 | | { |
| | 0 | 85 | | Debug.Assert(source != null); |
| | | 86 | | |
| | 0 | 87 | | _mediaType = source._mediaType; |
| | 0 | 88 | | _parameters = source._parameters.Clone(); |
| | 0 | 89 | | } |
| | | 90 | | |
| | | 91 | | /// <summary>Initializes a new instance of the <see cref="MediaTypeHeaderValue"/> class.</summary> |
| | | 92 | | /// <param name="mediaType">The source represented as a string to initialize the new instance.</param> |
| | | 93 | | public MediaTypeHeaderValue(string mediaType) |
| | 0 | 94 | | : this(mediaType, charSet: null) |
| | 0 | 95 | | { |
| | 0 | 96 | | } |
| | | 97 | | |
| | | 98 | | /// <summary>Initializes a new instance of the <see cref="MediaTypeHeaderValue"/> class.</summary> |
| | | 99 | | /// <param name="mediaType">The source represented as a string to initialize the new instance.</param> |
| | | 100 | | /// <param name="charSet">The value to use for the character set.</param> |
| | 0 | 101 | | public MediaTypeHeaderValue(string mediaType, string? charSet) |
| | 0 | 102 | | { |
| | 0 | 103 | | CheckMediaTypeFormat(mediaType); |
| | 0 | 104 | | _mediaType = mediaType; |
| | | 105 | | |
| | 0 | 106 | | if (!string.IsNullOrEmpty(charSet)) |
| | 0 | 107 | | { |
| | 0 | 108 | | CharSet = charSet; |
| | 0 | 109 | | } |
| | 0 | 110 | | } |
| | | 111 | | |
| | | 112 | | /// <summary>Returns a string that represents the current <see cref="MediaTypeHeaderValue"/> object.</summary> |
| | | 113 | | /// <returns>A string that represents the current object.</returns> |
| | | 114 | | public override string ToString() |
| | 6014 | 115 | | { |
| | 6014 | 116 | | if (_parameters is null || _parameters.Count == 0) |
| | 4790 | 117 | | { |
| | 4790 | 118 | | return _mediaType ?? string.Empty; |
| | | 119 | | } |
| | | 120 | | |
| | 1224 | 121 | | var sb = StringBuilderCache.Acquire(); |
| | 1224 | 122 | | sb.Append(_mediaType); |
| | 1224 | 123 | | NameValueHeaderValue.ToString(_parameters, ';', true, sb); |
| | 1224 | 124 | | return StringBuilderCache.GetStringAndRelease(sb); |
| | 6014 | 125 | | } |
| | | 126 | | |
| | | 127 | | /// <summary>Determines whether the specified <see cref="object"/> is equal to the current <see cref="MediaTypeH |
| | | 128 | | /// <param name="obj">The object to compare with the current object.</param> |
| | | 129 | | /// <returns><see langword="true"/> if the specified <see cref="object"/> is equal to the current object; otherw |
| | | 130 | | public override bool Equals([NotNullWhen(true)] object? obj) => |
| | 0 | 131 | | obj is MediaTypeHeaderValue other && |
| | 0 | 132 | | string.Equals(_mediaType, other._mediaType, StringComparison.OrdinalIgnoreCase) && |
| | 0 | 133 | | HeaderUtilities.AreEqualCollections(_parameters, other._parameters); |
| | | 134 | | |
| | | 135 | | /// <summary>Serves as a hash function for an <see cref="MediaTypeHeaderValue"/> object.</summary> |
| | | 136 | | /// <returns>A hash code for the current object.</returns> |
| | | 137 | | /// <remarks> |
| | | 138 | | /// A hash code is a numeric value that is used to identify an object during equality testing. It can also serve |
| | | 139 | | /// The GetHashCode method is suitable for use in hashing algorithms and data structures such as a hash table. |
| | | 140 | | /// </remarks> |
| | | 141 | | public override int GetHashCode() |
| | 0 | 142 | | { |
| | | 143 | | // The media-type string is case-insensitive. |
| | 0 | 144 | | return StringComparer.OrdinalIgnoreCase.GetHashCode(_mediaType!) ^ NameValueHeaderValue.GetHashCode(_paramet |
| | 0 | 145 | | } |
| | | 146 | | |
| | | 147 | | /// <summary>Converts a string to an <see cref="MediaTypeHeaderValue"/> instance.</summary> |
| | | 148 | | /// <param name="input">A string that represents media type header value information.</param> |
| | | 149 | | /// <returns>A <see cref="MediaTypeHeaderValue"/> instance.</returns> |
| | | 150 | | /// <exception cref="ArgumentNullException"><paramref name="input"/> is a <see langword="null"/> reference.</exc |
| | | 151 | | /// <exception cref="FormatException"><parmref name="input"/> is not valid media type header value information.< |
| | | 152 | | public static MediaTypeHeaderValue Parse(string input) |
| | 0 | 153 | | { |
| | 0 | 154 | | int index = 0; |
| | 0 | 155 | | return (MediaTypeHeaderValue)MediaTypeHeaderParser.SingleValueParser.ParseValue(input, null, ref index); |
| | 0 | 156 | | } |
| | | 157 | | |
| | | 158 | | /// <summary>Determines whether a string is valid <see cref="MediaTypeHeaderValue"/> information.</summary> |
| | | 159 | | /// <param name="input">The string to validate.</param> |
| | | 160 | | /// <param name="parsedValue">The <see cref="MediaTypeHeaderValue"/> version of the string.</param> |
| | | 161 | | /// <returns><see langword="true"/> if input is valid <see cref="MediaTypeHeaderValue"/> information; otherwise, |
| | | 162 | | public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out MediaTypeHeaderValue? par |
| | 0 | 163 | | { |
| | 0 | 164 | | int index = 0; |
| | 0 | 165 | | parsedValue = null; |
| | | 166 | | |
| | 0 | 167 | | if (MediaTypeHeaderParser.SingleValueParser.TryParseValue(input, null, ref index, out object? output)) |
| | 0 | 168 | | { |
| | 0 | 169 | | parsedValue = (MediaTypeHeaderValue)output!; |
| | 0 | 170 | | return true; |
| | | 171 | | } |
| | 0 | 172 | | return false; |
| | 0 | 173 | | } |
| | | 174 | | |
| | | 175 | | internal static int GetMediaTypeLength(string? input, int startIndex, |
| | | 176 | | Func<MediaTypeHeaderValue> mediaTypeCreator, out MediaTypeHeaderValue? parsedValue) |
| | 3969 | 177 | | { |
| | 3969 | 178 | | Debug.Assert(mediaTypeCreator != null); |
| | 3969 | 179 | | Debug.Assert(startIndex >= 0); |
| | | 180 | | |
| | 3969 | 181 | | parsedValue = null; |
| | | 182 | | |
| | 3969 | 183 | | if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) |
| | 0 | 184 | | { |
| | 0 | 185 | | return 0; |
| | | 186 | | } |
| | | 187 | | |
| | | 188 | | // Caller must remove leading whitespace. If not, we'll return 0. |
| | 3969 | 189 | | int mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out string? media |
| | | 190 | | |
| | 3969 | 191 | | if (mediaTypeLength == 0) |
| | 62 | 192 | | { |
| | 62 | 193 | | return 0; |
| | | 194 | | } |
| | | 195 | | |
| | 3907 | 196 | | int current = startIndex + mediaTypeLength; |
| | 3907 | 197 | | current += HttpRuleParser.GetWhitespaceLength(input, current); |
| | | 198 | | MediaTypeHeaderValue mediaTypeHeader; |
| | | 199 | | |
| | | 200 | | // If we're not done and we have a parameter delimiter, then we have a list of parameters. |
| | 3907 | 201 | | if ((current < input.Length) && (input[current] == ';')) |
| | 872 | 202 | | { |
| | 872 | 203 | | mediaTypeHeader = mediaTypeCreator(); |
| | 872 | 204 | | mediaTypeHeader._mediaType = mediaType; |
| | | 205 | | |
| | 872 | 206 | | current++; // skip delimiter. |
| | 872 | 207 | | int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', |
| | 872 | 208 | | (UnvalidatedObjectCollection<NameValueHeaderValue>)mediaTypeHeader.Parameters); |
| | | 209 | | |
| | 872 | 210 | | if (parameterLength == 0) |
| | 32 | 211 | | { |
| | 32 | 212 | | return 0; |
| | | 213 | | } |
| | | 214 | | |
| | 840 | 215 | | parsedValue = mediaTypeHeader; |
| | 840 | 216 | | return current + parameterLength - startIndex; |
| | | 217 | | } |
| | | 218 | | |
| | | 219 | | // We have a media type without parameters. |
| | 3035 | 220 | | mediaTypeHeader = mediaTypeCreator(); |
| | 3035 | 221 | | mediaTypeHeader._mediaType = mediaType; |
| | 3035 | 222 | | parsedValue = mediaTypeHeader; |
| | 3035 | 223 | | return current - startIndex; |
| | 3969 | 224 | | } |
| | | 225 | | |
| | | 226 | | private static int GetMediaTypeExpressionLength(string input, int startIndex, out string? mediaType) |
| | 3969 | 227 | | { |
| | 3969 | 228 | | Debug.Assert((input != null) && (input.Length > 0) && (startIndex < input.Length)); |
| | | 229 | | |
| | | 230 | | // This method just parses the "type/subtype" string, it does not parse parameters. |
| | 3969 | 231 | | mediaType = null; |
| | | 232 | | |
| | | 233 | | // Parse the type, i.e. <type> in media type string "<type>/<subtype>; param1=value1; param2=value2" |
| | 3969 | 234 | | int typeLength = HttpRuleParser.GetTokenLength(input, startIndex); |
| | | 235 | | |
| | 3969 | 236 | | if (typeLength == 0) |
| | 6 | 237 | | { |
| | 6 | 238 | | return 0; |
| | | 239 | | } |
| | | 240 | | |
| | 3963 | 241 | | int current = startIndex + typeLength; |
| | 3963 | 242 | | current += HttpRuleParser.GetWhitespaceLength(input, current); |
| | | 243 | | |
| | | 244 | | // Parse the separator between type and subtype |
| | 3963 | 245 | | if ((current >= input.Length) || (input[current] != '/')) |
| | 52 | 246 | | { |
| | 52 | 247 | | return 0; |
| | | 248 | | } |
| | 3911 | 249 | | current++; // skip delimiter. |
| | 3911 | 250 | | current += HttpRuleParser.GetWhitespaceLength(input, current); |
| | | 251 | | |
| | | 252 | | // Parse the subtype, i.e. <subtype> in media type string "<type>/<subtype>; param1=value1; param2=value2" |
| | 3911 | 253 | | int subtypeLength = HttpRuleParser.GetTokenLength(input, current); |
| | | 254 | | |
| | 3911 | 255 | | if (subtypeLength == 0) |
| | 4 | 256 | | { |
| | 4 | 257 | | return 0; |
| | | 258 | | } |
| | | 259 | | |
| | | 260 | | // If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using |
| | | 261 | | // one Substring call. Otherwise get substrings for <type> and <subtype> and combine them. |
| | 3907 | 262 | | int mediaTypeLength = current + subtypeLength - startIndex; |
| | 3907 | 263 | | if (typeLength + subtypeLength + 1 == mediaTypeLength) |
| | 3872 | 264 | | { |
| | 3872 | 265 | | mediaType = input.Substring(startIndex, mediaTypeLength); |
| | 3872 | 266 | | } |
| | | 267 | | else |
| | 35 | 268 | | { |
| | 35 | 269 | | mediaType = string.Concat(input.AsSpan(startIndex, typeLength), "/", input.AsSpan(current, subtypeLength |
| | 35 | 270 | | } |
| | | 271 | | |
| | 3907 | 272 | | return mediaTypeLength; |
| | 3969 | 273 | | } |
| | | 274 | | |
| | | 275 | | private static void CheckMediaTypeFormat(string mediaType, [CallerArgumentExpression(nameof(mediaType))] string? |
| | 0 | 276 | | { |
| | 0 | 277 | | ArgumentException.ThrowIfNullOrEmpty(mediaType, parameterName); |
| | | 278 | | |
| | | 279 | | // When adding values using strongly typed objects, no leading/trailing LWS (whitespace) are allowed. |
| | | 280 | | // Also no LWS between type and subtype are allowed. |
| | 0 | 281 | | int mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out string? tempMediaType); |
| | 0 | 282 | | if ((mediaTypeLength == 0) || (tempMediaType!.Length != mediaType.Length)) |
| | 0 | 283 | | { |
| | 0 | 284 | | throw new FormatException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_heade |
| | | 285 | | } |
| | 0 | 286 | | } |
| | | 287 | | |
| | | 288 | | // Implement ICloneable explicitly to allow derived types to "override" the implementation. |
| | | 289 | | object ICloneable.Clone() |
| | 0 | 290 | | { |
| | 0 | 291 | | return new MediaTypeHeaderValue(this); |
| | 0 | 292 | | } |
| | | 293 | | } |
| | | 294 | | } |