// 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; }
}
}
}