diff --git a/src/Nancy.MSBuild/Nancy.csproj b/src/Nancy.MSBuild/Nancy.csproj
index 01e96c1510..5cff659f7b 100644
--- a/src/Nancy.MSBuild/Nancy.csproj
+++ b/src/Nancy.MSBuild/Nancy.csproj
@@ -1004,15 +1004,6 @@
HttpFile.cs
-
- HttpMultipart.cs
-
-
- HttpMultipartBoundary.cs
-
-
- HttpMultipartBuffer.cs
-
HttpStatusCode.cs
@@ -1205,9 +1196,6 @@
Session\Session.cs
-
- HttpMultipartSubStream.cs
-
TinyIoc\TinyIoC.cs
@@ -1364,6 +1352,33 @@
Configuration\ConfigurationException.cs
+
+ HttpMultipart\BinaryStreamStack.cs
+
+
+ HttpMultipart\FilePart.cs
+
+
+ HttpMultipart\MultipartFormDataParser.cs
+
+
+ HttpMultipart\MultipartParseException.cs
+
+
+ HttpMultipart\MultipartStreamPart.cs
+
+
+ HttpMultipart\ParameterPart.cs
+
+
+ HttpMultipart\RebufferableBinaryReader.cs
+
+
+ HttpMultipart\StreamingMultipartFormDataParser.cs
+
+
+ HttpMultipart\SubsequenceFinder.cs
+
diff --git a/src/Nancy/HttpFile.cs b/src/Nancy/HttpFile.cs
index 0435d8293a..1ededc9dda 100644
--- a/src/Nancy/HttpFile.cs
+++ b/src/Nancy/HttpFile.cs
@@ -7,16 +7,6 @@
///
public class HttpFile
{
- ///
- /// Initializes a new instance of the class,
- /// using the provided .
- ///
- /// The that contains the file information.
- public HttpFile(HttpMultipartBoundary boundary)
- : this(boundary.ContentType, boundary.Filename, boundary.Value, boundary.Name)
- {
- }
-
///
/// Initializes a new instance of the class,
/// using the provided values
diff --git a/src/Nancy/HttpMultipart/BinaryStreamStack.cs b/src/Nancy/HttpMultipart/BinaryStreamStack.cs
new file mode 100644
index 0000000000..759cc91ed5
--- /dev/null
+++ b/src/Nancy/HttpMultipart/BinaryStreamStack.cs
@@ -0,0 +1,404 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+//
+// Provides character based and byte based stream-like read operations over multiple
+// streams and provides methods to add data to the front of the buffer.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Nancy.HttpMultipart //HttpMultipartParser
+{
+ ///
+ /// Provides character based and byte based stream-like read operations over multiple
+ /// streams and provides methods to add data to the front of the buffer.
+ ///
+ internal class BinaryStreamStack
+ {
+ #region Fields
+
+ ///
+ /// Holds the streams to read from, the stream on the top of the
+ /// stack will be read first.
+ ///
+ private readonly Stack streams = new Stack();
+
+ #endregion
+
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class with the default
+ /// encoding of UTF8.
+ ///
+ public BinaryStreamStack()
+ : this(Encoding.UTF8)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The encoding to use for character based operations.
+ ///
+ public BinaryStreamStack(Encoding encoding)
+ {
+ CurrentEncoding = encoding;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets or sets the current encoding.
+ ///
+ public Encoding CurrentEncoding { get; set; }
+
+ #endregion
+
+ #region Public Methods and Operators
+
+ ///
+ /// Returns true if there is any data left to read.
+ ///
+ ///
+ /// True or false.
+ ///
+ public bool HasData()
+ {
+ return streams.Any();
+ }
+
+ ///
+ /// Returns the reader on the top of the stack but does not remove it.
+ ///
+ ///
+ /// The .
+ ///
+ public BinaryReader Peek()
+ {
+ return streams.Peek();
+ }
+
+ ///
+ /// Returns the reader on the top of the stack and removes it
+ ///
+ ///
+ /// The .
+ ///
+ public BinaryReader Pop()
+ {
+ return streams.Pop();
+ }
+
+ ///
+ /// Pushes data to the front of the stack. The most recently pushed data will
+ /// be read first.
+ ///
+ ///
+ /// The data to add to the stack.
+ ///
+ public void Push(byte[] data)
+ {
+ streams.Push(new BinaryReader(new MemoryStream(data), CurrentEncoding));
+ }
+
+ ///
+ /// Reads a single byte as an integer from the stack. Returns -1 if no
+ /// data is left to read.
+ ///
+ ///
+ /// The that was read.
+ ///
+ public int Read()
+ {
+ BinaryReader top = streams.Peek();
+
+ int value;
+ while ((value = top.Read()) == -1)
+ {
+ top.Dispose();
+ streams.Pop();
+
+ if (!streams.Any())
+ {
+ return -1;
+ }
+
+ top = streams.Peek();
+ }
+
+ return value;
+ }
+
+ ///
+ /// Reads the specified number of bytes from the stack, starting from a specified point in the byte array.
+ ///
+ ///
+ /// The buffer to read data into.
+ ///
+ ///
+ /// The index of buffer to start reading into.
+ ///
+ ///
+ /// The number of bytes to read into the buffer.
+ ///
+ ///
+ /// The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available,
+ /// or it might be zero if the end of the stream is reached.
+ ///
+ public int Read(byte[] buffer, int index, int count)
+ {
+ if (!HasData())
+ {
+ return 0;
+ }
+
+ // Read through all the stream untill we exhaust them
+ // or untill count is satisfied
+ int amountRead = 0;
+ BinaryReader top = streams.Peek();
+ while (amountRead < count && streams.Any())
+ {
+ int read = top.Read(buffer, index + amountRead, count - amountRead);
+ if (read == 0)
+ {
+ if ((top = NextStream()) == null)
+ {
+ return amountRead;
+ }
+ }
+ else
+ {
+ amountRead += read;
+ }
+ }
+
+ return amountRead;
+ }
+
+ ///
+ /// Reads the specified number of characters from the stack, starting from a specified point in the byte array.
+ ///
+ ///
+ /// The buffer to read data into.
+ ///
+ ///
+ /// The index of buffer to start reading into.
+ ///
+ ///
+ /// The number of characters to read into the buffer.
+ ///
+ ///
+ /// The number of characters read into buffer. This might be less than the number of bytes requested if that many bytes are not available,
+ /// or it might be zero if the end of the stream is reached.
+ ///
+ public int Read(char[] buffer, int index, int count)
+ {
+ if (!HasData())
+ {
+ return 0;
+ }
+
+ // Read through all the stream untill we exhaust them
+ // or untill count is satisfied
+ int amountRead = 0;
+ BinaryReader top = streams.Peek();
+ while (amountRead < count && streams.Any())
+ {
+ int read = top.Read(buffer, index + amountRead, count - amountRead);
+ if (read == 0)
+ {
+ if ((top = NextStream()) == null)
+ {
+ return amountRead;
+ }
+ }
+ else
+ {
+ amountRead += read;
+ }
+ }
+
+ return amountRead;
+ }
+
+ ///
+ /// Reads the specified number of characters from the stack, starting from a specified point in the byte array.
+ ///
+ ///
+ /// A byte array containing all the data up to but not including the next newline in the stack.
+ ///
+ public byte[] ReadByteLine()
+ {
+ bool dummy;
+ return ReadByteLine(out dummy);
+ }
+
+ ///
+ /// Reads a line from the stack delimited by the newline for this platform. The newline
+ /// characters will not be included in the stream
+ ///
+ ///
+ /// This will be set to true if we did not end on a newline but instead found the end of
+ /// our data.
+ ///
+ ///
+ /// The containing the line.
+ ///
+ public byte[] ReadByteLine(out bool hitStreamEnd)
+ {
+ hitStreamEnd = false;
+ if (!HasData())
+ {
+ // No streams, no data!
+ return null;
+ }
+
+ // This is horribly inefficient, consider profiling here if
+ // it becomes an issue.
+ BinaryReader top = streams.Peek();
+ byte[] ignore = CurrentEncoding.GetBytes(new[] {'\r'});
+ byte[] search = CurrentEncoding.GetBytes(new[] {'\n'});
+ int searchPos = 0;
+ var builder = new MemoryStream();
+
+ while (true)
+ {
+ // First we need to read a byte from one of the streams
+ var bytes = new byte[search.Length];
+ int amountRead = top.Read(bytes, 0, bytes.Length);
+ while (amountRead == 0)
+ {
+ streams.Pop();
+ if (!streams.Any())
+ {
+ hitStreamEnd = true;
+ return builder.ToArray();
+ }
+
+ top.Dispose();
+ top = streams.Peek();
+ amountRead = top.Read(bytes, 0, bytes.Length);
+ }
+
+ // Now we've got some bytes, we need to check it against the search array.
+ foreach (byte b in bytes)
+ {
+ if (ignore.Contains(b))
+ {
+ continue;
+ }
+
+ if (b == search[searchPos])
+ {
+ searchPos += 1;
+ }
+ else
+ {
+ // We only want to append the information if it's
+ // not part of the newline sequence
+ if (searchPos != 0)
+ {
+ byte[] append = search.Take(searchPos).ToArray();
+ builder.Write(append, 0, append.Length);
+ }
+
+ builder.Write(new[] {b}, 0, 1);
+ searchPos = 0;
+ }
+
+ // Finally if we've found our string
+ if (searchPos == search.Length)
+ {
+ return builder.ToArray();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Reads a line from the stack delimited by the newline for this platform. The newline
+ /// characters will not be included in the stream
+ ///
+ ///
+ /// The containing the line.
+ ///
+ public string ReadLine()
+ {
+ bool dummy;
+ return ReadLine(out dummy);
+ }
+
+ ///
+ /// Reads a line from the stack delimited by the newline for this platform. The newline
+ /// characters will not be included in the stream
+ ///
+ ///
+ /// This will be set to true if we did not end on a newline but instead found the end of
+ /// our data.
+ ///
+ ///
+ /// The containing the line.
+ ///
+ public string ReadLine(out bool hitStreamEnd)
+ {
+ bool foundEnd;
+ byte[] result = ReadByteLine(out foundEnd);
+ hitStreamEnd = foundEnd;
+
+ if (result == null)
+ {
+ return null;
+ }
+
+ return CurrentEncoding.GetString(result);
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Removes the current reader from the stack and ensures it is correctly
+ /// destroyed and then returns the next available reader. If no reader
+ /// is available this method returns null.
+ ///
+ ///
+ /// The next reader.
+ ///
+ private BinaryReader NextStream()
+ {
+ BinaryReader top = streams.Pop();
+ top.Dispose();
+
+ return streams.Any() ? streams.Peek() : null;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/FilePart.cs b/src/Nancy/HttpMultipart/FilePart.cs
new file mode 100644
index 0000000000..c6a148680b
--- /dev/null
+++ b/src/Nancy/HttpMultipart/FilePart.cs
@@ -0,0 +1,115 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+//
+// Represents a single file extracted from a multipart/form-data
+// stream.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+using System.IO;
+using System.Linq;
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Represents a single file extracted from a multipart/form-data
+ /// stream.
+ ///
+ public class FilePart
+ {
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The name of the input field used for the upload.
+ ///
+ ///
+ /// The name of the file.
+ ///
+ ///
+ /// The file data.
+ ///
+ public FilePart(string name, string fileName, Stream data) :
+ this(name, fileName, data, "text/plain", "form-data")
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The name of the input field used for the upload.
+ ///
+ ///
+ /// The name of the file.
+ ///
+ ///
+ /// The file data.
+ ///
+ ///
+ /// The content type.
+ ///
+ ///
+ /// The content disposition.
+ ///
+ public FilePart(string name, string fileName, Stream data, string contentType, string contentDisposition)
+ {
+ Name = name;
+ FileName = fileName.Split(Path.GetInvalidFileNameChars()).Last();
+ Data = data;
+ ContentType = contentType;
+ ContentDisposition = contentDisposition;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets the data.
+ ///
+ public Stream Data { get; private set; }
+
+ ///
+ /// Gets or sets the file name.
+ ///
+ public string FileName { get; set; }
+
+ ///
+ /// Gets or sets the name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the content-type. Defaults to text/plain if unspecified.
+ ///
+ public string ContentType { get; set; }
+
+ ///
+ /// Gets or sets the content-disposition. Defaults to form-data if unspecified.
+ ///
+ public string ContentDisposition { get; set; }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/MultipartFormDataParser.cs b/src/Nancy/HttpMultipart/MultipartFormDataParser.cs
new file mode 100644
index 0000000000..40015a4c78
--- /dev/null
+++ b/src/Nancy/HttpMultipart/MultipartFormDataParser.cs
@@ -0,0 +1,285 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+// --------------------------------------------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Linq;
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Provides methods to parse a
+ ///
+ /// multipart/form-data
+ ///
+ /// stream into it's parameters and file data.
+ ///
+ ///
+ ///
+ /// A parameter is defined as any non-file data passed in the multipart stream. For example
+ /// any form fields would be considered a parameter.
+ ///
+ ///
+ /// The parser determines if a section is a file or not based on the presence or absence
+ /// of the filename argument for the Content-Type header. If filename is set then the section
+ /// is assumed to be a file, otherwise it is assumed to be parameter data.
+ ///
+ ///
+ ///
+ ///
+ /// Stream multipartStream = GetTheMultipartStream();
+ /// string boundary = GetTheBoundary();
+ /// var parser = new MultipartFormDataParser(multipartStream, boundary, Encoding.UTF8);
+ ///
+ /// // Grab the parameters (non-file data). Key is based on the name field
+ /// var username = parser.Parameters["username"].Data;
+ /// var password = parser.parameters["password"].Data;
+ ///
+ /// // Grab the first files data
+ /// var file = parser.Files.First();
+ /// var filename = file.FileName;
+ /// var filestream = file.Data;
+ ///
+ ///
+ /// // In the context of WCF you can get the boundary from the HTTP
+ /// // request
+ /// public ResponseClass MyMethod(Stream multipartData)
+ /// {
+ /// // First we need to get the boundary from the header, this is sent
+ /// // with the HTTP request. We can do that in WCF using the WebOperationConext:
+ /// var type = WebOperationContext.Current.IncomingRequest.Headers["Content-Type"];
+ ///
+ /// // Now we want to strip the boundary out of the Content-Type, currently the string
+ /// // looks like: "multipart/form-data; boundary=---------------------124123qase124"
+ /// var boundary = type.Substring(type.IndexOf('=')+1);
+ ///
+ /// // Now that we've got the boundary we can parse our multipart and use it as normal
+ /// var parser = new MultipartFormDataParser(data, boundary, Encoding.UTF8);
+ ///
+ /// ...
+ /// }
+ ///
+ ///
+ public class MultipartFormDataParser
+ {
+ #region Constants
+
+ ///
+ /// The default buffer size.
+ ///
+ private const int DefaultBufferSize = 4096;
+
+ #endregion
+
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class
+ /// with an input stream. Boundary will be automatically detected based on the
+ /// first line of input.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ public MultipartFormDataParser(Stream stream)
+ : this(stream, null, Encoding.UTF8, DefaultBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the boundary and input stream.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The multipart/form-data boundary. This should be the value
+ /// returned by the request header.
+ ///
+ public MultipartFormDataParser(Stream stream, string boundary)
+ : this(stream, boundary, Encoding.UTF8, DefaultBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the input stream and stream encoding. Boundary is automatically
+ /// detected.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ public MultipartFormDataParser(Stream stream, Encoding encoding)
+ : this(stream, null, encoding, DefaultBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the boundary, input stream and stream encoding.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The multipart/form-data boundary. This should be the value
+ /// returned by the request header.
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ public MultipartFormDataParser(Stream stream, string boundary, Encoding encoding)
+ : this(stream, boundary, encoding, DefaultBufferSize)
+ {
+ // 4096 is the optimal buffer size as it matches the internal buffer of a StreamReader
+ // See: http://stackoverflow.com/a/129318/203133
+ // See: http://msdn.microsoft.com/en-us/library/9kstw824.aspx (under remarks)
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the stream, input encoding and buffer size. Boundary is automatically
+ /// detected.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ ///
+ /// The size of the buffer to use for parsing the multipart form data. This must be larger
+ /// then (size of boundary + 4 + # bytes in newline).
+ ///
+ public MultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize)
+ : this(stream, null, encoding, binaryBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the boundary, stream, input encoding and buffer size.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The multipart/form-data boundary. This should be the value
+ /// returned by the request header.
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ ///
+ /// The size of the buffer to use for parsing the multipart form data. This must be larger
+ /// then (size of boundary + 4 + # bytes in newline).
+ ///
+ public MultipartFormDataParser(Stream stream, string boundary, Encoding encoding, int binaryBufferSize)
+ {
+ Files = new List();
+ Parameters = new List();
+
+ var streamingParser = new StreamingMultipartFormDataParser(stream, boundary, encoding, binaryBufferSize);
+ streamingParser.ParameterHandler += parameterPart => Parameters.Add(parameterPart);
+
+ streamingParser.FileHandler += (name, fileName, type, disposition, buffer, bytes) =>
+ {
+ if (Files.Count == 0 || name != Files[Files.Count - 1].Name)
+ {
+ Files.Add(new FilePart(name, fileName, new MemoryStream(), type, disposition));
+ }
+
+ Files[Files.Count - 1].Data.Write(buffer, 0, bytes);
+ };
+
+ streamingParser.Run();
+
+ // Reset all the written memory streams so they can be read.
+ foreach (var file in Files)
+ {
+ file.Data.Position = 0;
+ }
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets the mapping of parameters parsed files. The name of a given field
+ /// maps to the parsed file data.
+ ///
+ public List Files { get; private set; }
+
+ ///
+ /// Gets the parameters. Several ParameterParts may share the same name.
+ ///
+ public List Parameters { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Returns true if the parameter has any values. False otherwise
+ ///
+ /// The name of the parameter
+ /// True if the parameter exists. False otherwise
+ public bool HasParameter(string name)
+ {
+ return Parameters.Any(p => p.Name == name);
+ }
+
+ ///
+ /// Returns the value of a parameter or null if it doesn't exist.
+ ///
+ /// You should only use this method if you're sure the parameter has only one value.
+ ///
+ /// If you need to support multiple values use GetParameterValues.
+ ///
+ /// The name of the parameter
+ /// The value of the parameter
+ public string GetParameterValue(string name)
+ {
+ return Parameters.FirstOrDefault(p => p.Name == name).Data;
+ }
+
+ ///
+ /// Returns the values of a parameter or an empty enumerable if the parameter doesn't exist.
+ ///
+ /// The name of the parameter
+ /// The values of the parameter
+ public IEnumerable GetParameterValues(string name)
+ {
+ return Parameters
+ .Where(p => p.Name == name)
+ .Select(p => p.Data);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/MultipartParseException.cs b/src/Nancy/HttpMultipart/MultipartParseException.cs
new file mode 100644
index 0000000000..3b1d5ef0ba
--- /dev/null
+++ b/src/Nancy/HttpMultipart/MultipartParseException.cs
@@ -0,0 +1,51 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+//
+// Represents a parsing problem occurring within the MultipartFormDataParser
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+using System;
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Represents a parsing problem occurring within the MultipartFormDataParser
+ ///
+ [Serializable]
+ internal class MultipartParseException : Exception
+ {
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The message.
+ ///
+ public MultipartParseException(string message)
+ : base(message)
+ {
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/MultipartStreamPart.cs b/src/Nancy/HttpMultipart/MultipartStreamPart.cs
new file mode 100644
index 0000000000..fb14b8f379
--- /dev/null
+++ b/src/Nancy/HttpMultipart/MultipartStreamPart.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Nancy.HttpMultipart
+{
+ internal class MultipartStreamPart
+ {
+ }
+}
diff --git a/src/Nancy/HttpMultipart/ParameterPart.cs b/src/Nancy/HttpMultipart/ParameterPart.cs
new file mode 100644
index 0000000000..3ea1a71b53
--- /dev/null
+++ b/src/Nancy/HttpMultipart/ParameterPart.cs
@@ -0,0 +1,72 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+//
+// Represents a single parameter extracted from a multipart/form-data
+// stream.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Represents a single parameter extracted from a multipart/form-data
+ /// stream.
+ ///
+ ///
+ /// For our purposes a "parameter" is defined as any non-file data
+ /// in the multipart/form-data stream.
+ ///
+ public class ParameterPart
+ {
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The name.
+ ///
+ ///
+ /// The data.
+ ///
+ public ParameterPart(string name, string data)
+ {
+ Name = name;
+ Data = data;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets the data.
+ ///
+ public string Data { get; private set; }
+
+ ///
+ /// Gets or sets the name.
+ ///
+ public string Name { get; set; }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/RebufferableBinaryReader.cs b/src/Nancy/HttpMultipart/RebufferableBinaryReader.cs
new file mode 100644
index 0000000000..fa9d19c3d9
--- /dev/null
+++ b/src/Nancy/HttpMultipart/RebufferableBinaryReader.cs
@@ -0,0 +1,349 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+//
+// Provides methods to interpret and read a stream as either character or binary
+// data similar to a and provides the ability to push
+// data onto the front of the stream.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+using System.Text;
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Provides methods to interpret and read a stream as either character or binary
+ /// data similar to a and provides the ability to push
+ /// data onto the front of the stream.
+ ///
+ internal class RebufferableBinaryReader
+ {
+ #region Fields
+
+ ///
+ /// The size of the buffer to use when reading new data.
+ ///
+ private readonly int bufferSize;
+
+ ///
+ /// The encoding to use for character based operations
+ ///
+ private readonly Encoding encoding;
+
+ ///
+ /// The stream to read raw data from.
+ ///
+ private readonly Stream stream;
+
+ ///
+ /// The stream stack to store buffered data.
+ ///
+ private readonly BinaryStreamStack streamStack;
+
+ #endregion
+
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Default encoding of UTF8 will be used.
+ ///
+ ///
+ /// The input stream to read from.
+ ///
+ public RebufferableBinaryReader(Stream input)
+ : this(input, new UTF8Encoding(false))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The input stream to read from.
+ ///
+ ///
+ /// The encoding to use for character based operations.
+ ///
+ public RebufferableBinaryReader(Stream input, Encoding encoding)
+ : this(input, encoding, 4096)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The input stream to read from.
+ ///
+ ///
+ /// The encoding to use for character based operations.
+ ///
+ ///
+ /// The buffer size to use for new buffers.
+ ///
+ public RebufferableBinaryReader(Stream input, Encoding encoding, int bufferSize)
+ {
+ stream = input;
+ streamStack = new BinaryStreamStack(encoding);
+ this.encoding = encoding;
+ this.bufferSize = bufferSize;
+ }
+
+ #endregion
+
+ #region Public Methods and Operators
+
+ ///
+ /// Adds data to the front of the stream. The most recently buffered data will
+ /// be read first.
+ ///
+ ///
+ /// The data to buffer.
+ ///
+ public void Buffer(byte[] data)
+ {
+ streamStack.Push(data);
+ }
+
+ ///
+ /// Adds the string to the front of the stream. The most recently buffered data will
+ /// be read first.
+ ///
+ ///
+ /// The data.
+ ///
+ public void Buffer(string data)
+ {
+ streamStack.Push(encoding.GetBytes(data));
+ }
+
+ ///
+ /// Reads a single byte as an integer from the stream. Returns -1 if no
+ /// data is left to read.
+ ///
+ ///
+ /// The that was read.
+ ///
+ public int Read()
+ {
+ int value = -1;
+ while (value == -1)
+ {
+ if (!streamStack.HasData())
+ {
+ if (StreamData() == 0)
+ {
+ return -1;
+ }
+ }
+
+ value = streamStack.Read();
+ }
+
+ return value;
+ }
+
+ ///
+ /// Reads the specified number of bytes from the stream, starting from a
+ /// specified point in the byte array.
+ ///
+ ///
+ /// The buffer to read data into.
+ ///
+ ///
+ /// The index of buffer to start reading into.
+ ///
+ ///
+ /// The number of bytes to read into the buffer.
+ ///
+ ///
+ /// The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available,
+ /// or it might be zero if the end of the stream is reached.
+ ///
+ public int Read(byte[] buffer, int index, int count)
+ {
+ int amountRead = 0;
+ while (amountRead < count)
+ {
+ if (!streamStack.HasData())
+ {
+ if (StreamData() == 0)
+ {
+ return amountRead;
+ }
+ }
+
+ amountRead += streamStack.Read(buffer, index + amountRead, count - amountRead);
+ }
+
+ return amountRead;
+ }
+
+ ///
+ /// Reads the specified number of characters from the stream, starting from a
+ /// specified point in the byte array.
+ ///
+ ///
+ /// The buffer to read data into.
+ ///
+ ///
+ /// The index of buffer to start reading into.
+ ///
+ ///
+ /// The number of characters to read into the buffer.
+ ///
+ ///
+ /// The number of characters read into buffer. This might be less than the number of
+ /// characters requested if that many characters are not available,
+ /// or it might be zero if the end of the stream is reached.
+ ///
+ public int Read(char[] buffer, int index, int count)
+ {
+ int amountRead = 0;
+ while (amountRead < count)
+ {
+ if (!streamStack.HasData())
+ {
+ if (StreamData() == 0)
+ {
+ return amountRead;
+ }
+ }
+
+ amountRead += streamStack.Read(buffer, index + amountRead, count - amountRead);
+ }
+
+ return amountRead;
+ }
+
+ ///
+ /// Reads a series of bytes delimited by the byte encoding of newline for this platform.
+ /// the newline bytes will not be included in the return data.
+ ///
+ ///
+ /// A byte array containing all the data up to but not including the next newline in the stack.
+ ///
+ public byte[] ReadByteLine()
+ {
+ var builder = new MemoryStream();
+ while (true)
+ {
+ if (!streamStack.HasData())
+ {
+ if (StreamData() == 0)
+ {
+ return builder.Length > 0 ? builder.ToArray() : null;
+ }
+ }
+
+ bool hitStreamEnd;
+ byte[] line = streamStack.ReadByteLine(out hitStreamEnd);
+
+ builder.Write(line, 0, line.Length);
+ if (!hitStreamEnd)
+ {
+ return builder.ToArray();
+ }
+ }
+ }
+
+ ///
+ /// Reads a line from the stack delimited by the newline for this platform. The newline
+ /// characters will not be included in the stream
+ ///
+ ///
+ /// The containing the line or null if end of stream.
+ ///
+ public string ReadLine()
+ {
+ byte[] data = ReadByteLine();
+ return data == null ? null : encoding.GetString(data);
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Determines the byte order marking offset (if any) from the
+ /// given buffer.
+ ///
+ ///
+ /// The buffer to examine.
+ ///
+ ///
+ /// The representing the length of the byte order marking.
+ ///
+ private int GetBomOffset(byte[] buffer)
+ {
+ byte[] bom = encoding.GetPreamble();
+ bool usesBom = true;
+ for (int i = 0; i < bom.Length; ++i)
+ {
+ if (bom[i] != buffer[i])
+ {
+ usesBom = false;
+ }
+ }
+
+ return usesBom ? bom.Length : 0;
+ }
+
+ ///
+ /// Reads more data from the stream into the stream stack.
+ ///
+ ///
+ /// The number of bytes read into the stream stack as an
+ ///
+ private int StreamData()
+ {
+ var buffer = new byte[bufferSize];
+ int amountRead = stream.Read(buffer, 0, buffer.Length);
+
+ // We need to check if our stream is using our encodings
+ // BOM, if it is we need to jump it.
+ int bomOffset = GetBomOffset(buffer);
+
+ // Sometimes we'll get a buffer that's smaller then we expect, chop it down
+ // for the reader:
+ if (amountRead - bomOffset > 0)
+ {
+ if (amountRead != buffer.Length || bomOffset > 0)
+ {
+ var smallBuffer = new byte[amountRead - bomOffset];
+ System.Buffer.BlockCopy(buffer, bomOffset, smallBuffer, 0, amountRead - bomOffset);
+ streamStack.Push(smallBuffer);
+ }
+ else
+ {
+ streamStack.Push(buffer);
+ }
+ }
+
+ return amountRead;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/StreamingMultipartFormDataParser.cs b/src/Nancy/HttpMultipart/StreamingMultipartFormDataParser.cs
new file mode 100644
index 0000000000..ceb2b410ce
--- /dev/null
+++ b/src/Nancy/HttpMultipart/StreamingMultipartFormDataParser.cs
@@ -0,0 +1,752 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Provides methods to parse a
+ ///
+ /// multipart/form-data
+ ///
+ /// stream into it's parameters and file data.
+ ///
+ ///
+ ///
+ /// A parameter is defined as any non-file data passed in the multipart stream. For example
+ /// any form fields would be considered a parameter.
+ ///
+ ///
+ /// The parser determines if a section is a file or not based on the presence or absence
+ /// of the filename argument for the Content-Type header. If filename is set then the section
+ /// is assumed to be a file, otherwise it is assumed to be parameter data.
+ ///
+ ///
+ ///
+ ///
+ /// Stream multipartStream = GetTheMultipartStream();
+ /// string boundary = GetTheBoundary();
+ /// var parser = new StreamingMultipartFormDataParser(multipartStream, boundary, Encoding.UTF8);
+ ///
+ /// // Set up our delegates for how we want to handle recieved data.
+ /// // In our case parameters will be written to a dictionary and files
+ /// // will be written to a filestream
+ /// parser.ParameterHandler += parameter => AddToDictionary(parameter);
+ /// parser.FileHandler += (name, fileName, type, disposition, buffer, bytes) => WriteDataToFile(fileName, buffer, bytes);
+ /// parser.Run();
+ ///
+ ///
+ public class StreamingMultipartFormDataParser
+ {
+ #region Constants
+
+ ///
+ /// The default buffer size.
+ ///
+ private const int DefaultBufferSize = 4 * 1024 * 1024;
+
+ #endregion
+
+ #region Fields
+
+ ///
+ /// The stream we are parsing.
+ ///
+ private readonly Stream stream;
+
+ ///
+ /// The boundary of the multipart message as a string.
+ ///
+ private string boundary;
+
+ ///
+ /// The boundary of the multipart message as a byte string
+ /// encoded with CurrentEncoding
+ ///
+ private byte[] boundaryBinary;
+
+ ///
+ /// The end boundary of the multipart message as a string.
+ ///
+ private string endBoundary;
+
+ ///
+ /// The end boundary of the multipart message as a byte string
+ /// encoded with CurrentEncoding
+ ///
+ private byte[] endBoundaryBinary;
+
+ ///
+ /// Determines if we have consumed the end boundary binary and determines
+ /// if we are done parsing.
+ ///
+ private bool readEndBoundary;
+
+ #endregion
+
+ #region Constructors and Destructors
+
+ ///
+ /// Initializes a new instance of the class
+ /// with an input stream. Boundary will be automatically detected based on the
+ /// first line of input.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ public StreamingMultipartFormDataParser(Stream stream)
+ : this(stream, null, Encoding.UTF8, DefaultBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the boundary and input stream.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The multipart/form-data boundary. This should be the value
+ /// returned by the request header.
+ ///
+ public StreamingMultipartFormDataParser(Stream stream, string boundary)
+ : this(stream, boundary, Encoding.UTF8, DefaultBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the input stream and stream encoding. Boundary is automatically
+ /// detected.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ public StreamingMultipartFormDataParser(Stream stream, Encoding encoding)
+ : this(stream, null, encoding, DefaultBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the boundary, input stream and stream encoding.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The multipart/form-data boundary. This should be the value
+ /// returned by the request header.
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ public StreamingMultipartFormDataParser(Stream stream, string boundary, Encoding encoding)
+ : this(stream, boundary, encoding, DefaultBufferSize)
+ {
+ // 4096 is the optimal buffer size as it matches the internal buffer of a StreamReader
+ // See: http://stackoverflow.com/a/129318/203133
+ // See: http://msdn.microsoft.com/en-us/library/9kstw824.aspx (under remarks)
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the stream, input encoding and buffer size. Boundary is automatically
+ /// detected.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ ///
+ /// The size of the buffer to use for parsing the multipart form data. This must be larger
+ /// then (size of boundary + 4 + # bytes in newline).
+ ///
+ public StreamingMultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize)
+ : this(stream, null, encoding, binaryBufferSize)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the boundary, stream, input encoding and buffer size.
+ ///
+ ///
+ /// The stream containing the multipart data
+ ///
+ ///
+ /// The multipart/form-data boundary. This should be the value
+ /// returned by the request header.
+ ///
+ ///
+ /// The encoding of the multipart data
+ ///
+ ///
+ /// The size of the buffer to use for parsing the multipart form data. This must be larger
+ /// then (size of boundary + 4 + # bytes in newline).
+ ///
+ public StreamingMultipartFormDataParser(Stream stream, string boundary, Encoding encoding, int binaryBufferSize)
+ {
+ if(stream == null || stream == Stream.Null) { throw new ArgumentNullException("stream"); }
+ if(encoding == null) { throw new ArgumentNullException("encoding"); }
+
+ this.stream = stream;
+ this.boundary = boundary;
+ Encoding = encoding;
+ BinaryBufferSize = binaryBufferSize;
+ readEndBoundary = false;
+ }
+
+ #endregion
+
+ ///
+ /// Begins executing the parser. This should be called after all handlers have been set.
+ ///
+ public void Run()
+ {
+ var reader = new RebufferableBinaryReader(stream, Encoding, BinaryBufferSize);
+
+ // If we don't know the boundary now is the time to calculate it.
+ if (boundary == null)
+ {
+ boundary = DetectBoundary(reader);
+ }
+
+ // It's important to remember that the boundary given in the header has a -- appended to the start
+ // and the last one has a -- appended to the end
+ boundary = "--" + boundary;
+ endBoundary = boundary + "--";
+
+ // We add newline here because unlike reader.ReadLine() binary reading
+ // does not automatically consume the newline, we want to add it to our signature
+ // so we can automatically detect and consume newlines after the boundary
+ boundaryBinary = Encoding.GetBytes(boundary);
+ endBoundaryBinary = Encoding.GetBytes(endBoundary);
+
+ Debug.Assert(
+ BinaryBufferSize >= endBoundaryBinary.Length,
+ "binaryBufferSize must be bigger then the boundary");
+
+ Parse(reader);
+ }
+
+ #region Public Properties
+
+ ///
+ /// The FileStreamDelegate defining functions that can handle file stream data from this parser.
+ ///
+ /// Delegates can assume that the data is sequential i.e. the data recieved by any delegates will be
+ /// the data immediately following any previously recieved data.
+ ///
+ /// The name of the multipart data
+ /// The name of the file
+ /// The content type of the multipart data
+ /// The content disposition of the multipart data
+ /// Some of the data from the file (not neccecarily all of the data)
+ /// The length of data in buffer
+ public delegate void FileStreamDelegate(
+ string name, string fileName, string contentType, string contentDisposition, byte[] buffer, int bytes);
+
+ public delegate void StreamClosedDelegate();
+
+ ///
+ /// The ParameterDelegate defining functions that can handle multipart parameter data
+ ///
+ /// The parsed parameter part
+ public delegate void ParameterDelegate(ParameterPart part);
+
+ ///
+ /// Gets or sets the binary buffer size.
+ ///
+ public static int BinaryBufferSize { get; set; }
+
+ ///
+ /// Gets the encoding.
+ ///
+ public Encoding Encoding { get; private set; }
+
+ ///
+ /// The FileHandler. Delegates attached to this property will recieve sequential file stream data from this parser.
+ ///
+ public FileStreamDelegate FileHandler { get; set; }
+
+ ///
+ /// The ParameterHandler. Delegates attached to this property will recieve parameter data.
+ ///
+ public ParameterDelegate ParameterHandler { get; set; }
+
+ ///
+ /// The StreamClosedHandler. Delegates attached to this property will be notified when the source stream is exhausted.
+ ///
+ public StreamClosedDelegate StreamClosedHandler { get; set; }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Detects the boundary from the input stream. Assumes that the
+ /// current position of the reader is the start of the file and therefore
+ /// the beginning of the boundary.
+ ///
+ ///
+ /// The binary reader to parse
+ ///
+ ///
+ /// The boundary string
+ ///
+ private static string DetectBoundary(RebufferableBinaryReader reader)
+ {
+ // Presumably the boundary is --|||||||||||||| where -- is the stuff added on to
+ // the front as per the protocol and ||||||||||||| is the part we care about.
+ string boundary = string.Concat(reader.ReadLine().Skip(2));
+ reader.Buffer("--" + boundary + "\n");
+ return boundary;
+ }
+
+ ///
+ /// Finds the next sequence of newlines in the input stream.
+ ///
+ /// The data to search
+ /// The offset to start searching at
+ /// The maximum number of bytes (starting from offset) to search.
+ /// The offset of the next newline
+ private int FindNextNewline(ref byte[] data, int offset, int maxBytes)
+ {
+ byte[][] newlinePatterns = {Encoding.GetBytes("\r\n"), Encoding.GetBytes("\n")};
+ Array.Sort(newlinePatterns, (first, second) => second.Length.CompareTo(first.Length));
+
+ byte[] dataRef = data;
+ if (offset != 0)
+ {
+ dataRef = data.Skip(offset).ToArray();
+ }
+
+ foreach (var pattern in newlinePatterns)
+ {
+ int position = SubsequenceFinder.Search(dataRef, pattern, maxBytes);
+ if (position != -1)
+ {
+ return position + offset;
+ }
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Calculates the length of the next found newline.
+ /// data[offset] is the start of the space to search.
+ ///
+ ///
+ /// The data containing the newline
+ ///
+ ///
+ /// The offset of the start of the newline
+ ///
+ ///
+ /// The length in bytes of the newline sequence
+ ///
+ private int CalculateNewlineLength(ref byte[] data, int offset)
+ {
+ byte[][] newlinePatterns = {Encoding.GetBytes("\r\n"), Encoding.GetBytes("\n")};
+
+ // Go through each pattern and find which one matches.
+ foreach (var pattern in newlinePatterns)
+ {
+ bool found = false;
+ for (int i = 0; i < pattern.Length; ++i)
+ {
+ if (pattern[i] != data[offset + i])
+ {
+ found = false;
+ break;
+ }
+
+ found = true;
+ }
+
+ if (found)
+ {
+ return pattern.Length;
+ }
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Begins the parsing of the stream into objects.
+ ///
+ ///
+ /// The multipart/form-data binary reader to parse from.
+ ///
+ ///
+ /// thrown on finding unexpected data such as a boundary before we are ready for one.
+ ///
+ private void Parse(RebufferableBinaryReader reader)
+ {
+ // Parsing references include:
+ // RFC1341 section 7: http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
+ // RFC2388: http://www.ietf.org/rfc/rfc2388.txt
+
+ // First we need to read untill we find a boundary
+ while (true)
+ {
+ string line = reader.ReadLine();
+ if (line == boundary)
+ {
+ break;
+ }
+
+ if (line == null)
+ {
+ throw new MultipartParseException("Could not find expected boundary");
+ }
+ }
+
+ // Now that we've found the initial boundary we know where to start.
+ // We need parse each individual section
+ while (!readEndBoundary)
+ {
+ // ParseSection will parse up to and including
+ // the next boundary.
+ ParseSection(reader);
+ }
+
+ if (StreamClosedHandler != null)
+ {
+ StreamClosedHandler();
+ }
+ }
+
+ ///
+ /// Parses a section of the stream that is known to be file data.
+ ///
+ ///
+ /// The header parameters of this file, expects "name" and "filename" to be valid keys
+ ///
+ ///
+ /// The StreamReader to read the data from
+ ///
+ ///
+ /// The con[] taining the parsed data (name, filename, stream containing file).
+ ///
+ private void ParseFilePart(Dictionary parameters, RebufferableBinaryReader reader)
+ {
+ string name = parameters["name"];
+ string filename = parameters["filename"];
+ string contentType = parameters.ContainsKey("content-type") ? parameters["content-type"] : "text/plain";
+ string contentDisposition = parameters.ContainsKey("content-disposition")
+ ? parameters["content-disposition"]
+ : "form-data";
+
+ // We want to create a stream and fill it with the data from the
+ // file.
+ var curBuffer = new byte[BinaryBufferSize];
+ var prevBuffer = new byte[BinaryBufferSize];
+ var fullBuffer = new byte[BinaryBufferSize*2];
+ int curLength = 0;
+ int prevLength = 0;
+ int fullLength = 0;
+
+ prevLength = reader.Read(prevBuffer, 0, prevBuffer.Length);
+ do
+ {
+ curLength = reader.Read(curBuffer, 0, curBuffer.Length);
+
+ // Combine both buffers into the fullBuffer
+ // See: http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp
+ Buffer.BlockCopy(prevBuffer, 0, fullBuffer, 0, prevLength);
+ Buffer.BlockCopy(curBuffer, 0, fullBuffer, prevLength, curLength);
+ fullLength = prevLength + curLength;
+
+ // Now we want to check for a substring within the current buffer.
+ // We need to find the closest substring greedily. That is find the
+ // closest boundary and don't miss the end --'s if it's an end boundary.
+ int endBoundaryPos = SubsequenceFinder.Search(fullBuffer, endBoundaryBinary, fullLength);
+ int endBoundaryLength = endBoundaryBinary.Length;
+
+ int boundaryPos = SubsequenceFinder.Search(fullBuffer, boundaryBinary, fullLength);
+ int boundaryLength = boundaryBinary.Length;
+
+ // If the boundaryPos is exactly at the end of our full buffer then ignore it as it could
+ // actually be a endBoundary that's had the '--' chopped off by the buffer.
+ if(boundaryPos + boundaryLength == fullLength)
+ {
+ boundaryPos = -1;
+ }
+
+ // We need to select the appropriate position and length
+ // based on the smallest non-negative position.
+ int endPos = -1;
+ int endPosLength = 0;
+
+ if (endBoundaryPos >= 0 && boundaryPos >= 0)
+ {
+ if (boundaryPos < endBoundaryPos)
+ {
+ // Select boundary
+ endPos = boundaryPos;
+ endPosLength = boundaryLength;
+ }
+ else
+ {
+ // Select end boundary
+ endPos = endBoundaryPos;
+ endPosLength = endBoundaryLength;
+ readEndBoundary = true;
+ }
+ }
+ else if (boundaryPos >= 0 && endBoundaryPos < 0)
+ {
+ // Select boundary
+ endPos = boundaryPos;
+ endPosLength = boundaryLength;
+ }
+ else if (boundaryPos < 0 && endBoundaryPos >= 0)
+ {
+ // Select end boundary
+ endPos = endBoundaryPos;
+ endPosLength = endBoundaryLength;
+ readEndBoundary = true;
+ }
+
+ if (endPos != -1)
+ {
+ // Now we need to check if the endPos is followed by \r\n or just \n. HTTP
+ // specifies \r\n but some clients might encode with \n. Or we might get 0 if
+ // we are at the end of the file.
+ int boundaryNewlineOffset = CalculateNewlineLength(ref fullBuffer,
+ Math.Min(fullLength - 1,
+ endPos + endPosLength));
+
+ // We also need to check if the last n characters of the buffer to write
+ // are a newline and if they are ignore them.
+ int maxNewlineBytes = Encoding.GetMaxByteCount(2);
+ int bufferNewlineOffset = FindNextNewline(
+ ref fullBuffer, Math.Max(0, endPos - maxNewlineBytes), maxNewlineBytes);
+ int bufferNewlineLength = CalculateNewlineLength(ref fullBuffer, bufferNewlineOffset);
+
+ // We've found an end. We need to consume all the binary up to it
+ // and then write the remainder back to the original stream. Then we
+ // need to modify the original streams position to take into account
+ // the new data.
+ // We also want to chop off the newline that is inserted by the protocl.
+ // We can do this by reducing endPos by the length of newline in this environment
+ // and encoding
+ FileHandler(name, filename, contentType, contentDisposition, fullBuffer,
+ endPos - bufferNewlineLength);
+
+ int writeBackOffset = endPos + endPosLength + boundaryNewlineOffset;
+ int writeBackAmount = (prevLength + curLength) - writeBackOffset;
+ var writeBackBuffer = new byte[writeBackAmount];
+ Buffer.BlockCopy(fullBuffer, writeBackOffset, writeBackBuffer, 0, writeBackAmount);
+ reader.Buffer(writeBackBuffer);
+
+ break;
+ }
+
+ // No end, consume the entire previous buffer
+ FileHandler(name, filename, contentType, contentDisposition, prevBuffer, prevLength);
+
+ // Now we want to swap the two buffers, we don't care
+ // what happens to the data from prevBuffer so we set
+ // curBuffer to it so it gets overwrited.
+ byte[] tempBuffer = curBuffer;
+ curBuffer = prevBuffer;
+ prevBuffer = tempBuffer;
+
+ // We don't need to swap the lengths because
+ // curLength will be overwritten in the next
+ // iteration of the loop.
+ prevLength = curLength;
+ } while (prevLength != 0);
+ }
+
+ ///
+ /// Parses a section of the stream that is known to be parameter data.
+ ///
+ ///
+ /// The header parameters of this section. "name" must be a valid key.
+ ///
+ ///
+ /// The StreamReader to read the data from
+ ///
+ ///
+ /// The containing the parsed data (name, value).
+ ///
+ ///
+ /// thrown if unexpected data is found such as running out of stream before hitting the boundary.
+ ///
+ private void ParseParameterPart(Dictionary parameters, RebufferableBinaryReader reader)
+ {
+ // Our job is to get the actual "data" part of the parameter and construct
+ // an actual ParameterPart object with it. All we need to do is read data into a string
+ // untill we hit the boundary
+ var data = new StringBuilder();
+ bool firstTime = true;
+ string line = reader.ReadLine();
+ while (line != boundary && line != endBoundary)
+ {
+ if (line == null)
+ {
+ throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
+ }
+
+ if (firstTime)
+ {
+ data.Append(line);
+ firstTime = false;
+ }
+ else
+ {
+ data.Append(Environment.NewLine);
+ data.Append(line);
+ }
+ line = reader.ReadLine();
+ }
+
+ if (line == endBoundary)
+ {
+ readEndBoundary = true;
+ }
+
+ // If we're here we've hit the boundary and have the data!
+ var part = new ParameterPart(parameters["name"], data.ToString());
+ ParameterHandler(part);
+ }
+
+ ///
+ /// Parses the header of the next section of the multipart stream and
+ /// determines if it contains file data or parameter data.
+ ///
+ ///
+ /// The StreamReader to read data from.
+ ///
+ ///
+ /// thrown if unexpected data is hit such as end of stream.
+ ///
+ private void ParseSection(RebufferableBinaryReader reader)
+ {
+ // Our first job is to determine what type of section this is: form data or file.
+ // This is a bit tricky because files can still be encoded with Content-Disposition: form-data
+ // in the case of single file uploads. Multi-file uploads have Content-Disposition: file according
+ // to the spec however in practise it seems that multiple files will be represented by
+ // multiple Content-Disposition: form-data files.
+ var parameters = new Dictionary();
+
+ string line = reader.ReadLine();
+ while (line != string.Empty)
+ {
+ if (line == null)
+ {
+ throw new MultipartParseException("Unexpected end of stream");
+ }
+
+ if (line == boundary || line == endBoundary)
+ {
+ throw new MultipartParseException("Unexpected end of section");
+ }
+
+
+ // This line parses the header values into a set of key/value pairs. For example:
+ // Content-Disposition: form-data; name="textdata"
+ // ["content-disposition"] = "form-data"
+ // ["name"] = "textdata"
+ // Content-Disposition: form-data; name="file"; filename="data.txt"
+ // ["content-disposition"] = "form-data"
+ // ["name"] = "file"
+ // ["filename"] = "data.txt"
+ // Content-Type: text/plain
+ // ["content-type"] = "text/plain"
+ Dictionary values = SplitBySemicolonIgnoringSemicolonsInQuotes(line)
+ .Select(x => x.Split(new[] {':', '='}, 2))
+ // select where the length of the array is equal to two, that way if it is only one it will
+ // be ignored as it is invalid key-pair
+ .Where(x=> x.Length == 2)
+ // Limit split to 2 splits so we don't accidently split characters in file paths.
+ .ToDictionary(
+ x => x[0].Trim().Replace("\"", string.Empty).ToLower(),
+ x => x[1].Trim().Replace("\"", string.Empty));
+
+
+ // Here we just want to push all the values that we just retrieved into the
+ // parameters dictionary.
+ try
+ {
+ foreach (var pair in values)
+ {
+ parameters.Add(pair.Key, pair.Value);
+ }
+ }
+ catch (ArgumentException)
+ {
+ throw new MultipartParseException("Duplicate field in section");
+ }
+
+ line = reader.ReadLine();
+ }
+
+ // Now that we've consumed all the parameters we're up to the body. We're going to do
+ // different things depending on if we're parsing a, relatively small, form value or a
+ // potentially large file.
+ if (parameters.ContainsKey("filename"))
+ {
+ // Right now we assume that if a section contains filename then it is a file.
+ // This assumption needs to be checked, it holds true in firefox but is untested for other
+ // browsers.
+ ParseFilePart(parameters, reader);
+ }
+ else
+ {
+ ParseParameterPart(parameters, reader);
+ }
+ }
+
+ ///
+ /// Splits a line by semicolons but ignores semicolons in quotes.
+ ///
+ /// The line to split
+ /// The split strings
+ private IEnumerable SplitBySemicolonIgnoringSemicolonsInQuotes(string line)
+ {
+ // Loop over the line looking for a semicolon. Keep track of if we're currently inside quotes
+ // and if we are don't treat a semicolon as a splitting character.
+ bool inQuotes = false;
+ string workingString = "";
+
+ foreach (char c in line)
+ {
+ if (c == '"')
+ {
+ inQuotes = !inQuotes;
+ }
+
+ if (c == ';' && !inQuotes)
+ {
+ yield return workingString;
+ workingString = "";
+ }
+ else
+ {
+ workingString += c;
+ }
+ }
+
+ yield return workingString;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/HttpMultipart/SubsequenceFinder.cs b/src/Nancy/HttpMultipart/SubsequenceFinder.cs
new file mode 100644
index 0000000000..cddf826950
--- /dev/null
+++ b/src/Nancy/HttpMultipart/SubsequenceFinder.cs
@@ -0,0 +1,97 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2013 Jake Woods
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies
+// or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Jake Woods
+//
+// Provides methods to find a subsequence within a
+// sequence.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Nancy.HttpMultipart
+{
+ ///
+ /// Provides methods to find a subsequence within a
+ /// sequence.
+ ///
+ internal class SubsequenceFinder
+ {
+ #region Public Methods and Operators
+
+ public static int Search(byte[] haystack, byte[] needle)
+ {
+ return Search(haystack, needle, haystack.Length);
+ }
+
+ ///
+ /// Finds if a sequence exists within another sequence.
+ ///
+ ///
+ /// The sequence to search
+ ///
+ ///
+ /// The sequence to look for
+ ///
+ ///
+ /// The length of the haystack to consider for searching
+ ///
+ ///
+ /// The start position of the found sequence or -1 if nothing was found
+ ///
+ public static int Search(byte[] haystack, byte[] needle, int haystackLength)
+ {
+ var charactersInNeedle = new HashSet(needle);
+
+ var length = needle.Length;
+ var index = 0;
+ while (index + length <= haystackLength)
+ {
+ // Worst case scenario: Go back to character-by-character parsing until we find a non-match
+ // or we find the needle.
+ if (charactersInNeedle.Contains(haystack[index + length - 1]))
+ {
+ var needleIndex = 0;
+ while (haystack[index + needleIndex] == needle[needleIndex])
+ {
+ if (needleIndex == needle.Length - 1)
+ {
+ // Found our match!
+ return index;
+ }
+
+ needleIndex += 1;
+ }
+
+ index += 1;
+ index += needleIndex;
+ continue;
+ }
+
+ index += length;
+ }
+
+ return -1;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Nancy/Request.cs b/src/Nancy/Request.cs
index 32eb004251..be480a8817 100644
--- a/src/Nancy/Request.cs
+++ b/src/Nancy/Request.cs
@@ -1,3 +1,5 @@
+using Nancy.HttpMultipart;
+
namespace Nancy
{
using System;
@@ -230,6 +232,9 @@ public dynamic Form
public void Dispose()
{
((IDisposable)this.Body).Dispose();
+ foreach (var item in this.Files) {
+ item.Value.Close ();
+ }
}
private void ParseFormData()
@@ -253,26 +258,26 @@ private void ParseFormData()
return;
}
- var boundary = Regex.Match(contentType, @"boundary=""?(?[^\n\;\"" ]*)").Groups["token"].Value;
- var multipart = new HttpMultipart(this.Body, boundary);
-
- var formValues =
+ var formValues =
new NameValueCollection(StaticConfiguration.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
- foreach (var httpMultipartBoundary in multipart.GetBoundaries())
- {
- if (string.IsNullOrEmpty(httpMultipartBoundary.Filename))
- {
- var reader =
- new StreamReader(httpMultipartBoundary.Value);
- formValues.Add(httpMultipartBoundary.Name, reader.ReadToEnd());
+ var filestreamsByName = new Dictionary ();
- }
- else
+ var parser = new StreamingMultipartFormDataParser(this.Body);
+ parser.FileHandler += (string name, string fileName, string _contentType, string contentDisposition, byte[] buffer, int bytes) => {
+ if (!filestreamsByName.ContainsKey(fileName))
{
- this.files.Add(new HttpFile(httpMultipartBoundary));
+ string f = System.IO.Path.GetTempFileName();
+ var fs = System.IO.File.Create (f, bytes, System.IO.FileOptions.DeleteOnClose);
+ filestreamsByName.Add(fileName, fs);
+ this.files.Add(new HttpFile(contentType, fileName, fs, name));
}
- }
+ filestreamsByName[fileName].Write(buffer, 0, bytes);
+ };
+ parser.ParameterHandler += (ParameterPart part) => {
+ formValues.Add(part.Name, part.Data);
+ };
+ parser.Run();
foreach (var key in formValues.AllKeys.Where(key => key != null))
{