< Summary

Class:Azure.Core.MultipartFormDataContent
Assembly:Azure.Core
File(s):C:\Git\azure-sdk-for-net\sdk\core\Azure.Core\src\MultipartFormDataContent.cs
Covered lines:107
Uncovered lines:21
Coverable lines:128
Total lines:406
Line coverage:83.5% (107 of 128)
Covered branches:43
Total branches:46
Branch coverage:93.4% (43 of 46)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.cctor()-100%100%
.ctor()-100%100%
.ctor(...)-100%100%
ValidateBoundary(...)-100%100%
GetDefaultBoundary()-100%100%
ApplyToRequest(...)-100%100%
Add(...)-100%100%
Add(...)-100%100%
Add(...)-100%100%
Add(...)-100%100%
AddInternal(...)-100%100%
Dispose()-100%100%
WriteTo(...)-0%0%
WriteToAsync(...)-100%100%
SerializeToStreamAsync()-85.71%100%
SerializeHeadersToString(...)-100%100%
EncodeStringToStream(...)-0%100%
EncodeStringToStreamAsync(...)-100%100%
TryComputeLength(...)-86.36%87.5%
GetEncodedLength(...)-100%100%
.ctor(...)-100%100%

File(s)

C:\Git\azure-sdk-for-net\sdk\core\Azure.Core\src\MultipartFormDataContent.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.IO;
 7using System.Text;
 8using System.Threading;
 9using System.Threading.Tasks;
 10
 11namespace Azure.Core
 12{
 13    /// <summary>
 14    ///  Provides a container for content encoded using multipart/form-data MIME type.
 15    /// </summary>
 16    internal class MultipartFormDataContent : RequestContent
 17    {
 18        #region Fields
 19
 20        private const string CrLf = "\r\n";
 21        private const string FormData = "form-data";
 22
 223        private static readonly int s_crlfLength = GetEncodedLength(CrLf);
 224        private static readonly int s_dashDashLength = GetEncodedLength("--");
 225        private static readonly int s_colonSpaceLength = GetEncodedLength(": ");
 26
 27        private readonly List<MultipartRequestContent> _nestedContent;
 28        private readonly string _boundary;
 29
 30        #endregion Fields
 31
 32        #region Construction
 33
 34        /// <summary>
 35        ///  Initializes a new instance of the <see cref="MultipartFormDataContent"/> class.
 36        /// </summary>
 1837        public MultipartFormDataContent() : this(GetDefaultBoundary())
 1838        { }
 39
 40        /// <summary>
 41        ///  Initializes a new instance of the <see cref="MultipartFormDataContent"/> class.
 42        /// </summary>
 43        /// <param name="boundary">The boundary string for the multipart form data content.</param>
 10444        public MultipartFormDataContent(string boundary)
 45        {
 10446            ValidateBoundary(boundary);
 6847            _boundary = boundary;
 6848            _nestedContent = new List<MultipartRequestContent>();
 6849        }
 50
 51        private static void ValidateBoundary(string boundary)
 52        {
 53            // NameValueHeaderValue is too restrictive for boundary.
 54            // Instead validate it ourselves and then quote it.
 10455            Argument.AssertNotNullOrWhiteSpace(boundary, nameof(boundary));
 56
 57            // RFC 2046 Section 5.1.1
 58            // boundary := 0*69<bchars> bcharsnospace
 59            // bchars := bcharsnospace / " "
 60            // bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" / "+" / "_" / "," / "-" / "." / "/" / ":" / "=" / "?"
 10061            if (boundary.Length > 70)
 62            {
 263                throw new ArgumentOutOfRangeException(nameof(boundary), boundary, $"The field cannot be longer than {70}
 64            }
 65            // Cannot end with space.
 9866            if (boundary.EndsWith(" ", StringComparison.InvariantCultureIgnoreCase))
 67            {
 268                throw new ArgumentException($"The format of value '{boundary}' is invalid.", nameof(boundary));
 69            }
 70
 71            const string AllowedMarks = @"'()+_,-./:=? ";
 72
 226873            foreach (char ch in boundary)
 74            {
 105275                if (('0' <= ch && ch <= '9') || // Digit.
 105276                    ('a' <= ch && ch <= 'z') || // alpha.
 105277                    ('A' <= ch && ch <= 'Z') || // ALPHA.
 105278                    AllowedMarks.Contains(char.ToString(ch))) // Marks.
 79                {
 80                    // Valid.
 81                }
 82                else
 83                {
 2884                    throw new ArgumentException($"The format of value '{boundary}' is invalid.", nameof(boundary));
 85                }
 86            }
 6887        }
 88
 89        private static string GetDefaultBoundary()
 90        {
 1891            return Guid.NewGuid().ToString();
 92        }
 93
 94        /// <summary>
 95        ///  Add content type header to the request.
 96        /// </summary>
 97        /// <param name="request">The request.</param>
 98        public void ApplyToRequest(Request request)
 99        {
 4100            request.Headers.Add("Content-Type", $"multipart/form-data;boundary=\"{_boundary}\"");
 4101        }
 102
 103        /// <summary>
 104        ///  Add HTTP content to a collection of RequestContent objects that
 105        ///  get serialized to multipart/form-data MIME type.
 106        /// </summary>
 107        /// <param name="content">The Request content to add to the collection.</param>
 108        public void Add(RequestContent content)
 109        {
 8110            Argument.AssertNotNull(content, nameof(content));
 6111            AddInternal(content, null, null, null);
 6112        }
 113
 114        /// <summary>
 115        ///  Add HTTP content to a collection of RequestContent objects that
 116        ///  get serialized to multipart/form-data MIME type.
 117        /// </summary>
 118        /// <param name="content">The Request content to add to the collection.</param>
 119        /// <param name="headers">The headers to add to the collection.</param>
 120        public void Add(RequestContent content, Dictionary<string, string> headers)
 121        {
 6122            Argument.AssertNotNull(content, nameof(content));
 6123            Argument.AssertNotNull(headers, nameof(headers));
 124
 6125            AddInternal(content, headers, null, null);
 6126        }
 127
 128        /// <summary>
 129        ///  Add HTTP content to a collection of RequestContent objects that
 130        ///  get serialized to multipart/form-data MIME type.
 131        /// </summary>
 132        /// <param name="content">The Request content to add to the collection.</param>
 133        /// <param name="name">The name for the request content to add.</param>
 134        /// <param name="headers">The headers to add to the collection.</param>
 135        public void Add(RequestContent content, string name, Dictionary<string, string>? headers)
 136        {
 8137            Argument.AssertNotNull(content, nameof(content));
 8138            Argument.AssertNotNullOrWhiteSpace(name, nameof(name));
 139
 4140            AddInternal(content, headers, name, null);
 4141        }
 142
 143        /// <summary>
 144        ///  Add HTTP content to a collection of RequestContent objects that
 145        ///  get serialized to multipart/form-data MIME type.
 146        /// </summary>
 147        /// <param name="content">The Request content to add to the collection.</param>
 148        /// <param name="name">The name for the request content to add.</param>
 149        /// <param name="fileName">The file name for the reuest content to add to the collection.</param>
 150        /// <param name="headers">The headers to add to the collection.</param>
 151        public void Add(RequestContent content, string name, string fileName, Dictionary<string, string>? headers)
 152        {
 14153            Argument.AssertNotNull(content, nameof(content));
 14154            Argument.AssertNotNullOrWhiteSpace(name, nameof(name));
 14155            Argument.AssertNotNullOrWhiteSpace(fileName, nameof(fileName));
 156
 10157            AddInternal(content, headers, name, fileName);
 10158        }
 159
 160        private void AddInternal(RequestContent content, Dictionary<string, string>? headers, string? name, string? file
 161        {
 26162            if (headers == null)
 163            {
 6164                headers = new Dictionary<string, string>();
 165            }
 166
 26167            if (!headers.ContainsKey("Content-Disposition"))
 168            {
 24169                var value = FormData;
 170
 24171                if (name != null)
 172                {
 14173                    value = value + "; name=" + name;
 174                }
 24175                if (fileName != null)
 176                {
 10177                    value = value + "; filename=" + fileName;
 178                }
 179
 24180                headers.Add("Content-Disposition", value);
 181            }
 182
 26183            _nestedContent.Add(new MultipartRequestContent(content, headers));
 26184        }
 185
 186        #endregion Construction
 187
 188        #region Dispose
 189
 190        /// <summary>
 191        ///  Frees resources held by the <see cref="MultipartFormDataContent"/> object.
 192        /// </summary>
 193        public override void Dispose()
 194        {
 60195            foreach (MultipartRequestContent content in _nestedContent)
 196            {
 14197                content.RequestContent.Dispose();
 198            }
 16199            _nestedContent.Clear();
 200
 16201        }
 202
 203        #endregion Dispose
 204
 205        #region Serialization
 206
 207        // for-each content
 208        //   write "--" + boundary
 209        //   for-each content header
 210        //     write header: header-value
 211        //   write content.WriteTo[Async]
 212        // write "--" + boundary + "--"
 213        // Can't be canceled directly by the user.  If the overall request is canceled
 214        // then the stream will be closed an exception thrown.
 215        /// <summary>
 216        ///
 217        /// </summary>
 218        /// <param name="stream"></param>
 219        /// <param name="cancellationToken"></param>
 220        ///
 221        public override void WriteTo(Stream stream, CancellationToken cancellationToken)
 222        {
 0223            Argument.AssertNotNull(stream, nameof(stream));
 224
 225            try
 226            {
 227                // Write start boundary.
 0228                EncodeStringToStream(stream, "--" + _boundary + CrLf);
 229
 230                // Write each nested content.
 0231                var output = new StringBuilder();
 0232                for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++)
 233                {
 234                    // Write divider, headers, and content.
 0235                    RequestContent content = _nestedContent[contentIndex].RequestContent;
 0236                    Dictionary<string, string> headers = _nestedContent[contentIndex].Headers;
 0237                    EncodeStringToStream(stream, SerializeHeadersToString(output, contentIndex, headers));
 0238                    content.WriteTo(stream, cancellationToken);
 239                }
 240
 241                // Write footer boundary.
 0242                EncodeStringToStream(stream, CrLf + "--" + _boundary + "--" + CrLf);
 0243            }
 0244            catch (Exception)
 245            {
 0246                throw;
 247            }
 0248        }
 249
 250        // for-each content
 251        //   write "--" + boundary
 252        //   for-each content header
 253        //     write header: header-value
 254        //   write content.WriteTo[Async]
 255        // write "--" + boundary + "--"
 256        // Can't be canceled directly by the user.  If the overall request is canceled
 257        // then the stream will be closed an exception thrown.
 258        /// <summary>
 259        ///
 260        /// </summary>
 261        /// <param name="stream"></param>
 262        /// <param name="cancellation"></param>
 263        /// <returns></returns>
 264        public override Task WriteToAsync(Stream stream, CancellationToken cancellation) =>
 16265            SerializeToStreamAsync(stream, cancellation);
 266
 267        private async Task SerializeToStreamAsync(Stream stream, CancellationToken cancellationToken)
 268        {
 16269            Argument.AssertNotNull(stream, nameof(stream));
 270            try
 271            {
 272                // Write start boundary.
 16273                await EncodeStringToStreamAsync(stream, "--" + _boundary + CrLf, cancellationToken).ConfigureAwait(false
 274
 275                // Write each nested content.
 16276                var output = new StringBuilder();
 72277                for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++)
 278                {
 279                    // Write divider, headers, and content.
 20280                    RequestContent content = _nestedContent[contentIndex].RequestContent;
 20281                    Dictionary<string, string> headers = _nestedContent[contentIndex].Headers;
 20282                    await EncodeStringToStreamAsync(stream, SerializeHeadersToString(output, contentIndex, headers), can
 20283                    await content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false);
 20284                }
 285
 286                // Write footer boundary.
 16287                await EncodeStringToStreamAsync(stream, CrLf + "--" + _boundary + "--" + CrLf, cancellationToken).Config
 16288            }
 0289            catch (Exception)
 290            {
 0291                throw;
 292            }
 16293        }
 294
 295        private string SerializeHeadersToString(StringBuilder scratch, int contentIndex, Dictionary<string, string> head
 296        {
 20297            scratch.Clear();
 298
 299            // Add divider.
 20300            if (contentIndex != 0) // Write divider for all but the first content.
 301            {
 6302                scratch.Append(CrLf + "--"); // const strings
 6303                scratch.Append(_boundary);
 6304                scratch.Append(CrLf);
 305            }
 306
 307            // Add headers.
 120308            foreach (KeyValuePair<string, string> header in headers)
 309            {
 40310                scratch.Append(header.Key);
 40311                scratch.Append(": ");
 40312                scratch.Append(header.Value);
 40313                scratch.Append(CrLf);
 314            }
 315
 316            // Extra CRLF to end headers (even if there are no headers).
 20317            scratch.Append(CrLf);
 318
 20319            return scratch.ToString();
 320        }
 321
 322        private static void EncodeStringToStream(Stream stream, string input)
 323        {
 0324            byte[] buffer = Encoding.Default.GetBytes(input);
 0325            stream.Write(buffer, 0, buffer.Length);
 0326        }
 327
 328        private static Task EncodeStringToStreamAsync(Stream stream, string input, CancellationToken cancellationToken)
 329        {
 52330            byte[] buffer = Encoding.Default.GetBytes(input);
 52331            return stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
 332        }
 333
 334        /// <summary>
 335        /// Attempts to compute the length of the underlying content, if available.
 336        /// </summary>
 337        /// <param name="length">The length of the underlying data.</param>
 338        public override bool TryComputeLength(out long length)
 339        {
 4340            int boundaryLength = GetEncodedLength(_boundary);
 341
 4342            long currentLength = 0;
 4343            long internalBoundaryLength = s_crlfLength + s_dashDashLength + boundaryLength + s_crlfLength;
 344
 345            // Start Boundary.
 4346            currentLength += s_dashDashLength + boundaryLength + s_crlfLength;
 347
 4348            bool first = true;
 24349            foreach (MultipartRequestContent content in _nestedContent)
 350            {
 8351                if (first)
 352                {
 4353                    first = false; // First boundary already written.
 354                }
 355                else
 356                {
 357                    // Internal Boundary.
 4358                    currentLength += internalBoundaryLength;
 359                }
 360
 361                // Headers.
 48362                foreach (KeyValuePair<string, string> headerPair in content.Headers)
 363                {
 16364                    currentLength += GetEncodedLength(headerPair.Key) + s_colonSpaceLength;
 16365                    currentLength += GetEncodedLength(headerPair.Value);
 16366                    currentLength += s_crlfLength;
 367                }
 368
 8369                currentLength += s_crlfLength;
 370
 371                // Content.
 8372                if (!content.RequestContent.TryComputeLength(out long tempContentLength))
 373                {
 0374                    length = 0;
 0375                    return false;
 376                }
 8377                currentLength += tempContentLength;
 378            }
 379
 380            // Terminating boundary.
 4381            currentLength += s_crlfLength + s_dashDashLength + boundaryLength + s_dashDashLength + s_crlfLength;
 382
 4383            length = currentLength;
 4384            return true;
 0385        }
 386
 387        private static int GetEncodedLength(string input)
 388        {
 42389            return Encoding.Default.GetByteCount(input);
 390        }
 391
 392        #endregion Serialization
 393
 394        private class MultipartRequestContent
 395        {
 396            public readonly RequestContent RequestContent;
 397            public Dictionary<string, string> Headers;
 398
 26399            public MultipartRequestContent(RequestContent content, Dictionary<string, string> headers)
 400            {
 26401                RequestContent = content;
 26402                Headers = headers;
 26403            }
 404        }
 405    }
 406}