diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java
index 5498133bbcd..6e2e5f2eb1d 100644
--- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java
+++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java
@@ -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 RFC 6266
@@ -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) {
diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java
index fc8e6985715..2928b836763 100644
--- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java
+++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java
@@ -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 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 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());
+ }
+ }
+
}