Skip to content

Commit 03355fa

Browse files
committed
Merge branch 'release/8.4.0'
2 parents 8685415 + 9d340c5 commit 03355fa

File tree

6 files changed

+198
-9
lines changed

6 files changed

+198
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ var parser = await MultipartFormDataParser.ParseAsync(stream).ConfigureAwait(fal
7676
// From this point the data is parsed, we can retrieve the
7777
// form data using the GetParameterValue method.
7878
var username = parser.GetParameterValue("username");
79-
var email = parser.GetParameterValue("email")
79+
var email = parser.GetParameterValue("email");
8080

8181
// Files are stored in a list:
8282
var file = parser.Files.First();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Xunit;
6+
7+
namespace HttpMultipartParser.UnitTests.ParserScenarios
8+
{
9+
public class FilenameStar
10+
{
11+
private static readonly string _testData = TestUtil.TrimAllLines(
12+
@"--boundary
13+
Content-Disposition: form-data; name=""file1""; filename=""data.txt""; filename*=""iso-8859-1'en'%A3%20rates.txt"";
14+
Content-Type: text/plain
15+
16+
In this scenario, the filename* is preferred and filename is ignored.
17+
--boundary
18+
Content-Disposition: form-data; name=""file2""; filename*=""UTF-8''%c2%a3%20and%20%e2%82%ac%20rates.txt"";
19+
Content-Type: text/plain
20+
21+
In this scenario, only the filename* is provided.
22+
--boundary
23+
Content-Disposition: form-data; name=""file3""; filename=""Pr�sentation_Export.zip""; filename*=""utf-8''Pr%C3%A4sentation_Export.zip"";
24+
Content-Type: text/plain
25+
26+
This is the sample provided by @alexeyzimarev.
27+
--boundary--"
28+
);
29+
30+
/// <summary>
31+
/// Test case for files with filename*.
32+
/// </summary>
33+
private static readonly TestData _testCase = new TestData(
34+
_testData,
35+
new List<ParameterPart> { },
36+
new List<FilePart> {
37+
new FilePart("file1", "£ rates.txt", TestUtil.StringToStreamNoBom("In this scenario, the filename* is preferred and filename is ignored."), "text/plain", "form-data"),
38+
new FilePart("file2", "£ and € rates.txt", TestUtil.StringToStreamNoBom("In this scenario, only the filename* is provided."), "text/plain", "form-data"),
39+
new FilePart("file3", "Präsentation_Export.zip", TestUtil.StringToStreamNoBom("This is the sample provided by @alexeyzimarev."), "text/plain","form-data")
40+
}
41+
);
42+
43+
/// <summary>
44+
/// Initializes the test data before each run, this primarily
45+
/// consists of resetting data stream positions.
46+
/// </summary>
47+
public FilenameStar()
48+
{
49+
foreach (var filePart in _testCase.ExpectedFileData)
50+
{
51+
filePart.Data.Position = 0;
52+
}
53+
}
54+
55+
[Fact]
56+
public void FilenameStarTest()
57+
{
58+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
59+
{
60+
var parser = MultipartFormDataParser.Parse(stream, Encoding.UTF8);
61+
Assert.True(_testCase.Validate(parser));
62+
}
63+
}
64+
65+
[Fact]
66+
public async Task FilenameStarTest_Async()
67+
{
68+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
69+
{
70+
var parser = await MultipartFormDataParser.ParseAsync(stream, Encoding.UTF8);
71+
Assert.True(_testCase.Validate(parser));
72+
}
73+
}
74+
}
75+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// ===============================================================================
2+
// RFC5987 Decoder
3+
//
4+
// http://greenbytes.de/tech/webdav/rfc5987.html
5+
// ===============================================================================
6+
// Copyright Steven Robbins. All rights reserved.
7+
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
8+
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
9+
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
10+
// FITNESS FOR A PARTICULAR PURPOSE.
11+
// ===============================================================================
12+
namespace HttpMultipartParser
13+
{
14+
using System;
15+
using System.Collections.Generic;
16+
using System.Globalization;
17+
using System.Linq;
18+
using System.Text;
19+
using System.Text.RegularExpressions;
20+
21+
/// <summary>
22+
/// Provides a way to decode the value of so-called "star-parameters"
23+
/// according to RFC 5987 which is superceded by RFC 8187.
24+
///
25+
/// <see href="https://www.rfc-editor.org/rfc/rfc5987">RFC 5987</see>
26+
/// <see href="https://www.rfc-editor.org/rfc/rfc8187">RFC 8187</see>
27+
/// <see href="https://author-tools.ietf.org/diff?doc_1=5987&amp;doc_2=8187">Handy side-by-side comparison</see> of the two RFCs.
28+
/// </summary>
29+
/// <remarks>Taken from <see href="https://github.com/grumpydev/RFC5987-Decoder" />.</remarks>
30+
public static class RFC5987
31+
{
32+
/// <summary>
33+
/// Regex for the encoded string format detailed in
34+
/// http://greenbytes.de/tech/webdav/rfc5987.html.
35+
/// </summary>
36+
private static readonly Regex EncodedStringRegex = new Regex(@"(?:(?<charset>.*?))'(?<language>.*?)?'(?<encodeddata>.*?)$", RegexOptions.Compiled);
37+
38+
/// <summary>
39+
/// Decode a RFC5987 encoded value.
40+
/// </summary>
41+
/// <param name="inputString">Encoded input string.</param>
42+
/// <returns>Decoded string.</returns>
43+
public static string Decode(string inputString)
44+
{
45+
return EncodedStringRegex.Replace(
46+
inputString,
47+
m =>
48+
{
49+
var characterSet = m.Groups["charset"].Value;
50+
var language = m.Groups["language"].Value;
51+
var encodedData = m.Groups["encodeddata"].Value;
52+
53+
if (!IsSupportedCharacterSet(characterSet))
54+
{
55+
// Fall back to iso-8859-1 if invalid/unsupported character set found
56+
characterSet = @"UTF-8";
57+
}
58+
59+
var textEncoding = Encoding.GetEncoding(characterSet);
60+
61+
return textEncoding.GetString(GetDecodedBytes(encodedData).ToArray());
62+
});
63+
}
64+
65+
/// <summary>
66+
/// Get the decoded bytes from the encoded data string.
67+
/// </summary>
68+
/// <param name="encodedData">Encoded data.</param>
69+
/// <returns>Decoded bytes.</returns>
70+
private static IEnumerable<byte> GetDecodedBytes(string encodedData)
71+
{
72+
var encodedCharacters = encodedData.ToCharArray();
73+
for (int i = 0; i < encodedCharacters.Length; i++)
74+
{
75+
if (encodedCharacters[i] == '%')
76+
{
77+
var hexString = new string(encodedCharacters, i + 1, 2);
78+
79+
i += 2;
80+
81+
int characterValue;
82+
if (int.TryParse(hexString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue))
83+
{
84+
yield return (byte)characterValue;
85+
}
86+
}
87+
else
88+
{
89+
yield return (byte)encodedCharacters[i];
90+
}
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Determines if a character set is supported.
96+
/// </summary>
97+
/// <param name="characterSet">Character set name.</param>
98+
/// <returns>Bool representing whether the character set is supported.</returns>
99+
private static bool IsSupportedCharacterSet(string characterSet)
100+
{
101+
return Encoding.GetEncodings()
102+
.Where(e => string.Equals(e.Name, characterSet, StringComparison.InvariantCultureIgnoreCase))
103+
.Any();
104+
}
105+
}
106+
}

Source/HttpMultipartParser/StreamingBinaryMultipartFormDataParser.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ private bool IsFilePart(IDictionary<string, string> parameters)
361361
if (parameters.Count == 0) return false;
362362

363363
// If a section contains filename, then it's a file.
364-
else if (parameters.ContainsKey("filename")) return true;
364+
else if (parameters.ContainsKey("filename") || parameters.ContainsKey("filename*")) return true;
365365

366366
// Check if mimetype is a binary file
367367
else if (parameters.ContainsKey("content-type") && binaryMimeTypes.Contains(parameters["content-type"])) return true;
@@ -581,9 +581,13 @@ private void ParseFilePart(Dictionary<string, string> parameters, RebufferableBi
581581
// Read the parameters
582582
parameters.TryGetValue("name", out string name);
583583
parameters.TryGetValue("filename", out string filename);
584+
parameters.TryGetValue("filename*", out string filenameStar);
584585
parameters.TryGetValue("content-type", out string contentType);
585586
parameters.TryGetValue("content-disposition", out string contentDisposition);
586587

588+
// Per RFC6266 section 4.3, we should favor "filename*" over "filename"
589+
if (!string.IsNullOrEmpty(filenameStar)) filename = RFC5987.Decode(filenameStar);
590+
587591
// Filter out the "well known" parameters.
588592
var additionalParameters = GetAdditionalParameters(parameters);
589593

@@ -733,9 +737,13 @@ private async Task ParseFilePartAsync(Dictionary<string, string> parameters, Reb
733737
// Read the parameters
734738
parameters.TryGetValue("name", out string name);
735739
parameters.TryGetValue("filename", out string filename);
740+
parameters.TryGetValue("filename*", out string filenameStar);
736741
parameters.TryGetValue("content-type", out string contentType);
737742
parameters.TryGetValue("content-disposition", out string contentDisposition);
738743

744+
// Per RFC6266 section 4.3, we should favor "filename*" over "filename"
745+
if (!string.IsNullOrEmpty(filenameStar)) filename = RFC5987.Decode(filenameStar);
746+
739747
// Filter out the "well known" parameters.
740748
var additionalParameters = GetAdditionalParameters(parameters);
741749

@@ -1214,7 +1222,7 @@ private IEnumerable<string> SplitBySemicolonIgnoringSemicolonsInQuotes(string li
12141222
/// <returns>A dictionary of parameters.</returns>
12151223
private IDictionary<string, string> GetAdditionalParameters(IDictionary<string, string> parameters)
12161224
{
1217-
var wellKnownParameters = new[] { "name", "filename", "content-type", "content-disposition" };
1225+
var wellKnownParameters = new[] { "name", "filename", "filename*", "content-type", "content-disposition" };
12181226
var additionalParameters = parameters
12191227
.Where(param => !wellKnownParameters.Contains(param.Key, StringComparer.OrdinalIgnoreCase))
12201228
.ToDictionary(

build.cake

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// Install tools.
22
#tool dotnet:?package=GitVersion.Tool&version=5.12.0
33
#tool dotnet:?package=coveralls.net&version=4.0.1
4-
#tool nuget:https://f.feedz.io/jericho/jericho/nuget/?package=GitReleaseManager&version=0.17.0-collaborators0003
5-
#tool nuget:?package=ReportGenerator&version=5.2.0
6-
#tool nuget:?package=xunit.runner.console&version=2.6.5
7-
#tool nuget:?package=CodecovUploader&version=0.7.1
4+
#tool nuget:https://f.feedz.io/jericho/jericho/nuget/?package=GitReleaseManager&version=0.17.0-collaborators0004
5+
#tool nuget:?package=ReportGenerator&version=5.2.4
6+
#tool nuget:?package=xunit.runner.console&version=2.7.0
7+
#tool nuget:?package=CodecovUploader&version=0.7.2
88

99
// Install addins.
1010
#addin nuget:?package=Cake.Coveralls&version=1.1.0
11-
#addin nuget:?package=Cake.Git&version=3.0.0
11+
#addin nuget:?package=Cake.Git&version=4.0.0
1212
#addin nuget:?package=Cake.Codecov&version=1.0.1
1313

1414

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.100",
3+
"version": "8.0.203",
44
"rollForward": "patch",
55
"allowPrerelease": false
66
}

0 commit comments

Comments
 (0)