< Summary

Information
Class: System.Net.Http.Headers.ContentDispositionHeaderValue
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\Headers\ContentDispositionHeaderValue.cs
Line coverage
14%
Covered lines: 46
Uncovered lines: 277
Coverable lines: 323
Total lines: 554
Line coverage: 14.2%
Branch coverage
14%
Covered branches: 16
Total branches: 108
Branch coverage: 14.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor()100%11100%
.ctor(...)100%110%
.ctor(...)100%110%
ToString()100%11100%
Equals(...)0%440%
GetHashCode()100%110%
System.ICloneable.Clone()100%110%
Parse(...)100%110%
TryParse(...)0%220%
GetDispositionTypeLength(...)83.33%121292.59%
GetDispositionTypeExpressionLength(...)66.66%66100%
GetDate(...)0%660%
SetDate(...)0%660%
GetName(...)0%10100%
SetName(...)0%880%
EncodeAndQuoteMime(...)0%12120%
IsQuoted(...)0%440%
EncodeMime(...)100%110%
TryDecodeMime(...)0%12120%
TryDecode5987(...)0%14140%

File(s)

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\Headers\ContentDispositionHeaderValue.cs

#LineLine coverage
 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
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Globalization;
 8using System.Text;
 9
 10namespace System.Net.Http.Headers
 11{
 12    public class ContentDispositionHeaderValue : ICloneable
 13    {
 14        #region Fields
 15
 16        private const string fileName = "filename";
 17        private const string name = "name";
 18        private const string fileNameStar = "filename*";
 19        private const string creationDate = "creation-date";
 20        private const string modificationDate = "modification-date";
 21        private const string readDate = "read-date";
 22        private const string size = "size";
 23
 24        // Use UnvalidatedObjectCollection<T> since we may have multiple parameters with the same name.
 25        private UnvalidatedObjectCollection<NameValueHeaderValue>? _parameters;
 8426        private string _dispositionType = null!;
 27
 28        #endregion Fields
 29
 30        #region Properties
 31
 32        public string DispositionType
 33        {
 034            get { return _dispositionType; }
 35            set
 036            {
 037                HeaderUtilities.CheckValidToken(value);
 038                _dispositionType = value;
 039            }
 40        }
 41
 7242        public ICollection<NameValueHeaderValue> Parameters => _parameters ??= new UnvalidatedObjectCollection<NameValue
 43
 44        public string? Name
 45        {
 046            get { return GetName(name); }
 047            set { SetName(name, value); }
 48        }
 49
 50        public string? FileName
 51        {
 052            get { return GetName(fileName); }
 053            set { SetName(fileName, value); }
 54        }
 55
 56        public string? FileNameStar
 57        {
 058            get { return GetName(fileNameStar); }
 059            set { SetName(fileNameStar, value); }
 60        }
 61
 62        public DateTimeOffset? CreationDate
 63        {
 064            get { return GetDate(creationDate); }
 065            set { SetDate(creationDate, value); }
 66        }
 67
 68        public DateTimeOffset? ModificationDate
 69        {
 070            get { return GetDate(modificationDate); }
 071            set { SetDate(modificationDate, value); }
 72        }
 73
 74        public DateTimeOffset? ReadDate
 75        {
 076            get { return GetDate(readDate); }
 077            set { SetDate(readDate, value); }
 78        }
 79
 80        public long? Size
 81        {
 82            get
 083            {
 084                NameValueHeaderValue? sizeParameter = NameValueHeaderValue.Find(_parameters, size);
 85                ulong value;
 086                if (sizeParameter != null)
 087                {
 088                    string? sizeString = sizeParameter.Value;
 089                    if (ulong.TryParse(sizeString, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
 090                    {
 091                        return (long)value;
 92                    }
 093                }
 094                return null;
 095            }
 96            set
 097            {
 098                NameValueHeaderValue? sizeParameter = NameValueHeaderValue.Find(_parameters, size);
 099                if (value == null)
 0100                {
 101                    // Remove parameter.
 0102                    if (sizeParameter != null)
 0103                    {
 0104                        _parameters!.Remove(sizeParameter);
 0105                    }
 0106                }
 107                else
 0108                {
 0109                    ArgumentOutOfRangeException.ThrowIfNegative(value.GetValueOrDefault());
 0110                    if (sizeParameter != null)
 0111                    {
 0112                        sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture);
 0113                    }
 114                    else
 0115                    {
 0116                        string sizeString = value.Value.ToString(CultureInfo.InvariantCulture);
 0117                        Parameters.Add(new NameValueHeaderValue(size, sizeString));
 0118                    }
 0119                }
 0120            }
 121        }
 122
 123        #endregion Properties
 124
 125        #region Constructors
 126
 84127        private ContentDispositionHeaderValue()
 84128        {
 129            // Used by the parser to create a new instance of this type.
 84130        }
 131
 0132        protected ContentDispositionHeaderValue(ContentDispositionHeaderValue source)
 0133        {
 0134            Debug.Assert(source != null);
 135
 0136            _dispositionType = source._dispositionType;
 0137            _parameters = source._parameters.Clone();
 0138        }
 139
 0140        public ContentDispositionHeaderValue(string dispositionType)
 0141        {
 0142            HeaderUtilities.CheckValidToken(dispositionType);
 143
 0144            _dispositionType = dispositionType;
 0145        }
 146
 147        #endregion Constructors
 148
 149        #region Overloads
 150
 151        public override string ToString()
 39152        {
 39153            StringBuilder sb = StringBuilderCache.Acquire();
 39154            sb.Append(_dispositionType);
 39155            NameValueHeaderValue.ToString(_parameters, ';', true, sb);
 39156            return StringBuilderCache.GetStringAndRelease(sb);
 39157        }
 158
 159        public override bool Equals([NotNullWhen(true)] object? obj)
 0160        {
 0161            ContentDispositionHeaderValue? other = obj as ContentDispositionHeaderValue;
 162
 0163            if (other == null)
 0164            {
 0165                return false;
 166            }
 167
 0168            return string.Equals(_dispositionType, other._dispositionType, StringComparison.OrdinalIgnoreCase) &&
 0169                HeaderUtilities.AreEqualCollections(_parameters, other._parameters);
 0170        }
 171
 172        public override int GetHashCode()
 0173        {
 174            // The dispositionType string is case-insensitive.
 0175            return StringComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_pa
 0176        }
 177
 178        // Implement ICloneable explicitly to allow derived types to "override" the implementation.
 179        object ICloneable.Clone()
 0180        {
 0181            return new ContentDispositionHeaderValue(this);
 0182        }
 183
 184        #endregion Overloads
 185
 186        #region Parsing
 187
 188        public static ContentDispositionHeaderValue Parse(string input)
 0189        {
 0190            int index = 0;
 0191            return (ContentDispositionHeaderValue)GenericHeaderParser.ContentDispositionParser.ParseValue(input, null, r
 0192        }
 193
 194        public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out ContentDispositionHeaderV
 0195        {
 0196            int index = 0;
 0197            parsedValue = null;
 198
 0199            if (GenericHeaderParser.ContentDispositionParser.TryParseValue(input, null, ref index, out object? output))
 0200            {
 0201                parsedValue = (ContentDispositionHeaderValue)output!;
 0202                return true;
 203            }
 0204            return false;
 0205        }
 206
 207        internal static int GetDispositionTypeLength(string? input, int startIndex, out object? parsedValue)
 90208        {
 90209            Debug.Assert(startIndex >= 0);
 210
 90211            parsedValue = null;
 212
 90213            if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
 0214            {
 0215                return 0;
 216            }
 217
 218            // Caller must remove leading whitespace. If not, we'll return 0.
 219            string? dispositionType;
 90220            int dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out dispositionType);
 221
 90222            if (dispositionTypeLength == 0)
 6223            {
 6224                return 0;
 225            }
 226
 84227            int current = startIndex + dispositionTypeLength;
 84228            current += HttpRuleParser.GetWhitespaceLength(input, current);
 84229            ContentDispositionHeaderValue contentDispositionHeader = new ContentDispositionHeaderValue();
 84230            contentDispositionHeader._dispositionType = dispositionType!;
 231
 232            // If we're not done and we have a parameter delimiter, then we have a list of parameters.
 84233            if ((current < input.Length) && (input[current] == ';'))
 72234            {
 72235                current++; // Skip delimiter.
 72236                int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',
 72237                    (UnvalidatedObjectCollection<NameValueHeaderValue>)contentDispositionHeader.Parameters);
 238
 72239                if (parameterLength == 0)
 48240                {
 48241                    return 0;
 242                }
 243
 24244                parsedValue = contentDispositionHeader;
 24245                return current + parameterLength - startIndex;
 246            }
 247
 248            // We have a ContentDisposition header without parameters.
 12249            parsedValue = contentDispositionHeader;
 12250            return current - startIndex;
 90251        }
 252
 253        private static int GetDispositionTypeExpressionLength(string input, int startIndex, out string? dispositionType)
 90254        {
 90255            Debug.Assert((input != null) && (input.Length > 0) && (startIndex < input.Length));
 256
 257            // This method just parses the disposition type string, it does not parse parameters.
 90258            dispositionType = null;
 259
 260            // Parse the disposition type, i.e. <dispositiontype> in content-disposition string
 261            // "<dispositiontype>; param1=value1; param2=value2".
 90262            int typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
 263
 90264            if (typeLength == 0)
 6265            {
 6266                return 0;
 267            }
 268
 84269            dispositionType = input.Substring(startIndex, typeLength);
 84270            return typeLength;
 90271        }
 272
 273        #endregion Parsing
 274
 275        #region Helpers
 276
 277        // Gets a parameter of the given name and attempts to extract a date.
 278        // Returns null if the parameter is not present or the format is incorrect.
 279        private DateTimeOffset? GetDate(string parameter)
 0280        {
 0281            NameValueHeaderValue? dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
 282            DateTimeOffset date;
 0283            if (dateParameter != null)
 0284            {
 0285                ReadOnlySpan<char> dateString = dateParameter.Value;
 286                // Should have quotes, remove them.
 0287                if (IsQuoted(dateString))
 0288                {
 0289                    dateString = dateString.Slice(1, dateString.Length - 2);
 0290                }
 0291                if (HttpDateParser.TryParse(dateString, out date))
 0292                {
 0293                    return date;
 294                }
 0295            }
 0296            return null;
 0297        }
 298
 299        // Add the given parameter to the list. Remove if date is null.
 300        private void SetDate(string parameter, DateTimeOffset? date)
 0301        {
 0302            NameValueHeaderValue? dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
 0303            if (date == null)
 0304            {
 305                // Remove parameter.
 0306                if (dateParameter != null)
 0307                {
 0308                    _parameters!.Remove(dateParameter);
 0309                }
 0310            }
 311            else
 0312            {
 313                // Must always be quoted.
 0314                string dateString = $"\"{date.GetValueOrDefault():r}\"";
 0315                if (dateParameter != null)
 0316                {
 0317                    dateParameter.Value = dateString;
 0318                }
 319                else
 0320                {
 0321                    Parameters.Add(new NameValueHeaderValue(parameter, dateString));
 0322                }
 0323            }
 0324        }
 325
 326        // Gets a parameter of the given name and attempts to decode it if necessary.
 327        // Returns null if the parameter is not present or the raw value if the encoding is incorrect.
 328        private string? GetName(string parameter)
 0329        {
 0330            NameValueHeaderValue? nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
 0331            if (nameParameter != null)
 0332            {
 333                string? result;
 334                // filename*=utf-8'lang'%7FMyString
 0335                if (parameter.EndsWith('*'))
 0336                {
 0337                    Debug.Assert(nameParameter.Value != null);
 0338                    if (TryDecode5987(nameParameter.Value, out result))
 0339                    {
 0340                        return result;
 341                    }
 0342                    return null; // Unrecognized encoding.
 343                }
 344
 345                // filename="=?utf-8?B?BDFSDFasdfasdc==?="
 0346                if (TryDecodeMime(nameParameter.Value, out result))
 0347                {
 0348                    return result;
 349                }
 350                // May not have been encoded.
 0351                return IsQuoted(nameParameter.Value) ? nameParameter.Value!.Substring(1, nameParameter.Value.Length - 2)
 352            }
 0353            return null;
 0354        }
 355
 356        // Add/update the given parameter in the list, encoding if necessary.
 357        // Remove if value is null/Empty
 358        private void SetName(string parameter, string? value)
 0359        {
 0360            NameValueHeaderValue? nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
 0361            if (string.IsNullOrEmpty(value))
 0362            {
 363                // Remove parameter.
 0364                if (nameParameter != null)
 0365                {
 0366                    _parameters!.Remove(nameParameter);
 0367                }
 0368            }
 369            else
 0370            {
 371                string processedValue;
 0372                if (parameter.EndsWith('*'))
 0373                {
 0374                    processedValue = HeaderUtilities.Encode5987(value);
 0375                }
 376                else
 0377                {
 0378                    processedValue = EncodeAndQuoteMime(value);
 0379                }
 380
 0381                if (nameParameter != null)
 0382                {
 0383                    nameParameter.Value = processedValue;
 0384                }
 385                else
 0386                {
 0387                    Parameters.Add(new NameValueHeaderValue(parameter, processedValue));
 0388                }
 0389            }
 0390        }
 391
 392        // Returns input for decoding failures, as the content might not be encoded.
 393        private static string EncodeAndQuoteMime(string input)
 0394        {
 0395            string result = input;
 0396            bool needsQuotes = false;
 397            // Remove bounding quotes, they'll get re-added later.
 0398            if (IsQuoted(result))
 0399            {
 0400                result = result.Substring(1, result.Length - 2);
 0401                needsQuotes = true;
 0402            }
 403
 0404            if (result.Contains('"')) // Only bounding quotes are allowed.
 0405            {
 0406                throw new ArgumentException(SR.Format(CultureInfo.InvariantCulture,
 0407                    SR.net_http_headers_invalid_value, input));
 408            }
 0409            else if (!Ascii.IsValid(result))
 0410            {
 0411                needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens.
 0412                result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?=
 0413            }
 0414            else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length)
 0415            {
 0416                needsQuotes = true;
 0417            }
 418
 0419            if (needsQuotes)
 0420            {
 421                // Re-add quotes "value".
 0422                result = "\"" + result + "\"";
 0423            }
 0424            return result;
 0425        }
 426
 427        // Returns true if the value starts and ends with a quote.
 428        private static bool IsQuoted(ReadOnlySpan<char> value)
 0429        {
 0430            return
 0431                value.Length > 1 &&
 0432                value[0] == '"' &&
 0433                value[value.Length - 1] == '"';
 0434        }
 435
 436        // Encode using MIME encoding.
 437        private static string EncodeMime(string input)
 0438        {
 0439            byte[] buffer = Encoding.UTF8.GetBytes(input);
 0440            string encodedName = Convert.ToBase64String(buffer);
 0441            return "=?utf-8?B?" + encodedName + "?=";
 0442        }
 443
 444        // Attempt to decode MIME encoded strings.
 445        private static bool TryDecodeMime(string? input, [NotNullWhen(true)] out string? output)
 0446        {
 0447            Debug.Assert(input != null);
 448
 0449            output = null;
 0450            string? processedInput = input;
 451            // Require quotes, min of "=?e?b??="
 0452            if (!IsQuoted(processedInput) || processedInput.Length < 10)
 0453            {
 0454                return false;
 455            }
 456
 0457            Span<Range> parts = stackalloc Range[6];
 0458            ReadOnlySpan<char> processedInputSpan = processedInput;
 459            // "=, encodingName, encodingType, encodedData, ="
 0460            if (processedInputSpan.Split(parts, '?') != 5 ||
 0461                processedInputSpan[parts[0]] is not "\"=" ||
 0462                processedInputSpan[parts[4]] is not "=\"" ||
 0463                !processedInputSpan[parts[2]].Equals("b", StringComparison.OrdinalIgnoreCase))
 0464            {
 465                // Not encoded.
 466                // This does not support multi-line encoding.
 467                // Only base64 encoding is supported, not quoted printable.
 0468                return false;
 469            }
 470
 471            try
 0472            {
 0473                Encoding encoding = Encoding.GetEncoding(processedInput[parts[1]]);
 0474                byte[] bytes = Convert.FromBase64String(processedInput[parts[3]]);
 0475                output = encoding.GetString(bytes, 0, bytes.Length);
 0476                return true;
 477            }
 0478            catch (ArgumentException)
 0479            {
 480                // Unknown encoding or bad characters.
 0481            }
 0482            catch (FormatException)
 0483            {
 484                // Bad base64 decoding.
 0485            }
 0486            return false;
 0487        }
 488
 489
 490        // Attempt to decode using RFC 5987 encoding.
 491        // encoding'language'my%20string
 492        private static bool TryDecode5987(string input, out string? output)
 0493        {
 0494            output = null;
 495
 0496            int quoteIndex = input.IndexOf('\'');
 0497            if (quoteIndex == -1)
 0498            {
 0499                return false;
 500            }
 501
 0502            int lastQuoteIndex = input.LastIndexOf('\'');
 0503            if (quoteIndex == lastQuoteIndex || input.IndexOf('\'', quoteIndex + 1) != lastQuoteIndex)
 0504            {
 0505                return false;
 506            }
 507
 0508            string encodingString = input.Substring(0, quoteIndex);
 0509            string dataString = input.Substring(lastQuoteIndex + 1);
 510
 0511            StringBuilder decoded = new StringBuilder();
 512            try
 0513            {
 0514                Encoding encoding = Encoding.GetEncoding(encodingString);
 515
 0516                byte[] unescapedBytes = new byte[dataString.Length];
 0517                int unescapedBytesCount = 0;
 0518                for (int index = 0; index < dataString.Length; index++)
 0519                {
 0520                    if (Uri.IsHexEncoding(dataString, index)) // %FF
 0521                    {
 522                        // Unescape and cache bytes, multi-byte characters must be decoded all at once.
 0523                        unescapedBytes[unescapedBytesCount++] = (byte)Uri.HexUnescape(dataString, ref index);
 0524                        index--; // HexUnescape did +=3; Offset the for loop's ++
 0525                    }
 526                    else
 0527                    {
 0528                        if (unescapedBytesCount > 0)
 0529                        {
 530                            // Decode any previously cached bytes.
 0531                            decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
 0532                            unescapedBytesCount = 0;
 0533                        }
 0534                        decoded.Append(dataString[index]); // Normal safe character.
 0535                    }
 0536                }
 537
 0538                if (unescapedBytesCount > 0)
 0539                {
 540                    // Decode any previously cached bytes.
 0541                    decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
 0542                }
 0543            }
 0544            catch (ArgumentException)
 0545            {
 0546                return false; // Unknown encoding or bad characters.
 547            }
 548
 0549            output = decoded.ToString();
 0550            return true;
 0551        }
 552        #endregion Helpers
 553    }
 554}