Skip to content

Commit 23e6bcf

Browse files
committed
Merge branch 'release/5.0.0'
2 parents a9f5ae7 + 8011893 commit 23e6bcf

12 files changed

Lines changed: 335 additions & 574 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ The parser was built for and tested on NET 4.6.1, NET 4,7,2 and NETSTANDARD 2.0.
8181
// ==== Advanced Parsing ====
8282
var parser = new StreamingMultipartFormDataParser(stream);
8383
parser.ParameterHandler += parameter => DoSomethingWithParameter(parameter);
84-
parser.FileHandler += (name, fileName, type, disposition, buffer, bytes, partNumber) =>
84+
parser.FileHandler += (name, fileName, type, disposition, buffer, bytes, partNumber, additionalProperties) =>
8585
{
8686
// Write the part of the file we've received to a file stream. (Or do something else)
8787
filestream.Write(buffer, 0, bytes);
@@ -154,7 +154,7 @@ The parser was built for and tested on NET 4.6.1, NET 4,7,2 and NETSTANDARD 2.0.
154154
// ==== Advanced Parsing ====
155155
var parser = new StreamingMultipartFormDataParser(stream);
156156
parser.ParameterHandler += parameter => DoSomethingWithParameter(parameter);
157-
parser.FileHandler += (name, fileName, type, disposition, buffer, bytes, partNumber) =>
157+
parser.FileHandler += (name, fileName, type, disposition, buffer, bytes, partNumber, additionalProperties) =>
158158
{
159159
// Write the part of the file we've received to a file stream. (Or do something else)
160160
// Assume that filesreamsByName is a Dictionary<string, FileStream> of all the files
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace HttpMultipartParser.UnitTests.ParserScenarios
9+
{
10+
public class FileWithAdditionalParameter
11+
{
12+
private static readonly string _testData = TestUtil.TrimAllLines(
13+
@"-----------------------------41952539122868
14+
Content-ID: <some string>
15+
Content-Type: application/octet-stream
16+
17+
<... binary data ...>
18+
-----------------------------41952539122868--"
19+
);
20+
21+
/// <summary>
22+
/// Test case for files with additional parameter.
23+
/// </summary>
24+
private static readonly TestData _testCase = new TestData(
25+
_testData,
26+
new List<ParameterPart> { },
27+
new List<FilePart> {
28+
new FilePart(null, null, TestUtil.StringToStreamNoBom("<... binary data ...>"), (new[] { new KeyValuePair<string, string>("content-id", "<some string>") }).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), "application/octet-stream", "form-data")
29+
}
30+
);
31+
32+
/// <summary>
33+
/// Initializes the test data before each run, this primarily
34+
/// consists of resetting data stream positions.
35+
/// </summary>
36+
public FileWithAdditionalParameter()
37+
{
38+
foreach (var filePart in _testCase.ExpectedFileData)
39+
{
40+
filePart.Data.Position = 0;
41+
}
42+
}
43+
44+
[Fact]
45+
public void FileWithAdditionalParameterTest()
46+
{
47+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
48+
{
49+
var parser = MultipartFormDataParser.Parse(stream, Encoding.UTF8);
50+
Assert.True(_testCase.Validate(parser));
51+
}
52+
}
53+
54+
[Fact]
55+
public async Task FileWithAdditionalParameterTest_Async()
56+
{
57+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
58+
{
59+
var parser = await MultipartFormDataParser.ParseAsync(stream, Encoding.UTF8).ConfigureAwait(false);
60+
Assert.True(_testCase.Validate(parser));
61+
}
62+
}
63+
}
64+
}

Source/HttpMultipartParser.UnitTests/ParserScenarios/TestData.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ private bool ValidateFiles(MultipartFormDataParser parser)
112112
if (expectedFile.ContentType != actualFile.ContentType) return false;
113113
if (expectedFile.ContentDisposition != actualFile.ContentDisposition) return false;
114114

115+
if (expectedFile.AdditionalProperties.Count != actualFile.AdditionalProperties.Count) return false;
116+
if (expectedFile.AdditionalProperties.Except(actualFile.AdditionalProperties).Any()) return false;
117+
115118
// Read the data from the files and see if it's the same
116119
if (expectedFile.Data.CanSeek && expectedFile.Data.Position != 0) expectedFile.Data.Position = 0;
117120
if (actualFile.Data.CanSeek && actualFile.Data.Position != 0) actualFile.Data.Position = 0;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace HttpMultipartParser
5+
{
6+
/// <summary>
7+
/// Class containing various extension methods.
8+
/// </summary>
9+
public static class Extensions
10+
{
11+
/// <summary>
12+
/// Returns true if the parameter has any values. False otherwise.
13+
/// </summary>
14+
/// <param name="parser">The multipart form parser.</param>
15+
/// <param name="name">The name of the parameter.</param>
16+
/// <returns>True if the parameter exists. False otherwise.</returns>
17+
public static bool HasParameter(this IMultipartFormDataParser parser, string name)
18+
{
19+
return parser.Parameters.Any(p => p.Name == name);
20+
}
21+
22+
/// <summary>
23+
/// Returns the value of a parameter or null if it doesn't exist.
24+
///
25+
/// You should only use this method if you're sure the parameter has only one value.
26+
///
27+
/// If you need to support multiple values use GetParameterValues.
28+
/// </summary>
29+
/// <param name="parser">The multipart form parser.</param>
30+
/// <param name="name">The name of the parameter.</param>
31+
/// <returns>The value of the parameter.</returns>
32+
public static string GetParameterValue(this IMultipartFormDataParser parser, string name)
33+
{
34+
return parser.GetParameterValues(name).FirstOrDefault();
35+
}
36+
37+
/// <summary>
38+
/// Returns the values of a parameter or an empty enumerable if the parameter doesn't exist.
39+
/// </summary>
40+
/// <param name="parser">The multipart form parser.</param>
41+
/// <param name="name">The name of the parameter.</param>
42+
/// <returns>The values of the parameter.</returns>
43+
public static IEnumerable<string> GetParameterValues(this IMultipartFormDataParser parser, string name)
44+
{
45+
return parser.Parameters
46+
.Where(p => p.Name == name)
47+
.Select(p => p.Data);
48+
}
49+
}
50+
}

Source/HttpMultipartParser/FilePart.cs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
// </summary>
2525
// --------------------------------------------------------------------------------------------------------------------
2626

27+
using System.Collections.Generic;
28+
using System.Collections.ObjectModel;
2729
using System.IO;
2830
using System.Linq;
2931

@@ -35,6 +37,13 @@ namespace HttpMultipartParser
3537
/// </summary>
3638
public class FilePart
3739
{
40+
#region Fields
41+
42+
private const string DefaultContentType = "text/plain";
43+
private const string DontentDisposition = "form-data";
44+
45+
#endregion
46+
3847
#region Constructors and Destructors
3948

4049
/// <summary>
@@ -49,8 +58,14 @@ public class FilePart
4958
/// <param name="data">
5059
/// The file data.
5160
/// </param>
52-
public FilePart(string name, string fileName, Stream data)
53-
: this(name, fileName, data, "text/plain", "form-data")
61+
/// <param name="contentType">
62+
/// The content type.
63+
/// </param>
64+
/// <param name="contentDisposition">
65+
/// The content disposition.
66+
/// </param>
67+
public FilePart(string name, string fileName, Stream data, string contentType = DefaultContentType, string contentDisposition = DontentDisposition)
68+
: this(name, fileName, data, null, contentType, contentDisposition)
5469
{
5570
}
5671

@@ -66,19 +81,23 @@ public FilePart(string name, string fileName, Stream data)
6681
/// <param name="data">
6782
/// The file data.
6883
/// </param>
84+
/// <param name="additionalProperties">
85+
/// Additional properties associated with this file.
86+
/// </param>
6987
/// <param name="contentType">
7088
/// The content type.
7189
/// </param>
7290
/// <param name="contentDisposition">
7391
/// The content disposition.
7492
/// </param>
75-
public FilePart(string name, string fileName, Stream data, string contentType, string contentDisposition)
93+
public FilePart(string name, string fileName, Stream data, IDictionary<string, string> additionalProperties, string contentType = DefaultContentType, string contentDisposition = DontentDisposition)
7694
{
7795
Name = name;
7896
FileName = fileName?.Split(Path.GetInvalidFileNameChars()).Last();
7997
Data = data;
8098
ContentType = contentType;
8199
ContentDisposition = contentDisposition;
100+
AdditionalProperties = new ReadOnlyDictionary<string, string>(additionalProperties ?? new Dictionary<string, string>());
82101
}
83102

84103
#endregion
@@ -88,27 +107,33 @@ public FilePart(string name, string fileName, Stream data, string contentType, s
88107
/// <summary>
89108
/// Gets the data.
90109
/// </summary>
91-
public Stream Data { get; private set; }
110+
public Stream Data { get; }
111+
112+
/// <summary>
113+
/// Gets the file name.
114+
/// </summary>
115+
public string FileName { get; }
92116

93117
/// <summary>
94-
/// Gets or sets the file name.
118+
/// Gets the name.
95119
/// </summary>
96-
public string FileName { get; set; }
120+
public string Name { get; }
97121

98122
/// <summary>
99-
/// Gets or sets the name.
123+
/// Gets the content-type. Defaults to text/plain if unspecified.
100124
/// </summary>
101-
public string Name { get; set; }
125+
public string ContentType { get; }
102126

103127
/// <summary>
104-
/// Gets or sets the content-type. Defaults to text/plain if unspecified.
128+
/// Gets the content-disposition. Defaults to form-data if unspecified.
105129
/// </summary>
106-
public string ContentType { get; set; }
130+
public string ContentDisposition { get; }
107131

108132
/// <summary>
109-
/// Gets or sets the content-disposition. Defaults to form-data if unspecified.
133+
/// Gets the additional properties associated with this file.
134+
/// An additional property is any property other than the "well known" ones such as name, filename, content-type, etc.
110135
/// </summary>
111-
public string ContentDisposition { get; set; }
136+
public IReadOnlyDictionary<string, string> AdditionalProperties { get; private set; }
112137

113138
#endregion
114139
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Collections.Generic;
2+
3+
namespace HttpMultipartParser
4+
{
5+
/// <summary>
6+
/// Provides methods to parse a
7+
/// <see href="http://www.ietf.org/rfc/rfc2388.txt">
8+
/// <c>multipart/form-data</c>
9+
/// </see>
10+
/// stream into it's parameters and file data.
11+
/// </summary>
12+
public interface IMultipartFormDataParser
13+
{
14+
/// <summary>
15+
/// Gets the mapping of parameters parsed files. The name of a given field
16+
/// maps to the parsed file data.
17+
/// </summary>
18+
IReadOnlyList<FilePart> Files { get; }
19+
20+
/// <summary>
21+
/// Gets the parameters. Several ParameterParts may share the same name.
22+
/// </summary>
23+
IReadOnlyList<ParameterPart> Parameters { get; }
24+
}
25+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace HttpMultipartParser
6+
{
7+
/// <summary>
8+
/// The FileStreamDelegate defining functions that can handle file stream data from this parser.
9+
///
10+
/// Delegates can assume that the data is sequential i.e. the data received by any delegates will be
11+
/// the data immediately following any previously received data.
12+
/// </summary>
13+
/// <param name="name">The name of the multipart data.</param>
14+
/// <param name="fileName">The name of the file.</param>
15+
/// <param name="contentType">The content type of the multipart data.</param>
16+
/// <param name="contentDisposition">The content disposition of the multipart data.</param>
17+
/// <param name="buffer">Some of the data from the file (not necessarily all of the data).</param>
18+
/// <param name="bytes">The length of data in buffer.</param>
19+
/// <param name="partNumber">Each chunk (or "part") in a given file is sequentially numbered, starting at zero.</param>
20+
/// <param name="additionalProperties">Properties other than the "well known" ones (such as name, filename, content-type, etc.) associated with a file stream.</param>
21+
public delegate void FileStreamDelegate(string name, string fileName, string contentType, string contentDisposition, byte[] buffer, int bytes, int partNumber, IDictionary<string, string> additionalProperties);
22+
23+
/// <summary>
24+
/// The StreamClosedDelegate defining functions that can handle stream being closed.
25+
/// </summary>
26+
public delegate void StreamClosedDelegate();
27+
28+
/// <summary>
29+
/// The ParameterDelegate defining functions that can handle multipart parameter data.
30+
/// </summary>
31+
/// <param name="part">The parsed parameter part.</param>
32+
public delegate void ParameterDelegate(ParameterPart part);
33+
34+
/// <summary>
35+
/// Provides methods to parse a
36+
/// <see href="http://www.ietf.org/rfc/rfc2388.txt">
37+
/// <c>multipart/form-data</c>
38+
/// </see>
39+
/// stream into it's parameters and file data.
40+
/// </summary>
41+
public interface IStreamingMultipartFormDataParser
42+
{
43+
/// <summary>
44+
/// Gets or sets the FileHandler. Delegates attached to this property will receive sequential file stream data from this parser.
45+
/// </summary>
46+
FileStreamDelegate FileHandler { get; set; }
47+
48+
/// <summary>
49+
/// Gets or sets the ParameterHandler. Delegates attached to this property will receive parameter data.
50+
/// </summary>
51+
ParameterDelegate ParameterHandler { get; set; }
52+
53+
/// <summary>
54+
/// Gets or sets the StreamClosedHandler. Delegates attached to this property will be notified when the source stream is exhausted.
55+
/// </summary>
56+
StreamClosedDelegate StreamClosedHandler { get; set; }
57+
58+
/// <summary>
59+
/// Execute the parser. This should be called after all handlers have been set.
60+
/// </summary>
61+
void Run();
62+
63+
/// <summary>
64+
/// Execute the parser asynchronously. This should be called after all handlers have been set.
65+
/// </summary>
66+
/// <param name="cancellationToken">The cancellation token.</param>
67+
/// <returns>The asynchronous task.</returns>
68+
Task RunAsync(CancellationToken cancellationToken = default);
69+
}
70+
}

0 commit comments

Comments
 (0)