Skip to content

Commit 2125897

Browse files
committed
Merge branch 'release/9.0.0'
2 parents 03355fa + 501b4e1 commit 2125897

File tree

12 files changed

+265
-95
lines changed

12 files changed

+265
-95
lines changed

.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"isRoot": true,
44
"tools": {
55
"cake.tool": {
6-
"version": "4.0.0",
6+
"version": "5.0.0",
77
"commands": [
88
"dotnet-cake"
99
]

GitVersion.yml

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,7 @@
1-
mode: ContinuousDelivery
1+
workflow: GitFlow/v1 # https://github.com/GitTools/GitVersion/blob/main/docs/input/docs/reference/configuration.md#snippet-/docs/workflows/GitFlow/v1.yml
2+
23
branches:
3-
master:
4-
regex: (master|main)
5-
mode: ContinuousDelivery
6-
tag:
7-
increment: Patch
8-
prevent-increment-of-merged-branch-version: true
9-
track-merge-target: false
10-
feature:
11-
regex: feature(s)?[/-]
12-
mode: ContinuousDeployment
134
develop:
14-
regex: dev(elop)?(ment)?$
15-
mode: ContinuousDeployment
16-
tag: beta
17-
hotfix:
18-
regex: hotfix(es)?[/-]
19-
mode: ContinuousDeployment
20-
tag: hotfix
5+
label: beta # default is 'alpha' for the 'develop' branch. I prefer 'beta'.
216
release:
22-
regex: release(s)?[/-]
23-
mode: ContinuousDeployment
24-
tag: rc
25-
ignore:
26-
sha: []
7+
label: rc # default is 'beta' for the 'release' branch. I prefer 'RC'.

Source/HttpMultipartParser.Benchmark/HttpMultipartParser.Benchmark.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net7.0</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
9+
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
1010
</ItemGroup>
1111

1212
<ItemGroup>

Source/HttpMultipartParser.UnitTests/HttpMultipartParser.UnitTests.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net48;net6.0;net7.0</TargetFrameworks>
4+
<TargetFrameworks>net48;net9.0</TargetFrameworks>
55
<AssemblyName>HttpMultipartParser.UnitTests</AssemblyName>
66
<RootNamespace>HttpMultipartParser.UnitTests</RootNamespace>
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="coverlet.msbuild" Version="6.0.0">
10+
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
1111
<PrivateAssets>all</PrivateAssets>
1212
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1313
</PackageReference>
14-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
15-
<PackageReference Include="xunit" Version="2.6.5" />
16-
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
15+
<PackageReference Include="xunit" Version="2.9.3" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
1717
<PrivateAssets>all</PrivateAssets>
1818
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1919
</PackageReference>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 BoundaryEndsWithDoubleDash
11+
{
12+
// The boundary in this scenario ends with '--'. This is an unusual scenario but perfectly legitimate.
13+
// For details, see: https://github.com/Http-Multipart-Data-Parser/Http-Multipart-Data-Parser/issues/123
14+
private static readonly string _testData = TestUtil.TrimAllLines(
15+
@"--boundary_text--
16+
Content-type: text/plain; charset=UTF-8
17+
Content-Disposition: form-data; name=""file1""; filename=""file1.txt""
18+
19+
file content here
20+
--boundary_text--
21+
Content-type: text/plain; charset=UTF-8
22+
Content-Disposition: form-data; name=""file2""; filename=""file2.txt""
23+
24+
file content here 2
25+
--boundary_text----"
26+
);
27+
28+
private static readonly TestData _testCase = new TestData(
29+
_testData,
30+
Enumerable.Empty<ParameterPart>().ToList(),
31+
new List<FilePart>() {
32+
new FilePart("file1", "file1.txt", TestUtil.StringToStreamNoBom("file content here"), (new[] { new KeyValuePair<string, string>("charset", "UTF-8") }).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)),
33+
new FilePart("file2", "file2.txt", TestUtil.StringToStreamNoBom("file content here 2"), (new[] { new KeyValuePair<string, string>("charset", "UTF-8") }).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)),
34+
}
35+
);
36+
37+
public BoundaryEndsWithDoubleDash()
38+
{
39+
foreach (var filePart in _testCase.ExpectedFileData)
40+
{
41+
filePart.Data.Position = 0;
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Tests for correct detection of the boundary in the input stream.
47+
/// </summary>
48+
[Fact]
49+
public void CanAutoDetectBoundary()
50+
{
51+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
52+
{
53+
var parser = MultipartFormDataParser.Parse(stream);
54+
Assert.True(_testCase.Validate(parser));
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Tests for correct detection of the boundary in the input stream.
60+
/// </summary>
61+
[Fact]
62+
public async Task CanAutoDetectBoundaryAsync()
63+
{
64+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
65+
{
66+
var parser = await MultipartFormDataParser.ParseAsync(stream, Encoding.UTF8);
67+
Assert.True(_testCase.Validate(parser));
68+
}
69+
}
70+
}
71+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.IO;
2+
using System.Text;
3+
using System.Threading.Tasks;
4+
using Xunit;
5+
6+
namespace HttpMultipartParser.UnitTests
7+
{
8+
/// <summary>
9+
/// Unit tests for StreamingMultipartFormDataParser.
10+
/// </summary>
11+
public class StreamingMultipartFormDataParserUnitTests
12+
{
13+
private static readonly string _testData = TestUtil.TrimAllLines(
14+
@"--boundary
15+
Content-Disposition: form-data; name=""parameter1""
16+
17+
This is a sample parameter
18+
--boundary
19+
Content-Disposition: form-data; name=""file1""; filename=""file1.txt""
20+
Content-Type: text/plain
21+
22+
This is the content of a sample file
23+
--boundary--"
24+
);
25+
26+
[Fact]
27+
public void CanHandleNullDelegates()
28+
{
29+
using (Stream stream = TestUtil.StringToStream(_testData, Encoding.UTF8))
30+
{
31+
var parser = new StreamingMultipartFormDataParser(stream);
32+
33+
// Intentionally setting these handlers to null to verify that we can parse the stream despite missing handlers
34+
// See: https://github.com/Http-Multipart-Data-Parser/Http-Multipart-Data-Parser/issues/121
35+
parser.ParameterHandler = null;
36+
parser.FileHandler = null;
37+
38+
parser.Run();
39+
}
40+
}
41+
42+
[Fact]
43+
public async Task CanHandleNullDelegatesAsync()
44+
{
45+
using (Stream stream = TestUtil.StringToStream(_testData, Encoding.UTF8))
46+
{
47+
var parser = new StreamingMultipartFormDataParser(stream);
48+
49+
// Intentionally setting these handlers to null to verify that we can parse the stream despite missing handlers
50+
// See: https://github.com/Http-Multipart-Data-Parser/Http-Multipart-Data-Parser/issues/121
51+
parser.ParameterHandler = null;
52+
parser.FileHandler = null;
53+
54+
await parser.RunAsync();
55+
}
56+
}
57+
}
58+
}

Source/HttpMultipartParser/HttpMultipartParser.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net48;netstandard2.1;net6.0;net7.0</TargetFrameworks>
4+
<TargetFrameworks>net48;netstandard2.1</TargetFrameworks>
55
<LangVersion>preview</LangVersion>
66
<PlatformTarget>anycpu</PlatformTarget>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -40,10 +40,10 @@
4040
</PropertyGroup>
4141

4242
<ItemGroup>
43-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
44-
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
43+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
44+
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
4545
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
46-
<PackageReference Include="System.Buffers" Version="4.5.1" />
46+
<PackageReference Include="System.Buffers" Version="4.6.0" />
4747
</ItemGroup>
4848

4949
<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">

Source/HttpMultipartParser/StreamingBinaryMultipartFormDataParser.cs

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,12 @@ private static string DetectBoundary(RebufferableBinaryReader reader)
277277
// Remove the two dashes
278278
string boundary = line.Substring(2);
279279

280-
// If the string ends with '--' it means that we found the "end" boundary and we
281-
// need to trim the two dashes to get the actual boundary
282-
if (boundary.EndsWith("--"))
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);
285+
if (boundary.EndsWith("--") && !moreContentAvailable)
283286
{
284287
boundary = boundary.Substring(0, boundary.Length - 2);
285288
reader.Buffer($"--{boundary}--\n");
@@ -333,9 +336,12 @@ private static async Task<string> DetectBoundaryAsync(RebufferableBinaryReader r
333336
// Remove the two dashes
334337
string boundary = line.Substring(2);
335338

336-
// If the string ends with '--' it means that we found the "end" boundary and we
337-
// need to trim the two dashes to get the actual boundary.
338-
if (boundary.EndsWith("--"))
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)
339345
{
340346
boundary = boundary.Substring(0, boundary.Length - 2);
341347
reader.Buffer($"--{boundary}--\n");
@@ -348,6 +354,47 @@ private static async Task<string> DetectBoundaryAsync(RebufferableBinaryReader r
348354
return boundary;
349355
}
350356

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();
369+
370+
if (line == null) return false;
371+
else reader.Buffer($"{line}\n");
372+
373+
return true;
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+
351398
/// <summary>
352399
/// Use a few assumptions to determine if a section contains a file.
353400
/// </summary>
@@ -686,7 +733,7 @@ private void ParseFilePart(Dictionary<string, string> parameters, RebufferableBi
686733
// We also want to chop off the newline that is inserted by the protocol.
687734
// We can do this by reducing endPos by the length of newline in this environment
688735
// and encoding
689-
FileHandler(name, filename, contentType, contentDisposition, fullBuffer, endPos - bufferNewlineLength, partNumber++, additionalParameters);
736+
FileHandler?.Invoke(name, filename, contentType, contentDisposition, fullBuffer, endPos - bufferNewlineLength, partNumber++, additionalParameters);
690737

691738
int writeBackOffset = endPos + endPosLength + boundaryNewlineOffset;
692739
int writeBackAmount = (prevLength + curLength) - writeBackOffset;
@@ -696,7 +743,7 @@ private void ParseFilePart(Dictionary<string, string> parameters, RebufferableBi
696743
}
697744

698745
// No end, consume the entire previous buffer
699-
FileHandler(name, filename, contentType, contentDisposition, prevBuffer, prevLength, partNumber++, additionalParameters);
746+
FileHandler?.Invoke(name, filename, contentType, contentDisposition, prevBuffer, prevLength, partNumber++, additionalParameters);
700747

701748
// Now we want to swap the two buffers, we don't care
702749
// what happens to the data from prevBuffer so we set
@@ -843,7 +890,7 @@ private async Task ParseFilePartAsync(Dictionary<string, string> parameters, Reb
843890
// We also want to chop off the newline that is inserted by the protocl.
844891
// We can do this by reducing endPos by the length of newline in this environment
845892
// and encoding
846-
FileHandler(name, filename, contentType, contentDisposition, fullBuffer, endPos - bufferNewlineLength, partNumber++, additionalParameters);
893+
FileHandler?.Invoke(name, filename, contentType, contentDisposition, fullBuffer, endPos - bufferNewlineLength, partNumber++, additionalParameters);
847894

848895
int writeBackOffset = endPos + endPosLength + boundaryNewlineOffset;
849896
int writeBackAmount = (prevLength + curLength) - writeBackOffset;
@@ -853,7 +900,7 @@ private async Task ParseFilePartAsync(Dictionary<string, string> parameters, Reb
853900
}
854901

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

858905
// Now we want to swap the two buffers, we don't care
859906
// what happens to the data from prevBuffer so we set
@@ -898,7 +945,7 @@ private void ParseParameterPart(Dictionary<string, string> parameters, Rebuffera
898945
if (line.SequenceEqual(endBoundaryBinary)) readEndBoundary = true;
899946

900947
var part = new ParameterPartBinary(parameters["name"], data);
901-
ParameterHandler(part);
948+
ParameterHandler?.Invoke(part);
902949
}
903950

904951
/// <summary>
@@ -933,7 +980,7 @@ private async Task ParseParameterPartAsync(Dictionary<string, string> parameters
933980
if (line.SequenceEqual(endBoundaryBinary)) readEndBoundary = true;
934981

935982
var part = new ParameterPartBinary(parameters["name"], data);
936-
ParameterHandler(part);
983+
ParameterHandler?.Invoke(part);
937984
}
938985

939986
/// <summary>

0 commit comments

Comments
 (0)