mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Use ASCII chars in Content-Disposition filename parameter
Prior to this commit, gh-36328 avoided using RFC 2047 encoding for the
"filename" parameter and use ISO-8859-1 only. This change unfortunately
caused issues because some implementations might try and detect the
encoding automatically.
This commit restricts the filename parameter to ASCII encoding only by:
* transliterating characters to the closes ASCII character
("é"->"e", "ä"->"ae"...)
* falling back to "_" for other chacacters with non latin alphabet or
emojis
Fixes gh-36805
This commit is contained in:
@@ -17,12 +17,9 @@
|
||||
package org.springframework.http;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.Normalizer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
@@ -47,6 +44,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @author Sergey Tsypanov
|
||||
* @since 5.0
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6266">RFC 6266</a>
|
||||
@@ -174,19 +172,19 @@ public final class ContentDisposition {
|
||||
sb.append(this.type);
|
||||
}
|
||||
if (this.name != null) {
|
||||
sb.append("; name=\"");
|
||||
sb.append(this.name).append('\"');
|
||||
sb.append("; name=\"").append(this.name).append('\"');
|
||||
}
|
||||
if (this.filename != null) {
|
||||
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
|
||||
sb.append("; filename=\"");
|
||||
sb.append(encodeQuotedPairs(this.filename)).append('\"');
|
||||
sb.append("; filename=\"")
|
||||
.append(encodeQuotedPairs(this.filename))
|
||||
.append('\"');
|
||||
}
|
||||
else {
|
||||
sb.append("; filename=\"");
|
||||
sb.append(toIso88591(encodeQuotedPairs(this.filename))).append('\"');
|
||||
sb.append("; filename*=");
|
||||
sb.append(encodeRfc5987Filename(this.filename, this.charset));
|
||||
sb.append("; filename=\"")
|
||||
.append(transliterateToAscii(encodeQuotedPairs(this.filename)))
|
||||
.append("\"; filename*=")
|
||||
.append(encodeRfc5987Filename(this.filename, this.charset));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
@@ -435,16 +433,74 @@ public final class ContentDisposition {
|
||||
return StreamUtils.copyToString(baos, charset);
|
||||
}
|
||||
|
||||
private static String toIso88591(String input) {
|
||||
CharsetEncoder encoder = ISO_8859_1.newEncoder()
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE)
|
||||
.replaceWith(new byte[] { (byte) '_' });
|
||||
try {
|
||||
return ISO_8859_1.decode(encoder.encode(CharBuffer.wrap(input))).toString();
|
||||
}
|
||||
catch (CharacterCodingException exc) {
|
||||
throw new IllegalArgumentException("Failed to convert to ISO 8859-1", exc);
|
||||
private static String transliterateToAscii(String input) {
|
||||
StringBuilder sb = new StringBuilder(input.length() + 16);
|
||||
for (int i = 0; i < input.length();) {
|
||||
int codePoint = input.codePointAt(i);
|
||||
i += Character.charCount(codePoint);
|
||||
if (codePoint <= 127) {
|
||||
sb.append((char) codePoint);
|
||||
}
|
||||
else {
|
||||
switch (codePoint) {
|
||||
case 'ä':
|
||||
sb.append("ae");
|
||||
break;
|
||||
case 'ö':
|
||||
sb.append("oe");
|
||||
break;
|
||||
case 'ü':
|
||||
sb.append("ue");
|
||||
break;
|
||||
case 'Ä':
|
||||
sb.append("Ae");
|
||||
break;
|
||||
case 'Ö':
|
||||
sb.append("Oe");
|
||||
break;
|
||||
case 'Ü':
|
||||
sb.append("Ue");
|
||||
break;
|
||||
case 'ß':
|
||||
sb.append("ss");
|
||||
break;
|
||||
case 'æ':
|
||||
sb.append("ae");
|
||||
break;
|
||||
case 'Æ':
|
||||
sb.append("AE");
|
||||
break;
|
||||
case 'œ':
|
||||
sb.append("oe");
|
||||
break;
|
||||
case 'Œ':
|
||||
sb.append("OE");
|
||||
break;
|
||||
default:
|
||||
String cpStr = new String(Character.toChars(codePoint));
|
||||
// decompose accented characters into two separate parts
|
||||
String normalized = Normalizer.normalize(cpStr, Normalizer.Form.NFD);
|
||||
for (int j = 0; j < normalized.length(); ) {
|
||||
int ncp = normalized.codePointAt(j);
|
||||
j += Character.charCount(ncp);
|
||||
if (ncp <= 127) {
|
||||
sb.append((char) ncp);
|
||||
}
|
||||
else {
|
||||
int type = Character.getType(ncp);
|
||||
// do not write fallback character for accents
|
||||
if (type != Character.NON_SPACING_MARK &&
|
||||
type != Character.COMBINING_SPACING_MARK &&
|
||||
type != Character.ENCLOSING_MARK) {
|
||||
sb.append('_');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String encodeQuotedPairs(String filename) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.http;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -30,293 +31,10 @@ import static org.springframework.http.ContentDisposition.parse;
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ContentDispositionTests {
|
||||
|
||||
|
||||
@Test
|
||||
void parseFilenameQuoted() {
|
||||
assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\""))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFilenameUnquoted() {
|
||||
assertThat(parse("form-data; filename=unquoted"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.filename("unquoted")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test // SPR-16091
|
||||
void parseFilenameWithSemicolon() {
|
||||
assertThat(parse("attachment; filename=\"filename with ; semicolon.txt\""))
|
||||
.isEqualTo(ContentDisposition.attachment()
|
||||
.filename("filename with ; semicolon.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilename() {
|
||||
assertThat(parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test // gh-24112
|
||||
void parseEncodedFilenameWithPaddedCharset() {
|
||||
assertThat(parse("attachment; filename*= UTF-8''some-file.zip"))
|
||||
.isEqualTo(ContentDisposition.attachment()
|
||||
.filename("some-file.zip", StandardCharsets.UTF_8)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test // gh-26463
|
||||
void parseBase64EncodedFilename() {
|
||||
String input = "attachment; filename=\"=?UTF-8?B?5pel5pys6KqeLmNzdg==?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBase64EncodedFilenameMultipleSegments() {
|
||||
String input =
|
||||
"attachment; filename=\"=?utf-8?B?U3ByaW5n5qGG5p625Li65Z+65LqOSmF2YeeahOeOsOS7o+S8geS4muW6lA==?= " +
|
||||
"=?utf-8?B?55So56iL5bqP5o+Q5L6b5LqG5YWo6Z2i55qE57yW56iL5ZKM6YWN572u5qih?= " +
|
||||
"=?utf-8?B?5Z6LLnR4dA==?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("Spring框架为基于Java的现代企业应用程序提供了全面的编程和配置模型.txt");
|
||||
}
|
||||
|
||||
@Test // gh-26463
|
||||
void parseBase64EncodedShiftJISFilename() {
|
||||
String input = "attachment; filename=\"=?SHIFT_JIS?B?k/qWe4zqLmNzdg==?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseQuotedPrintableFilename() {
|
||||
String input = "attachment; filename=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E.csv?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseQuotedPrintableFilenameMultipleSegments() {
|
||||
String input =
|
||||
"attachment; filename=\"=?utf-8?Q?Spring=E6=A1=86=E6=9E=B6=E4=B8=BA=E5=9F=BA=E4=BA=8E?=" +
|
||||
"=?utf-8?Q?Java=E7=9A=84=E7=8E=B0=E4=BB=A3=E4=BC=81=E4=B8=9A=E5=BA=94?=" +
|
||||
"=?utf-8?Q?=E7=94=A8=E7=A8=8B=E5=BA=8F=E6=8F=90=E4=BE=9B=E4=BA=86=E5=85=A8?=" +
|
||||
"=?utf-8?Q?=E9=9D=A2=E7=9A=84=E7=BC=96=E7=A8=8B=E5=92=8C=E9=85=8D=E7=BD=AE?=" +
|
||||
"=?utf-8?Q?=E6=A8=A1=E5=9E=8B.txt?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("Spring框架为基于Java的现代企业应用程序提供了全面的编程和配置模型.txt");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseQuotedPrintableShiftJISFilename() {
|
||||
String input = "attachment; filename=\"=?SHIFT_JIS?Q?=93=FA=96{=8C=EA.csv?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilenameWithoutCharset() {
|
||||
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("test.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilenameWithInvalidCharset() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-16''test.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilenameWithInvalidName() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-8''%A"));
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-8''%A.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBackslash() {
|
||||
String s = "form-data; name=\"foo\"; filename=\"foo\\\\bar \\\"baz\\\" qux \\\\\\\" quux.txt\"";
|
||||
ContentDisposition cd = ContentDisposition.parse(
|
||||
s);
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("foo\\bar \"baz\" qux \\\" quux.txt");
|
||||
assertThat(cd.toString()).isEqualTo(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBackslashInLastPosition() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"bar\\\"");
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("bar\\");
|
||||
assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"bar\\\\\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWindowsPath() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"D:\\foo\\bar.txt\"");
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("D:\\foo\\bar.txt");
|
||||
assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"D:\\\\foo\\\\bar.txt\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithExtraSemicolons() {
|
||||
assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\";"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseAttributesCaseInsensitively() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; Name=\"foo\"; FileName=\"bar.txt\"");
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("bar.txt");
|
||||
assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"bar.txt\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEmpty() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> parse(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseNoType() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> parse(";"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInvalidParameter() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> parse("foo;bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void format() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.build().toString())
|
||||
.isEqualTo("form-data; name=\"foo\"; filename=\"foo.txt\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithEncodedFilename() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||
.build().toString())
|
||||
.isEqualTo("form-data; name=\"name\"; " +
|
||||
"filename=\"__.txt\"; " +
|
||||
"filename*=UTF-8''%E4%B8%AD%E6%96%87.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithEncodedFilenameUsingUsAscii() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("test.txt", StandardCharsets.US_ASCII)
|
||||
.build()
|
||||
.toString())
|
||||
.isEqualTo("form-data; name=\"name\"; filename=\"test.txt\"");
|
||||
}
|
||||
|
||||
@Test // gh-24220
|
||||
void formatWithFilenameWithQuotes() {
|
||||
BiConsumer<String, String> tester = (input, output) -> {
|
||||
assertThat(ContentDisposition.formData().filename(input).build().toString())
|
||||
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||
assertThat(ContentDisposition.formData().filename(input, StandardCharsets.US_ASCII).build().toString())
|
||||
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||
};
|
||||
|
||||
String filename = "\"foo.txt";
|
||||
tester.accept(filename, "\\\"foo.txt");
|
||||
|
||||
filename = "\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\"foo.txt");
|
||||
|
||||
filename = "\\\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\\\\\"foo.txt");
|
||||
|
||||
filename = "\\\\\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\\\\\\\\\"foo.txt");
|
||||
|
||||
filename = "\\\\\\\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\\\\\\\\\\\\\"foo.txt");
|
||||
|
||||
tester.accept("\"\"foo.txt", "\\\"\\\"foo.txt");
|
||||
tester.accept("\"\"\"foo.txt", "\\\"\\\"\\\"foo.txt");
|
||||
|
||||
tester.accept("foo.txt\\", "foo.txt\\\\");
|
||||
tester.accept("foo.txt\\\\", "foo.txt\\\\\\\\");
|
||||
tester.accept("foo.txt\\\\\\", "foo.txt\\\\\\\\\\\\");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithUtf8FilenameWithQuotes() {
|
||||
String filename = "\"中文.txt";
|
||||
assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString())
|
||||
.isEqualTo("form-data; filename=\"\\\"__.txt\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithEncodedFilenameUsingInvalidCharset() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("test.txt", StandardCharsets.UTF_16)
|
||||
.build()
|
||||
.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFormatted() {
|
||||
ContentDisposition cd = ContentDisposition.builder("form-data")
|
||||
.name("foo")
|
||||
.filename("foo\\bar \"baz\" qux \\\" quux.txt").build();
|
||||
ContentDisposition parsed = ContentDisposition.parse(cd.toString());
|
||||
assertThat(parsed).isEqualTo(cd);
|
||||
assertThat(parsed.toString()).isEqualTo(cd.toString());
|
||||
}
|
||||
|
||||
@Test // gh-30252
|
||||
void parseFormattedWithQuestionMark() {
|
||||
String filename = "filename with ?问号.txt";
|
||||
ContentDisposition cd = ContentDisposition.attachment()
|
||||
.filename(filename, StandardCharsets.UTF_8)
|
||||
.build();
|
||||
String result = cd.toString();
|
||||
assertThat(result).isEqualTo("attachment; " +
|
||||
"filename=\"filename with ?__.txt\"; " +
|
||||
"filename*=UTF-8''filename%20with%20%3F%E9%97%AE%E5%8F%B7.txt");
|
||||
|
||||
String[] parts = result.split("; ");
|
||||
|
||||
String quotedPrintableFilename = parts[0] + "; " + parts[1];
|
||||
assertThat(ContentDisposition.parse(quotedPrintableFilename).getFilename())
|
||||
.isEqualTo("filename with ?__.txt");
|
||||
|
||||
String rfc5987Filename = parts[0] + "; " + parts[2];
|
||||
assertThat(ContentDisposition.parse(rfc5987Filename).getFilename())
|
||||
.isEqualTo(filename);
|
||||
}
|
||||
|
||||
@Test
|
||||
void attachmentType(){
|
||||
ContentDisposition attachment = ContentDisposition.attachment().build();
|
||||
@@ -341,4 +59,307 @@ class ContentDispositionTests {
|
||||
assertThat(inline.isInline()).isTrue();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ParsingTests {
|
||||
|
||||
@Test
|
||||
void parseFilenameQuoted() {
|
||||
assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\""))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFilenameUnquoted() {
|
||||
assertThat(parse("form-data; filename=unquoted"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.filename("unquoted")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test // SPR-16091
|
||||
void parseFilenameWithSemicolon() {
|
||||
assertThat(parse("attachment; filename=\"filename with ; semicolon.txt\""))
|
||||
.isEqualTo(ContentDisposition.attachment()
|
||||
.filename("filename with ; semicolon.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilename() {
|
||||
assertThat(parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test // gh-24112
|
||||
void parseEncodedFilenameWithPaddedCharset() {
|
||||
assertThat(parse("attachment; filename*= UTF-8''some-file.zip"))
|
||||
.isEqualTo(ContentDisposition.attachment()
|
||||
.filename("some-file.zip", StandardCharsets.UTF_8)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test // gh-26463
|
||||
void parseBase64EncodedFilename() {
|
||||
String input = "attachment; filename=\"=?UTF-8?B?5pel5pys6KqeLmNzdg==?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBase64EncodedFilenameMultipleSegments() {
|
||||
String input =
|
||||
"attachment; filename=\"=?utf-8?B?U3ByaW5n5qGG5p625Li65Z+65LqOSmF2YeeahOeOsOS7o+S8geS4muW6lA==?= " +
|
||||
"=?utf-8?B?55So56iL5bqP5o+Q5L6b5LqG5YWo6Z2i55qE57yW56iL5ZKM6YWN572u5qih?= " +
|
||||
"=?utf-8?B?5Z6LLnR4dA==?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("Spring框架为基于Java的现代企业应用程序提供了全面的编程和配置模型.txt");
|
||||
}
|
||||
|
||||
@Test // gh-26463
|
||||
void parseBase64EncodedShiftJISFilename() {
|
||||
String input = "attachment; filename=\"=?SHIFT_JIS?B?k/qWe4zqLmNzdg==?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseQuotedPrintableFilename() {
|
||||
String input = "attachment; filename=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E.csv?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseQuotedPrintableFilenameMultipleSegments() {
|
||||
String input =
|
||||
"attachment; filename=\"=?utf-8?Q?Spring=E6=A1=86=E6=9E=B6=E4=B8=BA=E5=9F=BA=E4=BA=8E?=" +
|
||||
"=?utf-8?Q?Java=E7=9A=84=E7=8E=B0=E4=BB=A3=E4=BC=81=E4=B8=9A=E5=BA=94?=" +
|
||||
"=?utf-8?Q?=E7=94=A8=E7=A8=8B=E5=BA=8F=E6=8F=90=E4=BE=9B=E4=BA=86=E5=85=A8?=" +
|
||||
"=?utf-8?Q?=E9=9D=A2=E7=9A=84=E7=BC=96=E7=A8=8B=E5=92=8C=E9=85=8D=E7=BD=AE?=" +
|
||||
"=?utf-8?Q?=E6=A8=A1=E5=9E=8B.txt?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("Spring框架为基于Java的现代企业应用程序提供了全面的编程和配置模型.txt");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseQuotedPrintableShiftJISFilename() {
|
||||
String input = "attachment; filename=\"=?SHIFT_JIS?Q?=93=FA=96{=8C=EA.csv?=\"";
|
||||
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilenameWithoutCharset() {
|
||||
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("test.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilenameWithInvalidCharset() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-16''test.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEncodedFilenameWithInvalidName() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-8''%A"));
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> parse("form-data; name=\"name\"; filename*=UTF-8''%A.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBackslash() {
|
||||
String s = "form-data; name=\"foo\"; filename=\"foo\\\\bar \\\"baz\\\" qux \\\\\\\" quux.txt\"";
|
||||
ContentDisposition cd = ContentDisposition.parse(s);
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("foo\\bar \"baz\" qux \\\" quux.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBackslashInLastPosition() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"bar\\\"");
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("bar\\");
|
||||
assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"bar\\\\\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWindowsPath() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"D:\\foo\\bar.txt\"");
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("D:\\foo\\bar.txt");
|
||||
assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"D:\\\\foo\\\\bar.txt\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithExtraSemicolons() {
|
||||
assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\";"))
|
||||
.isEqualTo(ContentDisposition.formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseAttributesCaseInsensitively() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; Name=\"foo\"; FileName=\"bar.txt\"");
|
||||
assertThat(cd.getName()).isEqualTo("foo");
|
||||
assertThat(cd.getFilename()).isEqualTo("bar.txt");
|
||||
assertThat(cd.toString()).isEqualTo("form-data; name=\"foo\"; filename=\"bar.txt\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEmpty() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> parse(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseNoType() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> parse(";"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseInvalidParameter() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> parse("foo;bar"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void parseFormatted() {
|
||||
ContentDisposition cd = ContentDisposition.builder("form-data")
|
||||
.name("foo")
|
||||
.filename("foo\\bar \"baz\" qux \\\" quux.txt").build();
|
||||
ContentDisposition parsed = ContentDisposition.parse(cd.toString());
|
||||
assertThat(parsed).isEqualTo(cd);
|
||||
assertThat(parsed.toString()).isEqualTo(cd.toString());
|
||||
}
|
||||
|
||||
@Test // gh-30252
|
||||
void parseFormattedWithQuestionMark() {
|
||||
String filename = "filename with ?问号.txt";
|
||||
ContentDisposition cd = ContentDisposition.attachment()
|
||||
.filename(filename, StandardCharsets.UTF_8)
|
||||
.build();
|
||||
String result = cd.toString();
|
||||
assertThat(result).isEqualTo("attachment; " +
|
||||
"filename=\"filename with ?__.txt\"; " +
|
||||
"filename*=UTF-8''filename%20with%20%3F%E9%97%AE%E5%8F%B7.txt");
|
||||
|
||||
String[] parts = result.split("; ");
|
||||
|
||||
String quotedPrintableFilename = parts[0] + "; " + parts[1];
|
||||
assertThat(ContentDisposition.parse(quotedPrintableFilename).getFilename())
|
||||
.isEqualTo("filename with ?__.txt");
|
||||
|
||||
String rfc5987Filename = parts[0] + "; " + parts[2];
|
||||
assertThat(ContentDisposition.parse(rfc5987Filename).getFilename())
|
||||
.isEqualTo(filename);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FormattingTests {
|
||||
|
||||
@Test
|
||||
void format() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("foo")
|
||||
.filename("foo.txt")
|
||||
.build().toString())
|
||||
.isEqualTo("form-data; name=\"foo\"; filename=\"foo.txt\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithEncodedFilename() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("Äsprïngö.txt", StandardCharsets.UTF_8)
|
||||
.build().toString())
|
||||
.isEqualTo("form-data; name=\"name\"; " +
|
||||
"filename=\"Aespringoe.txt\"; " +
|
||||
"filename*=UTF-8''%C3%84spr%C3%AFng%C3%B6.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithUnmappableCharacters() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||
.build().toString())
|
||||
.isEqualTo("form-data; name=\"name\"; " +
|
||||
"filename=\"__.txt\"; " +
|
||||
"filename*=UTF-8''%E4%B8%AD%E6%96%87.txt");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void formatWithEncodedFilenameUsingUsAscii() {
|
||||
assertThat(
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("test.txt", StandardCharsets.US_ASCII)
|
||||
.build()
|
||||
.toString())
|
||||
.isEqualTo("form-data; name=\"name\"; filename=\"test.txt\"");
|
||||
}
|
||||
|
||||
@Test // gh-24220
|
||||
void formatWithFilenameWithQuotes() {
|
||||
BiConsumer<String, String> tester = (input, output) -> {
|
||||
assertThat(ContentDisposition.formData().filename(input).build().toString())
|
||||
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||
assertThat(ContentDisposition.formData().filename(input, StandardCharsets.US_ASCII).build().toString())
|
||||
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||
};
|
||||
|
||||
String filename = "\"foo.txt";
|
||||
tester.accept(filename, "\\\"foo.txt");
|
||||
|
||||
filename = "\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\"foo.txt");
|
||||
|
||||
filename = "\\\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\\\\\"foo.txt");
|
||||
|
||||
filename = "\\\\\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\\\\\\\\\"foo.txt");
|
||||
|
||||
filename = "\\\\\\\\\"foo.txt";
|
||||
tester.accept(filename, "\\\\\\\\\\\\\\\\\\\"foo.txt");
|
||||
|
||||
tester.accept("\"\"foo.txt", "\\\"\\\"foo.txt");
|
||||
tester.accept("\"\"\"foo.txt", "\\\"\\\"\\\"foo.txt");
|
||||
|
||||
tester.accept("foo.txt\\", "foo.txt\\\\");
|
||||
tester.accept("foo.txt\\\\", "foo.txt\\\\\\\\");
|
||||
tester.accept("foo.txt\\\\\\", "foo.txt\\\\\\\\\\\\");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithUtf8FilenameWithQuotes() {
|
||||
String filename = "\"中文.txt";
|
||||
assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString())
|
||||
.isEqualTo("form-data; filename=\"\\\"__.txt\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatWithEncodedFilenameUsingInvalidCharset() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ContentDisposition.formData()
|
||||
.name("name")
|
||||
.filename("test.txt", StandardCharsets.UTF_16)
|
||||
.build()
|
||||
.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user