• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Http-Multipart-Data-Parser / Http-Multipart-Data-Parser / 209

29 Jan 2025 01:16AM UTC coverage: 84.472% (+0.2%) from 84.31%
209

push

appveyor

Jericho
Merge branch 'main' into develop

408 of 483 relevant lines covered (84.47%)

6003.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.26
/Source/HttpMultipartParser/StreamingBinaryMultipartFormDataParser.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Diagnostics;
4
using System.IO;
5
using System.Linq;
6
using System.Text;
7
using System.Threading;
8
using System.Threading.Tasks;
9

10
namespace HttpMultipartParser
11
{
12
        /// <summary>
13
        ///     Provides methods to parse a
14
        ///     <see href="http://www.ietf.org/rfc/rfc2388.txt">
15
        ///         <c>multipart/form-data</c>
16
        ///     </see>
17
        ///     stream into it's parameters and file data.
18
        /// </summary>
19
        /// <remarks>
20
        ///     <para>
21
        ///         A parameter is defined as any non-file data passed in the multipart stream. For example
22
        ///         any form fields would be considered a parameter.
23
        ///     </para>
24
        ///     <para>
25
        ///         The parser determines if a section is a file or not based on the presence or absence
26
        ///         of the filename argument for the Content-Type header. If filename is set then the section
27
        ///         is assumed to be a file, otherwise it is assumed to be parameter data.
28
        ///     </para>
29
        ///     <para>
30
        ///          Please note that this parser is very similar to <seealso cref="StreamingMultipartFormDataParser"/>.
31
        ///          The main difference being that this parser will read the content of parameters as binary rather than text.
32
        ///     </para>
33
        /// </remarks>
34
        /// <example>
35
        ///     <code lang="C#">
36
        ///       Stream multipartStream = GetTheMultipartStream();
37
        ///       string boundary = GetTheBoundary();
38
        ///       var parser = new StreamingMultipartFormDataParser(multipartStream, boundary, Encoding.UTF8);
39
        ///
40
        ///       // Set up our delegates for how we want to handle recieved data.
41
        ///       // In our case parameters will be written to a dictionary and files
42
        ///       // will be written to a filestream
43
        ///       parser.ParameterHandler += parameter => AddToDictionary(parameter);
44
        ///       parser.FileHandler += (name, fileName, type, disposition, buffer, bytes) => WriteDataToFile(fileName, buffer, bytes);
45
        ///       parser.Run();
46
        ///   </code>
47
        /// </example>
48
        public class StreamingBinaryMultipartFormDataParser : IStreamingBinaryMultipartFormDataParser
49
        {
50
                /// <summary>
51
                ///     List of mimetypes that should be detected as file.
52
                /// </summary>
53
                private readonly string[] binaryMimeTypes;
54

55
                /// <summary>
56
                ///     The stream we are parsing.
57
                /// </summary>
58
                private readonly Stream stream;
59

60
                /// <summary>
61
                ///     Determines if we should throw an exception when we enconter an invalid part or ignore it.
62
                /// </summary>
63
                private readonly bool ignoreInvalidParts;
64

65
                /// <summary>
66
                ///     The boundary of the multipart message  as a string.
67
                /// </summary>
68
                private string boundary;
69

70
                /// <summary>
71
                ///     The boundary of the multipart message as a byte string
72
                ///     encoded with CurrentEncoding.
73
                /// </summary>
74
                private byte[] boundaryBinary;
75

76
                /// <summary>
77
                ///     The end boundary of the multipart message as a string.
78
                /// </summary>
79
                private string endBoundary;
80

81
                /// <summary>
82
                ///     The end boundary of the multipart message as a byte string
83
                ///     encoded with CurrentEncoding.
84
                /// </summary>
85
                private byte[] endBoundaryBinary;
86

87
                /// <summary>
88
                ///     Determines if we have consumed the end boundary binary and determines
89
                ///     if we are done parsing.
90
                /// </summary>
91
                private bool readEndBoundary;
92

93
                /// <summary>
94
                ///     Initializes a new instance of the <see cref="StreamingBinaryMultipartFormDataParser" /> class
95
                ///     with the boundary, stream, input encoding and buffer size.
96
                /// </summary>
97
                /// <param name="stream">
98
                ///     The stream containing the multipart data.
99
                /// </param>
100
                /// <param name="encoding">
101
                ///     The encoding of the multipart data.
102
                /// </param>
103
                /// <param name="binaryBufferSize">
104
                ///     The size of the buffer to use for parsing the multipart form data. This must be larger
105
                ///     then (size of boundary + 4 + # bytes in newline).
106
                /// </param>
107
                /// <param name="binaryMimeTypes">
108
                ///     List of mimetypes that should be detected as file.
109
                /// </param>
110
                /// <param name="ignoreInvalidParts">
111
                ///     By default the parser will throw an exception if it encounters an invalid part. set this to true to ignore invalid parts.
112
                /// </param>
113
                public StreamingBinaryMultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize = Constants.DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
114
                        : this(stream, null, encoding, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts)
×
115
                {
116
                }
×
117

118
                /// <summary>
119
                ///     Initializes a new instance of the <see cref="StreamingBinaryMultipartFormDataParser" /> class
120
                ///     with the boundary, stream, input encoding and buffer size.
121
                /// </summary>
122
                /// <param name="stream">
123
                ///     The stream containing the multipart data.
124
                /// </param>
125
                /// <param name="boundary">
126
                ///     The multipart/form-data boundary. This should be the value
127
                ///     returned by the request header.
128
                /// </param>
129
                /// <param name="encoding">
130
                ///     The encoding of the multipart data.
131
                /// </param>
132
                /// <param name="binaryBufferSize">
133
                ///     The size of the buffer to use for parsing the multipart form data. This must be larger
134
                ///     then (size of boundary + 4 + # bytes in newline).
135
                /// </param>
136
                /// <param name="binaryMimeTypes">
137
                ///     List of mimetypes that should be detected as file.
138
                /// </param>
139
                /// <param name="ignoreInvalidParts">
140
                ///     By default the parser will throw an exception if it encounters an invalid part. set this to true to ignore invalid parts.
141
                /// </param>
142
                public StreamingBinaryMultipartFormDataParser(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = Constants.DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
143
                {
144
                        if (stream == null || stream == Stream.Null) { throw new ArgumentNullException(nameof(stream)); }
553✔
145

146
                        this.stream = stream;
549✔
147
                        this.boundary = boundary;
549✔
148
                        Encoding = encoding ?? Constants.DefaultEncoding;
549✔
149
                        BinaryBufferSize = binaryBufferSize;
549✔
150
                        readEndBoundary = false;
549✔
151
                        this.binaryMimeTypes = binaryMimeTypes ?? Constants.DefaultBinaryMimeTypes;
549✔
152
                        this.ignoreInvalidParts = ignoreInvalidParts;
549✔
153
                }
549✔
154

155
                /// <summary>
156
                ///     Begins executing the parser. This should be called after all handlers have been set.
157
                /// </summary>
158
                public void Run()
159
                {
160
                        var reader = new RebufferableBinaryReader(stream, Encoding, BinaryBufferSize);
274✔
161

162
                        // If we don't know the boundary now is the time to calculate it.
163
                        boundary ??= DetectBoundary(reader);
274✔
164

165
                        // It's important to remember that the boundary given in the header has a -- appended to the start
166
                        // and the last one has a -- appended to the end
167
                        boundary = "--" + boundary;
272✔
168
                        endBoundary = boundary + "--";
272✔
169

170
                        // We add newline here because unlike reader.ReadLine() binary reading
171
                        // does not automatically consume the newline, we want to add it to our signature
172
                        // so we can automatically detect and consume newlines after the boundary
173
                        boundaryBinary = Encoding.GetBytes(boundary);
272✔
174
                        endBoundaryBinary = Encoding.GetBytes(endBoundary);
272✔
175

176
                        Debug.Assert(
177
                                BinaryBufferSize >= endBoundaryBinary.Length,
178
                                "binaryBufferSize must be bigger then the boundary");
179

180
                        Parse(reader);
272✔
181
                }
268✔
182

183
                /// <summary>
184
                ///     Begins executing the parser asynchronously. This should be called after all handlers have been set.
185
                /// </summary>
186
                /// <param name="cancellationToken">
187
                ///     The cancellation token.
188
                /// </param>
189
                /// <returns>
190
                ///     The asynchronous task.
191
                /// </returns>
192
                public async Task RunAsync(CancellationToken cancellationToken = default)
193
                {
194
                        var reader = new RebufferableBinaryReader(stream, Encoding, BinaryBufferSize);
195

196
                        // If we don't know the boundary now is the time to calculate it.
197
                        boundary ??= await DetectBoundaryAsync(reader, cancellationToken).ConfigureAwait(false);
198

199
                        // It's important to remember that the boundary given in the header has a -- appended to the start
200
                        // and the last one has a -- appended to the end
201
                        boundary = "--" + boundary;
202
                        endBoundary = boundary + "--";
203

204
                        // We add newline here because unlike reader.ReadLine() binary reading
205
                        // does not automatically consume the newline, we want to add it to our signature
206
                        // so we can automatically detect and consume newlines after the boundary
207
                        boundaryBinary = Encoding.GetBytes(boundary);
208
                        endBoundaryBinary = Encoding.GetBytes(endBoundary);
209

210
                        Debug.Assert(
211
                                BinaryBufferSize >= endBoundaryBinary.Length,
212
                                "binaryBufferSize must be bigger then the boundary");
213

214
                        await ParseAsync(reader, cancellationToken).ConfigureAwait(false);
215
                }
216

217
                /// <summary>
218
                /// Gets the binary buffer size.
219
                /// </summary>
220
                public int BinaryBufferSize { get; private set; }
221

222
                /// <summary>
223
                /// Gets the encoding.
224
                /// </summary>
225
                public Encoding Encoding { get; private set; }
226

227
                /// <summary>
228
                /// Gets or sets the FileHandler. Delegates attached to this property will receive sequential file stream data from this parser.
229
                /// </summary>
230
                public FileStreamDelegate FileHandler { get; set; }
231

232
                /// <summary>
233
                /// Gets or sets the ParameterHandler. Delegates attached to this property will receive parameter data.
234
                /// </summary>
235
                public BinaryParameterDelegate ParameterHandler { get; set; }
236

237
                /// <summary>
238
                /// Gets or sets the StreamClosedHandler. Delegates attached to this property will be notified when the source stream is exhausted.
239
                /// </summary>
240
                public StreamClosedDelegate StreamClosedHandler { get; set; }
241

242
                /// <summary>
243
                ///     Detects the boundary from the input stream. Assumes that the
244
                ///     current position of the reader is the start of the file and therefore
245
                ///     the beginning of the boundary.
246
                /// </summary>
247
                /// <remarks>
248
                /// As of version 8.2.0 (released in June 2023), we ignore blank lines at the
249
                /// start of the stream. In previous version, an exception was thrown if any
250
                /// blank line was present before the boundary marker.
251
                /// </remarks>
252
                /// <param name="reader">
253
                ///     The binary reader to parse.
254
                /// </param>
255
                /// <returns>
256
                ///     The boundary string.
257
                /// </returns>
258
                private static string DetectBoundary(RebufferableBinaryReader reader)
259
                {
260
                        // Presumably the boundary is --|||||||||||||| where -- is the stuff added on to
261
                        // the front as per the protocol and ||||||||||||| is the part we care about.
262

263
                        // The following loop ignores blank lines that may be present before the first line of the form.
264
                        // It's highly unusual to find blank lines at the start of the data but it's a possible scenario described in GH-116.
265
                        // Please note that we intentionally do NOT check for "string.IsNullOrEmpty(line)" because NULL does
266
                        // not indicate a blank line. It indicates that we have reached the end of the stream.
267
                        var line = string.Empty;
20✔
268
                        while (line == string.Empty)
43✔
269
                        {
270
                                line = reader.ReadLine();
23✔
271
                        }
272

273
                        // The line must not be empty and must starts with "--".
274
                        if (string.IsNullOrEmpty(line)) throw new MultipartParseException("Unable to determine boundary: either the stream is empty or we reached the end of the stream");
21✔
275
                        else if (!line.StartsWith("--")) throw new MultipartParseException("Unable to determine boundary: content does not start with a valid multipart boundary");
20✔
276

277
                        // Remove the two dashes
278
                        string boundary = line.Substring(2);
18✔
279

280
                        // If the string ends with '--' and it's not followed by content, we can safely assume that we
281
                        // found the "end" boundary. In this scenario, we must trim the two dashes to get the actual boundary.
282
                        // Otherwise, the boundary must be accepted as-is. The reason we check for "additional content" is to
283
                        // resolve the problem explained in GH-123.
284
                        var moreContentAvailable = MoreContentAvailable(reader);
18✔
285
                        if (boundary.EndsWith("--") && !moreContentAvailable)
18✔
286
                        {
287
                                boundary = boundary.Substring(0, boundary.Length - 2);
1✔
288
                                reader.Buffer($"--{boundary}--\n");
1✔
289
                        }
290
                        else
291
                        {
292
                                reader.Buffer($"--{boundary}\n");
17✔
293
                        }
294

295
                        return boundary;
18✔
296
                }
297

298
                /// <summary>
299
                ///     Detects the boundary from the input stream. Assumes that the
300
                ///     current position of the reader is the start of the file and therefore
301
                ///     the beginning of the boundary.
302
                /// </summary>
303
                /// <remarks>
304
                /// As of version 8.2.0 (released in June 2023), we ignore blank lines at the
305
                /// start of the stream. In previous version, an exception was thrown if any
306
                /// blank line was present before the boundary marker.
307
                /// </remarks>
308
                /// <param name="reader">
309
                ///     The binary reader to parse.
310
                /// </param>
311
                /// <param name="cancellationToken">
312
                ///     The cancellation token.
313
                /// </param>
314
                /// <returns>
315
                ///     The boundary string.
316
                /// </returns>
317
                private static async Task<string> DetectBoundaryAsync(RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
318
                {
319
                        // Presumably the boundary is --|||||||||||||| where -- is the stuff added on to
320
                        // the front as per the protocol and ||||||||||||| is the part we care about.
321

322
                        // The following loop ignores blank lines that may be present before the first line of the form.
323
                        // It's highly unusual to find blank lines at the start of the data but it's a possible scenario described in GH-116.
324
                        // Please note that we intentionally do NOT check for "string.IsNullOrEmpty(line)" because NULL does
325
                        // not indicate a blank line. It indicates that we have reached the end of the stream.
326
                        var line = string.Empty;
327
                        while (line == string.Empty)
328
                        {
329
                                line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
330
                        }
331

332
                        // The line must not be empty and must starts with "--".
333
                        if (string.IsNullOrEmpty(line)) throw new MultipartParseException("Unable to determine boundary: either the stream is empty or we reached the end of the stream");
334
                        else if (!line.StartsWith("--")) throw new MultipartParseException("Unable to determine boundary: content does not start with a valid multipart boundary");
335

336
                        // Remove the two dashes
337
                        string boundary = line.Substring(2);
338

339
                        // If the string ends with '--' and it's not followed by content, we can safely assume that we
340
                        // found the "end" boundary. In this scenario, we must trim the two dashes to get the actual boundary.
341
                        // Otherwise, the boundary must be accepted as-is. The reason we check for "additional content" is to
342
                        // resolve the problem explained in GH-123.
343
                        var moreContentAvailable = await MoreContentAvailableAsync(reader).ConfigureAwait(false);
344
                        if (boundary.EndsWith("--") && !moreContentAvailable)
345
                        {
346
                                boundary = boundary.Substring(0, boundary.Length - 2);
347
                                reader.Buffer($"--{boundary}--\n");
348
                        }
349
                        else
350
                        {
351
                                reader.Buffer($"--{boundary}\n");
352
                        }
353

354
                        return boundary;
355
                }
356

357
                /// <summary>
358
                ///     Determine if there is more content.
359
                /// </summary>
360
                /// <param name="reader">
361
                ///     The binary reader to parse.
362
                /// </param>
363
                /// <returns>
364
                ///     A boolean indicating whether more content is available in the binary reader.
365
                /// </returns>
366
                private static bool MoreContentAvailable(RebufferableBinaryReader reader)
367
                {
368
                        var line = reader.ReadLine();
18✔
369

370
                        if (line == null) return false;
19✔
371
                        else reader.Buffer($"{line}\n");
17✔
372

373
                        return true;
17✔
374
                }
375

376
                /// <summary>
377
                ///     Determine if there is more content.
378
                /// </summary>
379
                /// <param name="reader">
380
                ///     The binary reader to parse.
381
                /// </param>
382
                /// <param name="cancellationToken">
383
                ///     The cancellation token.
384
                /// </param>
385
                /// <returns>
386
                ///     A boolean indicating whether more content is available in the binary reader.
387
                /// </returns>
388
                private static async Task<bool> MoreContentAvailableAsync(RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
389
                {
390
                        var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
391

392
                        if (line == null) return false;
393
                        else reader.Buffer($"{line}\n");
394

395
                        return true;
396
                }
397

398
                /// <summary>
399
                /// Use a few assumptions to determine if a section contains a file.
400
                /// </summary>
401
                /// <param name="parameters">The section parameters.</param>
402
                /// <returns>true if the section contains a file, false otherwise.</returns>
403
                private bool IsFilePart(IDictionary<string, string> parameters)
404
                {
405
                        if (parameters == null) throw new ArgumentNullException(nameof(parameters));
1,566✔
406

407
                        // A section without any parameter is invalid. It is very likely to contain just a bunch of blank lines.
408
                        if (parameters.Count == 0) return false;
1,570✔
409

410
                        // If a section contains filename, then it's a file.
411
                        else if (parameters.ContainsKey("filename") || parameters.ContainsKey("filename*")) return true;
2,100✔
412

413
                        // Check if mimetype is a binary file
414
                        else if (parameters.ContainsKey("content-type") && binaryMimeTypes.Contains(parameters["content-type"])) return true;
1,027✔
415

416
                        // If the section is missing the filename and the name, then it's a file.
417
                        // For example, images in an mjpeg stream have neither a name nor a filename.
418
                        else if (!parameters.ContainsKey("name")) return true;
1,027✔
419

420
                        // Otherwise this section does not contain a file.
421
                        return false;
1,015✔
422
                }
423

424
                /// <summary>
425
                /// Use a few assumptions to determine if a section contains a "data" parameter.
426
                /// </summary>
427
                /// <param name="parameters">The section parameters.</param>
428
                /// <returns>true if the section contains a data parameter, false otherwise.</returns>
429
                private bool IsParameterPart(IDictionary<string, string> parameters)
430
                {
431
                        if (parameters == null) throw new ArgumentNullException(nameof(parameters));
1,019✔
432

433
                        // A section without any parameter is invalid. It is very likely to contain just a bunch of blank lines.
434
                        if (parameters.Count == 0) return false;
1,023✔
435

436
                        // A data parameter MUST have a name.
437
                        else if (parameters.ContainsKey("name")) return true;
2,030✔
438

439
                        // Otherwise this section does not contain a data parameter.
440
                        return false;
×
441
                }
442

443
                /// <summary>
444
                ///     Finds the next sequence of newlines in the input stream.
445
                /// </summary>
446
                /// <param name="data">The data to search.</param>
447
                /// <param name="offset">The offset to start searching at.</param>
448
                /// <param name="maxBytes">The maximum number of bytes (starting from offset) to search.</param>
449
                /// <returns>The offset of the next newline.</returns>
450
                private int FindNextNewline(ref byte[] data, int offset, int maxBytes)
451
                {
452
                        byte[][] newlinePatterns = { Encoding.GetBytes("\r\n"), Encoding.GetBytes("\n") };
547✔
453
                        Array.Sort(newlinePatterns, (first, second) => second.Length.CompareTo(first.Length));
547✔
454

455
                        byte[] dataRef = data;
547✔
456
                        if (offset != 0)
547✔
457
                        {
458
                                dataRef = data.Skip(offset).ToArray();
45✔
459
                        }
460

461
                        foreach (var pattern in newlinePatterns)
2,721✔
462
                        {
463
                                int position = SubsequenceFinder.Search(dataRef, pattern, maxBytes);
1,087✔
464
                                if (position != -1)
1,087✔
465
                                {
466
                                        return position + offset;
547✔
467
                                }
468
                        }
469

470
                        return -1;
×
471
                }
472

473
                /// <summary>
474
                ///     Calculates the length of the next found newline.
475
                ///     data[offset] is the start of the space to search.
476
                /// </summary>
477
                /// <param name="data">
478
                ///     The data containing the newline.
479
                /// </param>
480
                /// <param name="offset">
481
                ///     The offset of the start of the newline.
482
                /// </param>
483
                /// <returns>
484
                ///     The length in bytes of the newline sequence.
485
                /// </returns>
486
                private int CalculateNewlineLength(ref byte[] data, int offset)
487
                {
488
                        byte[][] newlinePatterns = { Encoding.GetBytes("\r\n"), Encoding.GetBytes("\n") };
1,094✔
489

490
                        // Go through each pattern and find which one matches.
491
                        foreach (var pattern in newlinePatterns)
5,477✔
492
                        {
493
                                bool found = false;
2,177✔
494
                                for (int i = 0; i < pattern.Length; ++i)
6,512✔
495
                                {
496
                                        int readPos = offset + i;
2,191✔
497
                                        if (data.Length == readPos || pattern[i] != data[readPos])
2,191✔
498
                                        {
499
                                                found = false;
1,112✔
500
                                                break;
1,112✔
501
                                        }
502

503
                                        found = true;
1,079✔
504
                                }
505

506
                                if (found)
2,177✔
507
                                {
508
                                        return pattern.Length;
1,065✔
509
                                }
510
                        }
511

512
                        return 0;
29✔
513
                }
514

515
                /// <summary>
516
                ///     Begins the parsing of the stream into objects.
517
                /// </summary>
518
                /// <param name="reader">
519
                ///     The multipart/form-data binary reader to parse from.
520
                /// </param>
521
                /// <exception cref="MultipartParseException">
522
                ///     thrown on finding unexpected data such as a boundary before we are ready for one.
523
                /// </exception>
524
                private void Parse(RebufferableBinaryReader reader)
525
                {
526
                        // Parsing references include:
527
                        // RFC1341 section 7: http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
528
                        // RFC2388: http://www.ietf.org/rfc/rfc2388.txt
529

530
                        // First we need to read until we find a boundary
531
                        while (true)
532
                        {
533
                                string line = reader.ReadLine();
273✔
534

535
                                if (line == boundary)
273✔
536
                                {
537
                                        break;
538
                                }
539
                                else if (line == endBoundary)
5✔
540
                                {
541
                                        readEndBoundary = true;
2✔
542
                                        break;
2✔
543
                                }
544
                                else if (line == null)
3✔
545
                                {
546
                                        throw new MultipartParseException("Could not find expected boundary");
2✔
547
                                }
548
                        }
549

550
                        // Now that we've found the initial boundary we know where to start.
551
                        // We need parse each individual section
552
                        while (!readEndBoundary)
1,051✔
553
                        {
554
                                // ParseSection will parse up to and including
555
                                // the next boundary.
556
                                ParseSection(reader);
783✔
557
                        }
558

559
                        StreamClosedHandler?.Invoke();
268✔
560
                }
×
561

562
                /// <summary>
563
                ///     Begins the asynchronous parsing of the stream into objects.
564
                /// </summary>
565
                /// <param name="reader">
566
                ///     The multipart/form-data binary reader to parse from.
567
                /// </param>
568
                /// <param name="cancellationToken">
569
                ///     The cancellation token.
570
                /// </param>
571
                /// <returns>
572
                ///     The asynchronous task.
573
                /// </returns>
574
                /// <exception cref="MultipartParseException">
575
                ///     thrown on finding unexpected data such as a boundary before we are ready for one.
576
                /// </exception>
577
                private async Task ParseAsync(RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
578
                {
579
                        // Parsing references include:
580
                        // RFC1341 section 7: http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
581
                        // RFC2388: http://www.ietf.org/rfc/rfc2388.txt
582

583
                        // First we need to read until we find a boundary
584
                        while (true)
585
                        {
586
                                string line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
587

588
                                if (line == boundary)
589
                                {
590
                                        break;
591
                                }
592
                                else if (line == endBoundary)
593
                                {
594
                                        readEndBoundary = true;
595
                                        break;
596
                                }
597
                                else if (line == null)
598
                                {
599
                                        throw new MultipartParseException("Could not find expected boundary");
600
                                }
601
                        }
602

603
                        // Now that we've found the initial boundary we know where to start.
604
                        // We need parse each individual section
605
                        while (!readEndBoundary)
606
                        {
607
                                // ParseSection will parse up to and including
608
                                // the next boundary.
609
                                await ParseSectionAsync(reader, cancellationToken).ConfigureAwait(false);
610
                        }
611

612
                        StreamClosedHandler?.Invoke();
613
                }
614

615
                /// <summary>
616
                ///     Parses a section of the stream that is known to be file data.
617
                /// </summary>
618
                /// <param name="parameters">
619
                ///     The header parameters of this file.
620
                /// </param>
621
                /// <param name="reader">
622
                ///     The StreamReader to read the data from.
623
                /// </param>
624
                private void ParseFilePart(Dictionary<string, string> parameters, RebufferableBinaryReader reader)
625
                {
626
                        int partNumber = 0; // begins count parts of file from 0
273✔
627

628
                        // Read the parameters
629
                        parameters.TryGetValue("name", out string name);
273✔
630
                        parameters.TryGetValue("filename", out string filename);
273✔
631
                        parameters.TryGetValue("filename*", out string filenameStar);
273✔
632
                        parameters.TryGetValue("content-type", out string contentType);
273✔
633
                        parameters.TryGetValue("content-disposition", out string contentDisposition);
273✔
634

635
                        // Per RFC6266 section 4.3, we should favor "filename*" over "filename"
636
                        if (!string.IsNullOrEmpty(filenameStar)) filename = RFC5987.Decode(filenameStar);
276✔
637

638
                        // Filter out the "well known" parameters.
639
                        var additionalParameters = GetAdditionalParameters(parameters);
273✔
640

641
                        // Default values if expected parameters are missing
642
                        contentType ??= "text/plain";
273✔
643
                        contentDisposition ??= "form-data";
273✔
644

645
                        // We want to create a stream and fill it with the data from the file.
646
                        var curBuffer = Utilities.ArrayPool.Rent(BinaryBufferSize);
273✔
647
                        var prevBuffer = Utilities.ArrayPool.Rent(BinaryBufferSize);
273✔
648
                        var fullBuffer = Utilities.ArrayPool.Rent(BinaryBufferSize * 2);
273✔
649
                        int curLength;
650
                        int prevLength;
651
                        int fullLength;
652

653
                        prevLength = reader.Read(prevBuffer, 0, BinaryBufferSize);
273✔
654
                        do
655
                        {
656
                                curLength = reader.Read(curBuffer, 0, BinaryBufferSize);
283✔
657

658
                                // Combine both buffers into the fullBuffer
659
                                // See: http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp
660
                                Buffer.BlockCopy(prevBuffer, 0, fullBuffer, 0, prevLength);
283✔
661
                                Buffer.BlockCopy(curBuffer, 0, fullBuffer, prevLength, curLength);
283✔
662
                                fullLength = prevLength + curLength;
283✔
663

664
                                // Now we want to check for a substring within the current buffer.
665
                                // We need to find the closest substring greedily. That is find the
666
                                // closest boundary and don't miss the end --'s if it's an end boundary.
667
                                int endBoundaryPos = SubsequenceFinder.Search(fullBuffer, endBoundaryBinary, fullLength);
283✔
668
                                int endBoundaryLength = endBoundaryBinary.Length;
283✔
669

670
                                int boundaryPos = SubsequenceFinder.Search(fullBuffer, boundaryBinary, fullLength);
283✔
671
                                int boundaryLength = boundaryBinary.Length;
283✔
672

673
                                // If the boundaryPos is exactly at the end of our full buffer then ignore it as it could
674
                                // actually be a endBoundary that's had one or both of the '--' chopped off by the buffer.
675
                                if (boundaryPos + boundaryLength == fullLength ||
283✔
676
                                   boundaryPos + boundaryLength + 1 == fullLength)
283✔
677
                                {
678
                                        boundaryPos = -1;
3✔
679
                                }
680

681
                                // We need to select the appropriate position and length
682
                                // based on the smallest non-negative position.
683
                                int endPos = -1;
283✔
684
                                int endPosLength = 0;
283✔
685

686
                                if (endBoundaryPos >= 0 && boundaryPos >= 0)
283✔
687
                                {
688
                                        if (boundaryPos < endBoundaryPos)
236✔
689
                                        {
690
                                                // Select boundary
691
                                                endPos = boundaryPos;
221✔
692
                                                endPosLength = boundaryLength;
221✔
693
                                        }
694
                                        else
695
                                        {
696
                                                // Select end boundary
697
                                                endPos = endBoundaryPos;
15✔
698
                                                endPosLength = endBoundaryLength;
15✔
699
                                                readEndBoundary = true;
15✔
700
                                        }
701
                                }
702
                                else if (boundaryPos >= 0 && endBoundaryPos < 0)
47✔
703
                                {
704
                                        // Select boundary
705
                                        endPos = boundaryPos;
37✔
706
                                        endPosLength = boundaryLength;
37✔
707
                                }
708
                                else if (boundaryPos < 0 && endBoundaryPos >= 0)
10✔
709
                                {
710
                                        // Select end boundary
711
                                        endPos = endBoundaryPos;
×
712
                                        endPosLength = endBoundaryLength;
×
713
                                        readEndBoundary = true;
×
714
                                }
715

716
                                if (endPos != -1)
283✔
717
                                {
718
                                        // Now we need to check if the endPos is followed by \r\n or just \n. HTTP
719
                                        // specifies \r\n but some clients might encode with \n. Or we might get 0 if
720
                                        // we are at the end of the file.
721
                                        int boundaryNewlineOffset = CalculateNewlineLength(ref fullBuffer, Math.Min(fullLength - 1, endPos + endPosLength));
273✔
722

723
                                        // We also need to check if the last n characters of the buffer to write
724
                                        // are a newline and if they are ignore them.
725
                                        int maxNewlineBytes = Encoding.GetMaxByteCount(2);
273✔
726
                                        int bufferNewlineOffset = FindNextNewline(ref fullBuffer, Math.Max(0, endPos - maxNewlineBytes), maxNewlineBytes);
273✔
727
                                        int bufferNewlineLength = CalculateNewlineLength(ref fullBuffer, bufferNewlineOffset);
273✔
728

729
                                        // We've found an end. We need to consume all the binary up to it
730
                                        // and then write the remainder back to the original stream. Then we
731
                                        // need to modify the original streams position to take into account
732
                                        // the new data.
733
                                        // We also want to chop off the newline that is inserted by the protocol.
734
                                        // We can do this by reducing endPos by the length of newline in this environment
735
                                        // and encoding
736
                                        FileHandler?.Invoke(name, filename, contentType, contentDisposition, fullBuffer, endPos - bufferNewlineLength, partNumber++, additionalParameters);
273✔
737

738
                                        int writeBackOffset = endPos + endPosLength + boundaryNewlineOffset;
273✔
739
                                        int writeBackAmount = (prevLength + curLength) - writeBackOffset;
273✔
740
                                        reader.Buffer(fullBuffer, writeBackOffset, writeBackAmount);
273✔
741

742
                                        break;
273✔
743
                                }
744

745
                                // No end, consume the entire previous buffer
746
                                FileHandler?.Invoke(name, filename, contentType, contentDisposition, prevBuffer, prevLength, partNumber++, additionalParameters);
10✔
747

748
                                // Now we want to swap the two buffers, we don't care
749
                                // what happens to the data from prevBuffer so we set
750
                                // curBuffer to it so it gets overwritten.
751
                                (prevBuffer, curBuffer) = (curBuffer, prevBuffer);
10✔
752

753
                                // We don't need to swap the lengths because
754
                                // curLength will be overwritten in the next
755
                                // iteration of the loop.
756
                                prevLength = curLength;
10✔
757
                        }
758
                        while (prevLength != 0);
10✔
759

760
                        Utilities.ArrayPool.Return(curBuffer);
273✔
761
                        Utilities.ArrayPool.Return(prevBuffer);
273✔
762
                        Utilities.ArrayPool.Return(fullBuffer);
273✔
763
                }
273✔
764

765
                /// <summary>
766
                ///     Asynchronously parses a section of the stream that is known to be file data.
767
                /// </summary>
768
                /// <param name="parameters">
769
                ///     The header parameters of this file, expects "name" and "filename" to be valid keys.
770
                /// </param>
771
                /// <param name="reader">
772
                ///     The StreamReader to read the data from.
773
                /// </param>
774
                /// <param name="cancellationToken">
775
                ///     The cancellation token.
776
                /// </param>
777
                /// <returns>
778
                ///     The asynchronous task.
779
                /// </returns>
780
                private async Task ParseFilePartAsync(Dictionary<string, string> parameters, RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
781
                {
782
                        int partNumber = 0; // begins count parts of file from 0
783

784
                        // Read the parameters
785
                        parameters.TryGetValue("name", out string name);
786
                        parameters.TryGetValue("filename", out string filename);
787
                        parameters.TryGetValue("filename*", out string filenameStar);
788
                        parameters.TryGetValue("content-type", out string contentType);
789
                        parameters.TryGetValue("content-disposition", out string contentDisposition);
790

791
                        // Per RFC6266 section 4.3, we should favor "filename*" over "filename"
792
                        if (!string.IsNullOrEmpty(filenameStar)) filename = RFC5987.Decode(filenameStar);
793

794
                        // Filter out the "well known" parameters.
795
                        var additionalParameters = GetAdditionalParameters(parameters);
796

797
                        // Default values if expected parameters are missing
798
                        contentType ??= "text/plain";
799
                        contentDisposition ??= "form-data";
800

801
                        // We want to create a stream and fill it with the data from the
802
                        // file.
803
                        var curBuffer = Utilities.ArrayPool.Rent(BinaryBufferSize);
804
                        var prevBuffer = Utilities.ArrayPool.Rent(BinaryBufferSize);
805
                        var fullBuffer = Utilities.ArrayPool.Rent(BinaryBufferSize * 2);
806
                        int curLength;
807
                        int prevLength;
808
                        int fullLength;
809

810
                        prevLength = await reader.ReadAsync(prevBuffer, 0, BinaryBufferSize, cancellationToken).ConfigureAwait(false);
811
                        do
812
                        {
813
                                curLength = await reader.ReadAsync(curBuffer, 0, BinaryBufferSize, cancellationToken).ConfigureAwait(false);
814

815
                                // Combine both buffers into the fullBuffer
816
                                // See: http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp
817
                                Buffer.BlockCopy(prevBuffer, 0, fullBuffer, 0, prevLength);
818
                                Buffer.BlockCopy(curBuffer, 0, fullBuffer, prevLength, curLength);
819
                                fullLength = prevLength + curLength;
820

821
                                // Now we want to check for a substring within the current buffer.
822
                                // We need to find the closest substring greedily. That is find the
823
                                // closest boundary and don't miss the end --'s if it's an end boundary.
824
                                int endBoundaryPos = SubsequenceFinder.Search(fullBuffer, endBoundaryBinary, fullLength);
825
                                int endBoundaryLength = endBoundaryBinary.Length;
826

827
                                int boundaryPos = SubsequenceFinder.Search(fullBuffer, boundaryBinary, fullLength);
828
                                int boundaryLength = boundaryBinary.Length;
829

830
                                // If the boundaryPos is exactly at the end of our full buffer then ignore it as it could
831
                                // actually be a endBoundary that's had one or both of the '--' chopped off by the buffer.
832
                                if (boundaryPos + boundaryLength == fullLength ||
833
                                   boundaryPos + boundaryLength + 1 == fullLength)
834
                                {
835
                                        boundaryPos = -1;
836
                                }
837

838
                                // We need to select the appropriate position and length
839
                                // based on the smallest non-negative position.
840
                                int endPos = -1;
841
                                int endPosLength = 0;
842

843
                                if (endBoundaryPos >= 0 && boundaryPos >= 0)
844
                                {
845
                                        if (boundaryPos < endBoundaryPos)
846
                                        {
847
                                                // Select boundary
848
                                                endPos = boundaryPos;
849
                                                endPosLength = boundaryLength;
850
                                        }
851
                                        else
852
                                        {
853
                                                // Select end boundary
854
                                                endPos = endBoundaryPos;
855
                                                endPosLength = endBoundaryLength;
856
                                                readEndBoundary = true;
857
                                        }
858
                                }
859
                                else if (boundaryPos >= 0 && endBoundaryPos < 0)
860
                                {
861
                                        // Select boundary
862
                                        endPos = boundaryPos;
863
                                        endPosLength = boundaryLength;
864
                                }
865
                                else if (boundaryPos < 0 && endBoundaryPos >= 0)
866
                                {
867
                                        // Select end boundary
868
                                        endPos = endBoundaryPos;
869
                                        endPosLength = endBoundaryLength;
870
                                        readEndBoundary = true;
871
                                }
872

873
                                if (endPos != -1)
874
                                {
875
                                        // Now we need to check if the endPos is followed by \r\n or just \n. HTTP
876
                                        // specifies \r\n but some clients might encode with \n. Or we might get 0 if
877
                                        // we are at the end of the file.
878
                                        int boundaryNewlineOffset = CalculateNewlineLength(ref fullBuffer, Math.Min(fullLength - 1, endPos + endPosLength));
879

880
                                        // We also need to check if the last n characters of the buffer to write
881
                                        // are a newline and if they are ignore them.
882
                                        int maxNewlineBytes = Encoding.GetMaxByteCount(2);
883
                                        int bufferNewlineOffset = FindNextNewline(ref fullBuffer, Math.Max(0, endPos - maxNewlineBytes), maxNewlineBytes);
884
                                        int bufferNewlineLength = CalculateNewlineLength(ref fullBuffer, bufferNewlineOffset);
885

886
                                        // We've found an end. We need to consume all the binary up to it
887
                                        // and then write the remainder back to the original stream. Then we
888
                                        // need to modify the original streams position to take into account
889
                                        // the new data.
890
                                        // We also want to chop off the newline that is inserted by the protocl.
891
                                        // We can do this by reducing endPos by the length of newline in this environment
892
                                        // and encoding
893
                                        FileHandler?.Invoke(name, filename, contentType, contentDisposition, fullBuffer, endPos - bufferNewlineLength, partNumber++, additionalParameters);
894

895
                                        int writeBackOffset = endPos + endPosLength + boundaryNewlineOffset;
896
                                        int writeBackAmount = (prevLength + curLength) - writeBackOffset;
897
                                        reader.Buffer(fullBuffer, writeBackOffset, writeBackAmount);
898

899
                                        break;
900
                                }
901

902
                                // No end, consume the entire previous buffer
903
                                FileHandler?.Invoke(name, filename, contentType, contentDisposition, prevBuffer, prevLength, partNumber++, additionalParameters);
904

905
                                // Now we want to swap the two buffers, we don't care
906
                                // what happens to the data from prevBuffer so we set
907
                                // curBuffer to it so it gets overwritten.
908
                                (prevBuffer, curBuffer) = (curBuffer, prevBuffer);
909

910
                                // We don't need to swap the lengths because
911
                                // curLength will be overwritten in the next
912
                                // iteration of the loop.
913
                                prevLength = curLength;
914
                        }
915
                        while (prevLength != 0);
916

917
                        Utilities.ArrayPool.Return(curBuffer);
918
                        Utilities.ArrayPool.Return(prevBuffer);
919
                        Utilities.ArrayPool.Return(fullBuffer);
920
                }
921

922
                /// <summary>
923
                ///     Parses a section of the stream that is known to be parameter data.
924
                /// </summary>
925
                /// <param name="parameters">
926
                ///     The header parameters of this section. "name" must be a valid key.
927
                /// </param>
928
                /// <param name="reader">
929
                ///     The StreamReader to read the data from.
930
                /// </param>
931
                /// <exception cref="MultipartParseException">
932
                ///     thrown if unexpected data is found such as running out of stream before hitting the boundary.
933
                /// </exception>
934
                private void ParseParameterPart(Dictionary<string, string> parameters, RebufferableBinaryReader reader)
935
                {
936
                        var data = new List<byte[]>();
507✔
937
                        var line = reader.ReadByteLine();
507✔
938
                        while (!line.SequenceEqual(boundaryBinary) && !line.SequenceEqual(endBoundaryBinary))
1,016✔
939
                        {
940
                                if (line == null) throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
509✔
941
                                data.Add(line);
509✔
942
                                line = reader.ReadByteLine();
509✔
943
                        }
944

945
                        if (line.SequenceEqual(endBoundaryBinary)) readEndBoundary = true;
757✔
946

947
                        var part = new ParameterPartBinary(parameters["name"], data);
507✔
948
                        ParameterHandler?.Invoke(part);
507✔
949
                }
507✔
950

951
                /// <summary>
952
                ///     Asynchronously parses a section of the stream that is known to be parameter data.
953
                /// </summary>
954
                /// <param name="parameters">
955
                ///     The header parameters of this section. "name" must be a valid key.
956
                /// </param>
957
                /// <param name="reader">
958
                ///     The StreamReader to read the data from.
959
                /// </param>
960
                /// <param name="cancellationToken">
961
                ///     The cancellation token.
962
                /// </param>
963
                /// <returns>
964
                ///     The asynchronous task.
965
                /// </returns>
966
                /// <exception cref="MultipartParseException">
967
                ///     thrown if unexpected data is found such as running out of stream before hitting the boundary.
968
                /// </exception>
969
                private async Task ParseParameterPartAsync(Dictionary<string, string> parameters, RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
970
                {
971
                        var data = new List<byte[]>();
972
                        var line = await reader.ReadByteLineAsync().ConfigureAwait(false);
973
                        while (!line.SequenceEqual(boundaryBinary) && !line.SequenceEqual(endBoundaryBinary))
974
                        {
975
                                if (line == null) throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
976
                                data.Add(line);
977
                                line = await reader.ReadByteLineAsync(cancellationToken).ConfigureAwait(false);
978
                        }
979

980
                        if (line.SequenceEqual(endBoundaryBinary)) readEndBoundary = true;
981

982
                        var part = new ParameterPartBinary(parameters["name"], data);
983
                        ParameterHandler?.Invoke(part);
984
                }
985

986
                /// <summary>
987
                ///     Skip a section of the stream.
988
                ///     This is used when a section is deemed to be invalid and the developer has requested to ignore invalid parts.
989
                /// </summary>
990
                /// <param name="reader">
991
                ///     The StreamReader to read the data from.
992
                /// </param>
993
                /// <exception cref="MultipartParseException">
994
                ///     thrown if unexpected data is found such as running out of stream before hitting the boundary.
995
                /// </exception>
996
                private void SkipPart(RebufferableBinaryReader reader)
997
                {
998
                        // Our job is to consume the lines in this section and discard them
999
                        var line = reader.ReadByteLine();
1✔
1000
                        while (!line.SequenceEqual(boundaryBinary) && !line.SequenceEqual(endBoundaryBinary))
1✔
1001
                        {
1002
                                if (line == null) throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
×
1003
                                line = reader.ReadByteLine();
×
1004
                        }
1005

1006
                        if (line.SequenceEqual(endBoundaryBinary)) readEndBoundary = true;
2✔
1007
                }
1✔
1008

1009
                /// <summary>
1010
                ///     Asynchronously skip a section of the stream.
1011
                ///     This is used when a section is deemed to be invalid and the developer has requested to ignore invalid parts.
1012
                /// </summary>
1013
                /// <param name="reader">
1014
                ///     The StreamReader to read the data from.
1015
                /// </param>
1016
                /// <param name="cancellationToken">
1017
                ///     The cancellation token.
1018
                /// </param>
1019
                /// <returns>
1020
                ///     The asynchronous task.
1021
                /// </returns>
1022
                /// <exception cref="MultipartParseException">
1023
                ///     thrown if unexpected data is found such as running out of stream before hitting the boundary.
1024
                /// </exception>
1025
                private async Task SkipPartAsync(RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
1026
                {
1027
                        // Our job is to consume the lines in this section and discard them
1028
                        var line = await reader.ReadByteLineAsync(cancellationToken).ConfigureAwait(false);
1029
                        while (!line.SequenceEqual(boundaryBinary) && !line.SequenceEqual(endBoundaryBinary))
1030
                        {
1031
                                if (line == null) throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
1032
                                line = await reader.ReadByteLineAsync(cancellationToken).ConfigureAwait(false);
1033
                        }
1034

1035
                        if (line.SequenceEqual(endBoundaryBinary)) readEndBoundary = true;
1036
                }
1037

1038
                /// <summary>
1039
                ///     Parses the header of the next section of the multipart stream and
1040
                ///     determines if it contains file data or parameter data.
1041
                /// </summary>
1042
                /// <param name="reader">
1043
                ///     The StreamReader to read data from.
1044
                /// </param>
1045
                /// <exception cref="MultipartParseException">
1046
                ///     thrown if unexpected data is hit such as end of stream.
1047
                /// </exception>
1048
                private void ParseSection(RebufferableBinaryReader reader)
1049
                {
1050
                        // Our first job is to determine what type of section this is: form data or file.
1051
                        // This is a bit tricky because files can still be encoded with Content-Disposition: form-data
1052
                        // in the case of single file uploads. Multi-file uploads have Content-Disposition: file according
1053
                        // to the spec however in practise it seems that multiple files will be represented by
1054
                        // multiple Content-Disposition: form-data files.
1055
                        var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
783✔
1056

1057
                        string line = reader.ReadLine();
783✔
1058
                        while (line != string.Empty)
1,833✔
1059
                        {
1060
                                if (line == null)
1,051✔
1061
                                {
1062
                                        throw new MultipartParseException("Unexpected end of stream");
1✔
1063
                                }
1064

1065
                                if (line == boundary || line == endBoundary)
1,050✔
1066
                                {
1067
                                        throw new MultipartParseException("Unexpected end of section");
×
1068
                                }
1069

1070
                                // This line parses the header values into a set of key/value pairs.
1071
                                // For example:
1072
                                //   Content-Disposition: form-data; name="textdata"
1073
                                //     ["content-disposition"] = "form-data"
1074
                                //     ["name"] = "textdata"
1075
                                //   Content-Disposition: form-data; name="file"; filename="data.txt"
1076
                                //     ["content-disposition"] = "form-data"
1077
                                //     ["name"] = "file"
1078
                                //     ["filename"] = "data.txt"
1079
                                //   Content-Type: text/plain
1080
                                //     ["content-type"] = "text/plain"
1081
                                Dictionary<string, string> values = SplitBySemicolonIgnoringSemicolonsInQuotes(line)
1,050✔
1082
                                        .Select(x => x.Split(new[] { ':', '=' }, 2))
1,050✔
1083

1,050✔
1084
                                        // select where the length of the array is equal to two, that way if it is only one it will
1,050✔
1085
                                        // be ignored as it is invalid key-pair
1,050✔
1086
                                        .Where(x => x.Length == 2)
1,050✔
1087

1,050✔
1088
                                        // Limit split to 2 splits so we don't accidently split characters in file paths.
1,050✔
1089
                                        .ToDictionary(
1,050✔
1090
                                                x => x[0].Trim().Replace("\"", string.Empty),
1,050✔
1091
                                                x => x[1].Trim().Replace("\"", string.Empty),
1,050✔
1092
                                                StringComparer.OrdinalIgnoreCase);
1,050✔
1093

1094
                                // Here we just want to push all the values that we just retrieved into the
1095
                                // parameters dictionary.
1096
                                try
1097
                                {
1098
                                        foreach (var pair in values)
6,292✔
1099
                                        {
1100
                                                parameters.Add(pair.Key, pair.Value);
2,096✔
1101
                                        }
1102
                                }
1,050✔
1103
                                catch (ArgumentException)
×
1104
                                {
1105
                                        throw new MultipartParseException("Duplicate field in section");
×
1106
                                }
1107

1108
                                line = reader.ReadLine();
1,050✔
1109
                        }
1110

1111
                        // Now that we've consumed all the parameters we're up to the body. We're going to do
1112
                        // different things depending on if we're parsing a, relatively small, form value or a
1113
                        // potentially large file.
1114
                        if (IsFilePart(parameters))
782✔
1115
                        {
1116
                                ParseFilePart(parameters, reader);
273✔
1117
                        }
1118
                        else if (IsParameterPart(parameters))
509✔
1119
                        {
1120
                                ParseParameterPart(parameters, reader);
507✔
1121
                        }
1122
                        else if (ignoreInvalidParts)
2✔
1123
                        {
1124
                                SkipPart(reader);
1✔
1125
                        }
1126
                        else
1127
                        {
1128
                                throw new MultipartParseException("Unable to determine the section type. Some possible reasons include: section is malformed, required parameters such as 'name', 'content-type' or 'filename' are missing, section contains nothing but empty lines.");
1✔
1129
                        }
1130
                }
1131

1132
                /// <summary>
1133
                ///     Asynchronously parses the header of the next section of the multipart stream and
1134
                ///     determines if it contains file data or parameter data.
1135
                /// </summary>
1136
                /// <param name="reader">
1137
                ///     The StreamReader to read data from.
1138
                /// </param>
1139
                /// <param name="cancellationToken">
1140
                ///     The cancellation token.
1141
                /// </param>
1142
                /// <returns>
1143
                ///     The asynchronous task.
1144
                /// </returns>
1145
                /// <exception cref="MultipartParseException">
1146
                ///     thrown if unexpected data is hit such as end of stream.
1147
                /// </exception>
1148
                private async Task ParseSectionAsync(RebufferableBinaryReader reader, CancellationToken cancellationToken)
1149
                {
1150
                        // Our first job is to determine what type of section this is: form data or file.
1151
                        // This is a bit tricky because files can still be encoded with Content-Disposition: form-data
1152
                        // in the case of single file uploads. Multi-file uploads have Content-Disposition: file according
1153
                        // to the spec however in practise it seems that multiple files will be represented by
1154
                        // multiple Content-Disposition: form-data files.
1155
                        var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
1156

1157
                        string line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
1158
                        while (line != string.Empty)
1159
                        {
1160
                                if (line == null)
1161
                                {
1162
                                        throw new MultipartParseException("Unexpected end of stream");
1163
                                }
1164

1165
                                if (line == boundary || line == endBoundary)
1166
                                {
1167
                                        throw new MultipartParseException("Unexpected end of section");
1168
                                }
1169

1170
                                // This line parses the header values into a set of key/value pairs.
1171
                                // For example:
1172
                                //   Content-Disposition: form-data; name="textdata"
1173
                                //     ["content-disposition"] = "form-data"
1174
                                //     ["name"] = "textdata"
1175
                                //   Content-Disposition: form-data; name="file"; filename="data.txt"
1176
                                //     ["content-disposition"] = "form-data"
1177
                                //     ["name"] = "file"
1178
                                //     ["filename"] = "data.txt"
1179
                                //   Content-Type: text/plain
1180
                                //     ["content-type"] = "text/plain"
1181
                                Dictionary<string, string> values = SplitBySemicolonIgnoringSemicolonsInQuotes(line)
1182
                                        .Select(x => x.Split(new[] { ':', '=' }, 2))
1183

1184
                                        // select where the length of the array is equal to two, that way if it is only one it will
1185
                                        // be ignored as it is invalid key-pair
1186
                                        .Where(x => x.Length == 2)
1187

1188
                                        // Limit split to 2 splits so we don't accidently split characters in file paths.
1189
                                        .ToDictionary(
1190
                                                x => x[0].Trim().Replace("\"", string.Empty),
1191
                                                x => x[1].Trim().Replace("\"", string.Empty),
1192
                                                StringComparer.OrdinalIgnoreCase);
1193

1194
                                // Here we just want to push all the values that we just retrieved into the
1195
                                // parameters dictionary.
1196
                                try
1197
                                {
1198
                                        foreach (var pair in values)
1199
                                        {
1200
                                                parameters.Add(pair.Key, pair.Value);
1201
                                        }
1202
                                }
1203
                                catch (ArgumentException)
1204
                                {
1205
                                        throw new MultipartParseException("Duplicate field in section");
1206
                                }
1207

1208
                                line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
1209
                        }
1210

1211
                        // Now that we've consumed all the parameters we're up to the body. We're going to do
1212
                        // different things depending on if we're parsing a, relatively small, form value or a
1213
                        // potentially large file.
1214
                        if (IsFilePart(parameters))
1215
                        {
1216
                                await ParseFilePartAsync(parameters, reader, cancellationToken).ConfigureAwait(false);
1217
                        }
1218
                        else if (IsParameterPart(parameters))
1219
                        {
1220
                                await ParseParameterPartAsync(parameters, reader, cancellationToken).ConfigureAwait(false);
1221
                        }
1222
                        else if (ignoreInvalidParts)
1223
                        {
1224
                                await SkipPartAsync(reader).ConfigureAwait(false);
1225
                        }
1226
                        else
1227
                        {
1228
                                throw new MultipartParseException("Unable to determine the section type. Some possible reasons include: section is malformed, required parameters such as 'name', 'content-type' or 'filename' are missing, section contains nothing but empty lines.");
1229
                        }
1230
                }
1231

1232
                /// <summary>
1233
                ///     Splits a line by semicolons but ignores semicolons in quotes.
1234
                /// </summary>
1235
                /// <param name="line">The line to split.</param>
1236
                /// <returns>The split strings.</returns>
1237
                private IEnumerable<string> SplitBySemicolonIgnoringSemicolonsInQuotes(string line)
1238
                {
1239
                        // Loop over the line looking for a semicolon. Keep track of if we're currently inside quotes
1240
                        // and if we are don't treat a semicolon as a splitting character.
1241
                        bool inQuotes = false;
1242
                        string workingString = string.Empty;
1243

1244
                        foreach (char c in line)
1245
                        {
1246
                                if (c == '"')
1247
                                {
1248
                                        inQuotes = !inQuotes;
1249
                                }
1250

1251
                                if (c == ';' && !inQuotes)
1252
                                {
1253
                                        yield return workingString;
1254
                                        workingString = string.Empty;
1255
                                }
1256
                                else
1257
                                {
1258
                                        workingString += c;
1259
                                }
1260
                        }
1261

1262
                        yield return workingString;
1263
                }
1264

1265
                /// <summary>
1266
                /// Check if there are parameters other than the "well known" parameters associated with this file.
1267
                /// This is quite rare but, as an example, the Alexa service includes a "content-ID" parameter with each file.
1268
                /// </summary>
1269
                /// <returns>A dictionary of parameters.</returns>
1270
                private IDictionary<string, string> GetAdditionalParameters(IDictionary<string, string> parameters)
1271
                {
1272
                        var wellKnownParameters = new[] { "name", "filename", "filename*", "content-type", "content-disposition" };
547✔
1273
                        var additionalParameters = parameters
547✔
1274
                                .Where(param => !wellKnownParameters.Contains(param.Key, StringComparer.OrdinalIgnoreCase))
547✔
1275
                                .ToDictionary(
547✔
1276
                                        x => x.Key,
547✔
1277
                                        x => x.Value,
547✔
1278
                                        StringComparer.OrdinalIgnoreCase);
547✔
1279
                        return additionalParameters;
547✔
1280
                }
1281
        }
1282
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc