Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.BitSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -59,23 +58,9 @@ public final class ContentDisposition {
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
"Invalid header field parameter format (as defined in RFC 5987)";

private static final BitSet PRINTABLE = new BitSet(256);

private static final HexFormat HEX_FORMAT = HexFormat.of().withUpperCase();


static {
// RFC 2045, Section 6.7, and RFC 2047, Section 4.2
for (int i=33; i<= 126; i++) {
PRINTABLE.set(i);
}
PRINTABLE.set(34, false); // "
PRINTABLE.set(61, false); // =
PRINTABLE.set(63, false); // ?
PRINTABLE.set(95, false); // _
}


private final @Nullable String type;

private final @Nullable String name;
Expand Down Expand Up @@ -195,7 +180,7 @@ public String toString() {
}
else {
sb.append("; filename=\"");
sb.append(encodeQuotedPrintableFilename(this.filename, this.charset)).append('\"');
sb.append(toIso88591(encodeQuotedPairs(this.filename))).append('\"');
sb.append("; filename*=");
sb.append(encodeRfc5987Filename(this.filename, this.charset));
}
Expand Down Expand Up @@ -446,44 +431,8 @@ else if (b == '=' && index < value.length - 2) {
return StreamUtils.copyToString(baos, charset);
}

/**
* Encode the given header field param as described in RFC 2047.
* @param filename the filename
* @param charset the charset for the filename
* @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc2047">RFC 2047</a>
*/
private static String encodeQuotedPrintableFilename(String filename, Charset charset) {
Assert.notNull(filename, "'filename' must not be null");
Assert.notNull(charset, "'charset' must not be null");

byte[] source = filename.getBytes(charset);
StringBuilder sb = new StringBuilder(source.length << 1);
sb.append("=?");
sb.append(charset.name());
sb.append("?Q?");
for (byte b : source) {
if (b == 32) { // RFC 2047, section 4.2, rule (2)
sb.append('_');
}
else if (isPrintable(b)) {
sb.append((char) b);
}
else {
sb.append('=');
HEX_FORMAT.toHexDigits(sb, b);
}
}
sb.append("?=");
return sb.toString();
}

private static boolean isPrintable(byte c) {
int b = c;
if (b < 0) {
b = 256 + b;
}
return PRINTABLE.get(b);
private static String toIso88591(String input) {
return new String(input.getBytes(StandardCharsets.ISO_8859_1));
Copy link

@shardt68 shardt68 Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would lead to Questionmarks "?" in the propose filename, which is not allowed in most filesystems,
I would rather propose an underline in place of non ascii characters
Suggetion:
// NFD decomposition splits characters like ä into base character 'a' + combining diacritic.
// Removing the combining diacritics (Unicode category Mn) gives a readable ASCII approximation
// (e.g. "Schöne Äpfel" → "Schone Apfel") without resorting to '?' which is a forbidden
// filename character on Windows.
String decomposed = java.text.Normalizer.normalize(input, java.text.Normalizer.Form.NFD);
return decomposed
.replaceAll("\p{InCombiningDiacriticalMarks}", "")
.replaceAll("[^\\x20-\\x7E]", "_");

}

private static String encodeQuotedPairs(String filename) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ void formatWithEncodedFilename() {
.filename("中文.txt", StandardCharsets.UTF_8)
.build().toString())
.isEqualTo("form-data; name=\"name\"; " +
"filename=\"=?UTF-8?Q?=E4=B8=AD=E6=96=87.txt?=\"; " +
"filename=\"??.txt\"; " +
"filename*=UTF-8''%E4%B8%AD%E6%96%87.txt");
}

Expand Down Expand Up @@ -272,7 +272,7 @@ void formatWithFilenameWithQuotes() {
void formatWithUtf8FilenameWithQuotes() {
String filename = "\"中文.txt";
assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString())
.isEqualTo("form-data; filename=\"=?UTF-8?Q?=22=E4=B8=AD=E6=96=87.txt?=\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
.isEqualTo("form-data; filename=\"\\\"??.txt\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
}

@Test
Expand Down Expand Up @@ -303,14 +303,14 @@ void parseFormattedWithQuestionMark() {
.build();
String result = cd.toString();
assertThat(result).isEqualTo("attachment; " +
"filename=\"=?UTF-8?Q?filename_with_=3F=E9=97=AE=E5=8F=B7.txt?=\"; " +
"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);
.isEqualTo("filename with ???.txt");

String rfc5987Filename = parts[0] + "; " + parts[2];
assertThat(ContentDisposition.parse(rfc5987Filename).getFilename())
Expand Down