|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Text;
namespace Microsoft.Net.Http.Headers;
public class ContentDispositionHeaderValueTest
{
[Fact]
public void Ctor_ContentDispositionNull_Throw()
{
Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(null));
}
[Fact]
public void Ctor_ContentDispositionEmpty_Throw()
{
// null and empty should be treated the same. So we also throw for empty strings.
Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(string.Empty));
}
[Fact]
public void Ctor_ContentDispositionInvalidFormat_ThrowFormatException()
{
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
AssertFormatException(" inline ");
AssertFormatException(" inline");
AssertFormatException("inline ");
AssertFormatException("\"inline\"");
AssertFormatException("te xt");
AssertFormatException("te=xt");
AssertFormatException("teäxt");
AssertFormatException("text;");
AssertFormatException("te/xt;");
AssertFormatException("inline; name=someName; ");
AssertFormatException("text;name=someName"); // ctor takes only disposition-type name, no parameters
}
[Fact]
public void Ctor_ContentDispositionValidFormat_SuccessfullyCreated()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Equal("inline", contentDisposition.DispositionType.AsSpan());
Assert.Empty(contentDisposition.Parameters);
Assert.Null(contentDisposition.Name.Value);
Assert.Null(contentDisposition.FileName.Value);
Assert.Null(contentDisposition.CreationDate);
Assert.Null(contentDisposition.ModificationDate);
Assert.Null(contentDisposition.ReadDate);
Assert.Null(contentDisposition.Size);
}
[Fact]
public void Parameters_AddNull_Throw()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Throws<ArgumentNullException>(() => contentDisposition.Parameters.Add(null!));
}
[Fact]
public void ContentDisposition_SetAndGetContentDisposition_MatchExpectations()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Equal("inline", contentDisposition.DispositionType.AsSpan());
contentDisposition.DispositionType = "attachment";
Assert.Equal("attachment", contentDisposition.DispositionType.AsSpan());
}
[Fact]
public void Name_SetNameAndValidateObject_ParametersEntryForNameAdded()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.Name = "myname";
Assert.Equal("myname", contentDisposition.Name.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("name", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Name = null;
Assert.Null(contentDisposition.Name.Value);
Assert.Empty(contentDisposition.Parameters);
contentDisposition.Name = null; // It's OK to set it again to null; no exception.
}
[Fact]
public void Name_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
NameValueHeaderValue name = new NameValueHeaderValue("NAME", "old_name");
contentDisposition.Parameters.Add(name);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("NAME", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Name = "new_name";
Assert.Equal("new_name", contentDisposition.Name.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("NAME", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Parameters.Remove(name);
Assert.Null(contentDisposition.Name.Value);
}
[Fact]
public void FileName_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileName = new NameValueHeaderValue("FILENAME", "old_name");
contentDisposition.Parameters.Add(fileName);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.FileName = "new_name";
Assert.Equal("new_name", contentDisposition.FileName.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Parameters.Remove(fileName);
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileName_NeedsEncoding_EncodedAndDecodedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileName = "FileÃName.bat";
Assert.Equal("FileÃName.bat", contentDisposition.FileName.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("filename", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value.AsSpan());
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileName_NeedsEncodingBecauseOfNewLine_EncodedAndDecodedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileName = "File\nName.bat";
Assert.Equal("File\nName.bat", contentDisposition.FileName.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("filename", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("\"=?utf-8?B?RmlsZQpOYW1lLmJhdA==?=\"", contentDisposition.Parameters.First().Value.AsSpan());
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileName_UnknownOrBadEncoding_PropertyFails()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileName = new NameValueHeaderValue("FILENAME", "\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"");
contentDisposition.Parameters.Add(fileName);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value.AsSpan());
Assert.Equal("=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=", contentDisposition.FileName.AsSpan());
contentDisposition.FileName = "new_name";
Assert.Equal("new_name", contentDisposition.FileName.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Parameters.Remove(fileName);
Assert.Null(contentDisposition.FileName.Value);
}
[Fact]
public void FileNameStar_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileNameStar = new NameValueHeaderValue("FILENAME*", "old_name");
contentDisposition.Parameters.Add(fileNameStar);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure
contentDisposition.FileNameStar = "new_name";
Assert.Equal("new_name", contentDisposition.FileNameStar.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value.AsSpan());
contentDisposition.Parameters.Remove(fileNameStar);
Assert.Null(contentDisposition.FileNameStar.Value);
}
[Fact]
public void FileNameStar_NeedsEncoding_EncodedAndDecodedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileNameStar = "FileÃName.bat";
Assert.Equal("FileÃName.bat", contentDisposition.FileNameStar.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("filename*", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value.AsSpan());
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
Assert.Null(contentDisposition.FileNameStar.Value);
}
[Fact]
public void NonValidAscii_WhenNeedsEncoding_UsesHex()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileNameStar = "a\u0080b";
Assert.Equal($"UTF-8\'\'a%C2%80b", contentDisposition.Parameters.First().Value.AsSpan()); //%C2 added because the value in UTF-8 is encoded on 2 bytes.
}
[Fact]
public void LongValidAscii_FullyProcessedWithout()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileNameStar = new string('a', 400); // 400 is larger to the max stackallow size
Assert.Equal($"UTF-8\'\'{new string('a', 400)}", contentDisposition.Parameters.First().Value.AsSpan());
}
[Fact]
public void FileNameStar_WhenNeedsEncoding_UsesHex()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
foreach (byte b in Enumerable.Range(0, 128))
{
contentDisposition.FileNameStar = $"a{(char)b}b";
if (b <= 0x20
|| b == '"'
|| b == '%'
|| (b >= 0x27 && b <= 0x2A)
|| b == ','
|| b == '/'
|| (b >= 0x3A && b <= 0x40)
|| (b >= 0x5B && b <= 0x5D)
|| (b >= 0x61 && b <= 0x5D)
|| b == '{'
|| b == '}'
|| b >= 0x7F)
{
var hexC = Convert.ToHexString([b]);
Assert.Equal($"UTF-8\'\'a%{hexC}b", contentDisposition.Parameters.First().Value.AsSpan());
}
else
{
Assert.Equal($"UTF-8\'\'a{(char)b}b", contentDisposition.Parameters.First().Value.AsSpan());
}
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
}
}
[Fact]
public void FileNameStar_UnknownOrBadEncoding_PropertyFails()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var fileNameStar = new NameValueHeaderValue("FILENAME*", "utf-99'lang'File%CZName.bat");
contentDisposition.Parameters.Add(fileNameStar);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value.AsSpan());
Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure
contentDisposition.FileNameStar = "new_name";
Assert.Equal("new_name", contentDisposition.FileNameStar.AsSpan());
Assert.Single(contentDisposition.Parameters);
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Parameters.Remove(fileNameStar);
Assert.Null(contentDisposition.FileNameStar.Value);
}
[Theory]
[InlineData("FileName.bat", "FileName.bat")]
[InlineData("FileÃName.bat", "File_Name.bat")]
[InlineData("File\nName.bat", "File_Name.bat")]
[InlineData("File\x19Name.bat", "File_Name.bat")]
[InlineData("File\x20Name.bat", "File\x20Name.bat")] // First unsanitized
[InlineData("File\x7eName.bat", "File\x7eName.bat")] // Last unsanitized
[InlineData("File\x7fName.bat", "File_Name.bat")]
public void SetHttpFileName_ShouldSanitizeFileNameWhereNeeded(string httpFileName, string expectedFileName)
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.SetHttpFileName(httpFileName);
Assert.Equal(expectedFileName.AsSpan(), contentDisposition.FileName);
Assert.Equal(httpFileName.AsSpan(), contentDisposition.FileNameStar); // Should roundtrip through FileNameStar encoding
}
[Fact]
public void Dates_AddDateParameterThenUseProperty_ParametersEntryIsOverwritten()
{
string validDateString = "\"Tue, 15 Nov 1994 08:12:31 GMT\"";
DateTimeOffset validDate = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT", CultureInfo.InvariantCulture);
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var dateParameter = new NameValueHeaderValue("Creation-DATE", validDateString);
contentDisposition.Parameters.Add(dateParameter);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal(validDate, contentDisposition.CreationDate);
var newDate = validDate.AddSeconds(1);
contentDisposition.CreationDate = newDate;
Assert.Equal(newDate, contentDisposition.CreationDate);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal("\"Tue, 15 Nov 1994 08:12:32 GMT\"", contentDisposition.Parameters.First().Value.AsSpan());
contentDisposition.Parameters.Remove(dateParameter);
Assert.Null(contentDisposition.CreationDate);
}
[Fact]
public void Dates_InvalidDates_PropertyFails()
{
string invalidDateString = "\"Tue, 15 Nov 94 08:12 GMT\"";
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var dateParameter = new NameValueHeaderValue("read-DATE", invalidDateString);
contentDisposition.Parameters.Add(dateParameter);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("read-DATE", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Null(contentDisposition.ReadDate);
contentDisposition.ReadDate = null;
Assert.Null(contentDisposition.ReadDate);
Assert.Empty(contentDisposition.Parameters);
}
[Fact]
public void Size_AddSizeParameterThenUseProperty_ParametersEntryIsOverwritten()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var sizeParameter = new NameValueHeaderValue("SIZE", "279172874239");
contentDisposition.Parameters.Add(sizeParameter);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Equal(279172874239, contentDisposition.Size);
contentDisposition.Size = 279172874240;
Assert.Equal(279172874240, contentDisposition.Size);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Parameters.Remove(sizeParameter);
Assert.Null(contentDisposition.Size);
}
[Fact]
public void Size_InvalidSizes_PropertyFails()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
// Note that uppercase letters are used. Comparison should happen case-insensitive.
var sizeParameter = new NameValueHeaderValue("SIZE", "-279172874239");
contentDisposition.Parameters.Add(sizeParameter);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name.AsSpan());
Assert.Null(contentDisposition.Size);
// Negatives not allowed
Assert.Throws<ArgumentOutOfRangeException>(() => contentDisposition.Size = -279172874240);
Assert.Null(contentDisposition.Size);
Assert.Single(contentDisposition.Parameters);
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name.AsSpan());
contentDisposition.Parameters.Remove(sizeParameter);
Assert.Null(contentDisposition.Size);
}
[Fact]
public void ToString_UseDifferentContentDispositions_AllSerializedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
Assert.Equal("inline", contentDisposition.ToString());
contentDisposition.Name = "myname";
Assert.Equal("inline; name=myname", contentDisposition.ToString());
contentDisposition.FileName = "my File Name";
Assert.Equal("inline; name=myname; filename=\"my File Name\"", contentDisposition.ToString());
contentDisposition.CreationDate = new DateTimeOffset(new DateTime(2011, 2, 15), new TimeSpan(-8, 0, 0));
Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"", contentDisposition.ToString());
contentDisposition.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\""));
Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString());
contentDisposition.Name = null;
Assert.Equal("inline; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString());
contentDisposition.FileNameStar = "File%Name";
Assert.Equal("inline; filename=\"my File Name\"; creation-date="
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name",
contentDisposition.ToString());
contentDisposition.FileName = null;
Assert.Equal("inline; creation-date=\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\";"
+ " filename*=UTF-8\'\'File%25Name", contentDisposition.ToString());
contentDisposition.CreationDate = null;
Assert.Equal("inline; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name",
contentDisposition.ToString());
}
[Fact]
public void GetHashCode_UseContentDispositionWithAndWithoutParameters_SameOrDifferentHashCodes()
{
var contentDisposition1 = new ContentDispositionHeaderValue("inline");
var contentDisposition2 = new ContentDispositionHeaderValue("inline");
contentDisposition2.Name = "myname";
var contentDisposition3 = new ContentDispositionHeaderValue("inline");
contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value"));
var contentDisposition4 = new ContentDispositionHeaderValue("INLINE");
var contentDisposition5 = new ContentDispositionHeaderValue("INLINE");
contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition2.GetHashCode());
Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition3.GetHashCode());
Assert.NotEqual(contentDisposition2.GetHashCode(), contentDisposition3.GetHashCode());
Assert.Equal(contentDisposition1.GetHashCode(), contentDisposition4.GetHashCode());
Assert.Equal(contentDisposition2.GetHashCode(), contentDisposition5.GetHashCode());
}
[Fact]
public void Equals_UseContentDispositionWithAndWithoutParameters_EqualOrNotEqualNoExceptions()
{
var contentDisposition1 = new ContentDispositionHeaderValue("inline");
var contentDisposition2 = new ContentDispositionHeaderValue("inline");
contentDisposition2.Name = "myName";
var contentDisposition3 = new ContentDispositionHeaderValue("inline");
contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value"));
var contentDisposition4 = new ContentDispositionHeaderValue("INLINE");
var contentDisposition5 = new ContentDispositionHeaderValue("INLINE");
contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
var contentDisposition6 = new ContentDispositionHeaderValue("INLINE");
contentDisposition6.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
contentDisposition6.Parameters.Add(new NameValueHeaderValue("custom", "value"));
var contentDisposition7 = new ContentDispositionHeaderValue("attachment");
Assert.False(contentDisposition1.Equals(contentDisposition2), "No params vs. name.");
Assert.False(contentDisposition2.Equals(contentDisposition1), "name vs. no params.");
Assert.False(contentDisposition1.Equals(null), "No params vs. <null>.");
Assert.False(contentDisposition1!.Equals(contentDisposition3), "No params vs. custom param.");
Assert.False(contentDisposition2.Equals(contentDisposition3), "name vs. custom param.");
Assert.True(contentDisposition1.Equals(contentDisposition4), "Different casing.");
Assert.True(contentDisposition2.Equals(contentDisposition5), "Different casing in name.");
Assert.False(contentDisposition5.Equals(contentDisposition6), "name vs. custom param.");
Assert.False(contentDisposition1.Equals(contentDisposition7), "inline vs. text/other.");
}
[Fact]
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
{
var expected = new ContentDispositionHeaderValue("inline");
CheckValidParse("\r\n inline ", expected);
CheckValidParse("inline", expected);
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
// The purpose of this test is to verify that these other parsers are combined correctly to build a
// Content-Disposition parser.
expected.Name = "myName";
CheckValidParse("\r\n inline ; name = myName ", expected);
CheckValidParse(" inline;name=myName", expected);
expected.Name = null;
expected.DispositionType = "attachment";
expected.FileName = "foo-ae.html";
expected.Parameters.Add(new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4.html"));
CheckValidParse(@"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=foo-ae.html", expected);
}
[Fact]
public void Parse_SetOfInvalidValueStrings_Throws()
{
CheckInvalidParse("");
CheckInvalidParse(" ");
CheckInvalidParse(null);
CheckInvalidParse("inline会");
CheckInvalidParse("inline ,");
CheckInvalidParse("inline,");
CheckInvalidParse("inline; name=myName ,");
CheckInvalidParse("inline; name=myName,");
CheckInvalidParse("inline; name=my会Name");
CheckInvalidParse("inline/");
}
[Fact]
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
{
var expected = new ContentDispositionHeaderValue("inline");
CheckValidTryParse("\r\n inline ", expected);
CheckValidTryParse("inline", expected);
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
// The purpose of this test is to verify that these other parsers are combined correctly to build a
// Content-Disposition parser.
expected.Name = "myName";
CheckValidTryParse("\r\n inline ; name = myName ", expected);
CheckValidTryParse(" inline;name=myName", expected);
}
[Fact]
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
{
CheckInvalidTryParse("");
CheckInvalidTryParse(" ");
CheckInvalidTryParse(null);
CheckInvalidTryParse("inline会");
CheckInvalidTryParse("inline ,");
CheckInvalidTryParse("inline,");
CheckInvalidTryParse("inline; name=myName ,");
CheckInvalidTryParse("inline; name=myName,");
CheckInvalidTryParse("text/");
}
public static TheoryData<string, ContentDispositionHeaderValue> ValidContentDispositionTestCases = new TheoryData<string, ContentDispositionHeaderValue>()
{
{ "inline", new ContentDispositionHeaderValue("inline") }, // @"This should be equivalent to not including the header at all."
{ "inline;", new ContentDispositionHeaderValue("inline") },
{ "inline;name=", new ContentDispositionHeaderValue("inline") { Parameters = { new NameValueHeaderValue("name", "") } } }, // TODO: passing in a null value causes a strange assert on CoreCLR before the test even starts. Not reproducible in the body of a test.
{ "inline;name=value", new ContentDispositionHeaderValue("inline") { Name = "value" } },
{ "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } },
{ "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } },
{ @"inline; filename=""foo.html""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.html""" } },
{ @"inline; filename=""Not an attachment!""", new ContentDispositionHeaderValue("inline") { FileName = @"""Not an attachment!""" } }, // 'inline', specifying a filename of Not an attachment! - this checks for proper parsing for disposition types.
{ @"inline; filename=""foo.pdf""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.pdf""" } },
{ "attachment", new ContentDispositionHeaderValue("attachment") },
{ "ATTACHMENT", new ContentDispositionHeaderValue("ATTACHMENT") },
{ @"attachment; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
{ @"attachment; filename=""\""quoting\"" tested.html""", new ContentDispositionHeaderValue("attachment") { FileName = "\"\"quoting\" tested.html\"" } }, // 'attachment', specifying a filename of \"quoting\" tested.html (using double quotes around "quoting" to test... quoting)
{ @"attachment; filename=""Here's a semicolon;.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""Here's a semicolon;.html""" } }, // , 'attachment', specifying a filename of Here's a semicolon;.html - this checks for proper parsing for parameters.
{ @"attachment; foo=""bar""; filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""bar""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.).
{ @"attachment; foo=""\""\\"";filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""\""\\""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.). The extension parameter actually uses backslash-escapes. This tests whether the UA properly skips the parameter.
{ @"attachment; FILENAME=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
{ @"attachment; filename=foo.html", new ContentDispositionHeaderValue("attachment") { FileName = "foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string.
{ @"attachment; filename='foo.bar'", new ContentDispositionHeaderValue("attachment") { FileName = "'foo.bar'" } }, // 'attachment', specifying a filename of 'foo.bar' using single quotes.
{ @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("filename", @"""foo-ä.html""") } } }, // 'attachment', specifying a filename of foo-ä.html, using plain ISO-8859-1
{ @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ä.html""" } }, // 'attachment', specifying a filename of foo-ä.html, which happens to be foo-ä.html using UTF-8 encoding.
{ @"attachment; filename=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%41.html""") } } },
{ @"attachment; filename=""50%.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""50%.html""") } } },
{ @"attachment; filename=""foo-%\41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%\41.html""") } } }, // 'attachment', specifying a filename of foo-%41.html, using an escape character (this tests whether adding an escape character inside a %xx sequence can be used to disable the non-conformant %xx-unescaping).
{ @"attachment; name=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Name = @"""foo-%41.html""" } }, // 'attachment', specifying a <i>name</i> parameter of foo-%41.html. (this test was added to observe the behavior of the (unspecified) treatment of ""name"" as synonym for ""filename""; see <a href=""http://www.imc.org/ietf-smtp/mail-archive/msg05023.html"">Ned Freed's summary</a> where this comes from in MIME messages)
{ @"attachment; filename=""ä-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""ä-%41.html""") } } }, // 'attachment', specifying a filename parameter of ä-%41.html. (this test was added to observe the behavior when non-ASCII characters and percent-hexdig sequences are combined)
{ @"attachment; filename=""foo-%c3%a4-%e2%82%ac.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-%c3%a4-%e2%82%ac.html""" } }, // 'attachment', specifying a filename of foo-%c3%a4-%e2%82%ac.html, using raw percent encoded UTF-8 to represent foo-ä-€.html
{ @"attachment; filename =""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
{ @"attachment; xfilename=foo.html", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("xfilename", "foo.html") } } },
{ @"attachment; filename=""/foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""/foo.html""" } },
{ @"attachment; creation-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("creation-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } },
{ @"attachment; modification-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("modification-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } },
{ @"foobar", new ContentDispositionHeaderValue("foobar") }, // @"This should be equivalent to using ""attachment""."
{ @"attachment; example=""filename=example.txt""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("example", @"""filename=example.txt""") } } },
{ @"attachment; filename*=iso-8859-1''foo-%E4.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "iso-8859-1''foo-%E4.html") } } }, // 'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded ISO-8859-1
{ @"attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4-%e2%82%ac.html") } } }, // 'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8
{ @"attachment; filename*=''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "''foo-%c3%a4-%e2%82%ac.html") } } }, // Behavior is undefined in RFC 2231, the charset part is missing, although UTF-8 was used.
{ @"attachment; filename*=UTF-8''foo-a%22.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"foo-a"".html" } },
{ @"attachment; filename*= UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } },
{ @"attachment; filename* =UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } },
{ @"attachment; filename*=UTF-8''A-%2541.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "A-%41.html" } },
{ @"attachment; filename*=UTF-8''%5cfoo.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"\foo.html" } },
{ @"attachment; filename=""foo-ae.html""; filename*=UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ae.html""", FileNameStar = "foo-ä.html" } },
{ @"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=""foo-ae.html""", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html", FileName = @"""foo-ae.html""" } },
{ @"attachment; foobar=x; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foobar", "x") } } },
{ @"attachment; filename=""=?ISO-8859-1?Q?foo-=E4.html?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?ISO-8859-1?Q?foo-=E4.html?=""" } }, // attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="
{ @"attachment; filename=""=?utf-8?B?Zm9vLeQuaHRtbA==?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?utf-8?B?Zm9vLeQuaHRtbA==?=""" } }, // attachment; filename="=?utf-8?B?Zm9vLeQuaHRtbA==?="
{ @"attachment; filename=foo.html ;", new ContentDispositionHeaderValue("attachment") { FileName="foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string, and adding a trailing semicolon.,
};
[Theory]
[MemberData(nameof(ValidContentDispositionTestCases))]
public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected)
{
// System.Diagnostics.Debugger.Launch();
var result = ContentDispositionHeaderValue.Parse(input);
Assert.Equal(expected, result);
}
[Theory]
// Invalid values
[InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) },
[InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) },
[InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) },
// Duplicate file name parameter
// @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) },
[InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP <a href=""http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-latest.html#rfc.section.1.2.2"">token</a> production.", false) },
[InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) },
// HTML escaping, not supported
// @"attachment; filename=foo-ä.html", // "'attachment', specifying a filename of foo-ä.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) },
[InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) },
[InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) },
[InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) },
[InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) },
[InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) },
// This is permitted as a parameter without a value
// @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) },
// This is permitted as a parameter without a value
// @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) },
[InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) },
[InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) },
[InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) },
[InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to <a href=""http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.2.p.5"">RFC 2616, Section 4.2</a>, roughly equivalent to having two separate header field instances.", false) },
[InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) },
// Escaping is not verified
// @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) },
// Escaping is not verified
// @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) },
// Escaping is not verified
// @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) },
[InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) },
[InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) },
public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input)
{
Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input));
}
[Fact]
public void HeaderNamesWithQuotes_ExpectNamesToNotHaveQuotes()
{
var contentDispositionLine = "form-data; name =\"dotnet\"; filename=\"example.png\"";
var expectedName = "dotnet";
var expectedFileName = "example.png";
var result = ContentDispositionHeaderValue.Parse(contentDispositionLine);
Assert.Equal(expectedName.AsSpan(), result.Name);
Assert.Equal(expectedFileName.AsSpan(), result.FileName);
}
[Fact]
public void FileNameWithSurrogatePairs_EncodedCorrectly()
{
var contentDisposition = new ContentDispositionHeaderValue("attachment");
contentDisposition.SetHttpFileName("File 🤩 name.txt");
Assert.Equal("File __ name.txt", contentDisposition.FileName.AsSpan());
Assert.Equal(2, contentDisposition.Parameters.Count);
Assert.Equal("UTF-8\'\'File%20%F0%9F%A4%A9%20name.txt", contentDisposition.Parameters[1].Value.AsSpan());
}
public class ContentDispositionValue
{
public ContentDispositionValue(string value, string description, bool valid)
{
Value = value;
Description = description;
Valid = valid;
}
public string Value { get; }
public string Description { get; }
public bool Valid { get; }
}
private void CheckValidParse(string? input, ContentDispositionHeaderValue expectedResult)
{
var result = ContentDispositionHeaderValue.Parse(input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidParse(string? input)
{
Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input));
}
private void CheckValidTryParse(string? input, ContentDispositionHeaderValue expectedResult)
{
Assert.True(ContentDispositionHeaderValue.TryParse(input, out var result), input);
Assert.Equal(expectedResult, result);
}
private void CheckInvalidTryParse(string? input)
{
Assert.False(ContentDispositionHeaderValue.TryParse(input, out var result), input);
Assert.Null(result);
}
private static void AssertFormatException(string contentDisposition)
{
Assert.Throws<FormatException>(() => new ContentDispositionHeaderValue(contentDisposition));
}
}
|