// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Net.Http.Formatting.Parsers; using System.Threading.Tasks; namespace System.Net.Http { /// /// Extension methods to read MIME multipart entities from instances. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class HttpContentMultipartExtensions { private const int MinBufferSize = 256; private const int DefaultBufferSize = 32 * 1024; private static readonly AsyncCallback _onMultipartReadAsyncComplete = new AsyncCallback(OnMultipartReadAsyncComplete); private static readonly AsyncCallback _onMultipartWriteSegmentAsyncComplete = new AsyncCallback(OnMultipartWriteSegmentAsyncComplete); /// /// Determines whether the specified content is MIME multipart content. /// /// The content. /// /// true if the specified content is MIME multipart content; otherwise, false. /// public static bool IsMimeMultipartContent(this HttpContent content) { if (content == null) { throw new ArgumentNullException("content"); } return MimeMultipartBodyPartParser.IsMimeMultipartContent(content); } /// /// Determines whether the specified content is MIME multipart content with the /// specified subtype. For example, the subtype mixed would match content /// with a content type of multipart/mixed. /// /// The content. /// The MIME multipart subtype to match. /// /// true if the specified content is MIME multipart content with the specified subtype; otherwise, false. /// public static bool IsMimeMultipartContent(this HttpContent content, string subtype) { if (String.IsNullOrWhiteSpace(subtype)) { throw new ArgumentNullException("subtype"); } if (IsMimeMultipartContent(content)) { if (content.Headers.ContentType.MediaType.Equals("multipart/" + subtype, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Reads all body parts within a MIME multipart message and produces a set of instances as a result. /// /// An existing instance to use for the object's content. /// A representing the tasks of getting the collection of instances where each instance represents a body part. [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting of generic types is required with Task")] public static Task> ReadAsMultipartAsync(this HttpContent content) { return ReadAsMultipartAsync(content, MultipartMemoryStreamProvider.Instance, DefaultBufferSize); } /// /// Reads all body parts within a MIME multipart message and produces a set of instances as a result /// using the instance to determine where the contents of each body part is written. /// /// An existing instance to use for the object's content. /// A stream provider providing output streams for where to write body parts as they are parsed. /// A representing the tasks of getting the collection of instances where each instance represents a body part. [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting of generic types is required with Task")] public static Task> ReadAsMultipartAsync(this HttpContent content, IMultipartStreamProvider streamProvider) { return ReadAsMultipartAsync(content, streamProvider, DefaultBufferSize); } /// /// Reads all body parts within a MIME multipart message and produces a set of instances as a result /// using the instance to determine where the contents of each body part is written and /// as read buffer size. /// /// An existing instance to use for the object's content. /// A stream provider providing output streams for where to write body parts as they are parsed. /// Size of the buffer used to read the contents. /// A representing the tasks of getting the collection of instances where each instance represents a body part. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")] [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting of generic types is required with Task")] public static Task> ReadAsMultipartAsync(this HttpContent content, IMultipartStreamProvider streamProvider, int bufferSize) { if (content == null) { throw new ArgumentNullException("content"); } if (streamProvider == null) { throw new ArgumentNullException("streamProvider"); } if (bufferSize < MinBufferSize) { throw new ArgumentOutOfRangeException("bufferSize", bufferSize, RS.Format(Properties.Resources.ArgumentMustBeGreaterThanOrEqualTo, MinBufferSize)); } return content.ReadAsStreamAsync().Then(stream => { TaskCompletionSource> taskCompletionSource = new TaskCompletionSource>(); MimeMultipartBodyPartParser parser = new MimeMultipartBodyPartParser(content, streamProvider); byte[] data = new byte[bufferSize]; MultipartAsyncContext context = new MultipartAsyncContext(stream, taskCompletionSource, parser, data); // Start async read/write loop MultipartReadAsync(context); // Return task and complete later return taskCompletionSource.Task; }); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")] private static void MultipartReadAsync(MultipartAsyncContext context) { Contract.Assert(context != null, "context cannot be null"); IAsyncResult result = null; try { result = context.ContentStream.BeginRead(context.Data, 0, context.Data.Length, _onMultipartReadAsyncComplete, context); if (result.CompletedSynchronously) { MultipartReadAsyncComplete(result); } } catch (Exception e) { Exception exception = (result != null && result.CompletedSynchronously) ? e : new IOException(Properties.Resources.ReadAsMimeMultipartErrorReading, e); context.TaskCompletionSource.TrySetException(exception); } } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")] private static void OnMultipartReadAsyncComplete(IAsyncResult result) { if (result.CompletedSynchronously) { return; } MultipartAsyncContext context = (MultipartAsyncContext)result.AsyncState; Contract.Assert(context != null, "context cannot be null"); try { MultipartReadAsyncComplete(result); } catch (Exception e) { context.TaskCompletionSource.TrySetException(e); } } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")] private static void MultipartReadAsyncComplete(IAsyncResult result) { Contract.Assert(result != null, "result cannot be null"); MultipartAsyncContext context = (MultipartAsyncContext)result.AsyncState; int bytesRead = 0; try { bytesRead = context.ContentStream.EndRead(result); } catch (Exception e) { context.TaskCompletionSource.TrySetException(new IOException(Properties.Resources.ReadAsMimeMultipartErrorReading, e)); } IEnumerable parts = context.MimeParser.ParseBuffer(context.Data, bytesRead); context.PartsEnumerator = parts.GetEnumerator(); MoveNextPart(context); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")] private static void MultipartWriteSegmentAsync(MultipartAsyncContext context) { Contract.Assert(context != null, "context cannot be null."); Stream output = context.PartsEnumerator.Current.GetOutputStream(); ArraySegment segment = (ArraySegment)context.SegmentsEnumerator.Current; try { IAsyncResult result = output.BeginWrite(segment.Array, segment.Offset, segment.Count, _onMultipartWriteSegmentAsyncComplete, context); if (result.CompletedSynchronously) { MultipartWriteSegmentAsyncComplete(result); } } catch (Exception e) { context.PartsEnumerator.Current.Dispose(); context.TaskCompletionSource.TrySetException(new IOException(Properties.Resources.ReadAsMimeMultipartErrorWriting, e)); } } private static void OnMultipartWriteSegmentAsyncComplete(IAsyncResult result) { if (result.CompletedSynchronously) { return; } MultipartWriteSegmentAsyncComplete(result); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")] private static void MultipartWriteSegmentAsyncComplete(IAsyncResult result) { Contract.Assert(result != null, "result cannot be null."); MultipartAsyncContext context = (MultipartAsyncContext)result.AsyncState; Contract.Assert(context != null, "context cannot be null"); MimeBodyPart part = context.PartsEnumerator.Current; try { Stream output = context.PartsEnumerator.Current.GetOutputStream(); output.EndWrite(result); } catch (Exception e) { part.Dispose(); context.TaskCompletionSource.TrySetException(new IOException(Properties.Resources.ReadAsMimeMultipartErrorWriting, e)); } if (!MoveNextSegment(context)) { MoveNextPart(context); } } private static void MoveNextPart(MultipartAsyncContext context) { Contract.Assert(context != null, "context cannot be null"); while (context.PartsEnumerator.MoveNext()) { context.SegmentsEnumerator = context.PartsEnumerator.Current.Segments.GetEnumerator(); if (MoveNextSegment(context)) { return; } } // Read some more MultipartReadAsync(context); } private static bool MoveNextSegment(MultipartAsyncContext context) { Contract.Assert(context != null, "context cannot be null"); if (context.SegmentsEnumerator.MoveNext()) { MultipartWriteSegmentAsync(context); return true; } else if (CheckPartCompletion(context.PartsEnumerator.Current, context.Result)) { // We are done parsing context.TaskCompletionSource.TrySetResult(context.Result); return true; } return false; } private static bool CheckPartCompletion(MimeBodyPart part, List result) { Contract.Assert(part != null, "part cannot be null."); Contract.Assert(result != null, "result cannot be null."); if (part.IsComplete) { if (part.HttpContent != null) { result.Add(part.HttpContent); } bool isFinal = part.IsFinal; part.Dispose(); return isFinal; } return false; } /// /// Managing state for asynchronous read and write operations /// private class MultipartAsyncContext { /// /// Initializes a new instance of the class. /// /// The content stream. /// The task completion source. /// The MIME parser. /// The buffer that we read data from. public MultipartAsyncContext(Stream contentStream, TaskCompletionSource> taskCompletionSource, MimeMultipartBodyPartParser mimeParser, byte[] data) { Contract.Assert(contentStream != null, "contentStream cannot be null"); Contract.Assert(taskCompletionSource != null, "task cannot be null"); Contract.Assert(mimeParser != null, "mimeParser cannot be null"); Contract.Assert(data != null, "data cannot be null"); ContentStream = contentStream; Result = new List(); TaskCompletionSource = taskCompletionSource; MimeParser = mimeParser; Data = data; } /// /// Gets the that we read from. /// /// /// The content stream. /// public Stream ContentStream { get; private set; } /// /// Gets the collection of parsed instances. /// /// /// The result collection. /// public List Result { get; private set; } /// /// Gets the task completion source. /// /// /// The task completion source. /// public TaskCompletionSource> TaskCompletionSource { get; private set; } /// /// Gets the data. /// /// /// The buffer that we read data from. /// public byte[] Data { get; private set; } /// /// Gets the MIME parser. /// /// /// The MIME parser. /// public MimeMultipartBodyPartParser MimeParser { get; private set; } /// /// Gets or sets the parts enumerator for going through the parsed parts. /// /// /// The parts enumerator. /// public IEnumerator PartsEnumerator { get; set; } /// /// Gets or sets the segments enumerator for going through the segments within each part. /// /// /// The segments enumerator. /// public IEnumerator SegmentsEnumerator { get; set; } } } }