Ensure parsing/tostring symmetry in ContentDisposition

Prior to this commit, building a "Content-Disposition" header to a
String and then parsing it back would not always result in the original
header.

This commit ensures that ContentDisposition guarantees this and honors
the "equals" contract.

Fixes gh-37064
This commit is contained in:
Brian Clozel
2026-08-14 09:11:50 +02:00
parent 1994e0ebd0
commit 062032373e
2 changed files with 27 additions and 17 deletions
@@ -172,17 +172,17 @@ public final class ContentDisposition {
sb.append(this.type);
}
if (this.name != null) {
sb.append("; name=\"").append(this.name).append('\"');
sb.append("; name=\"");
appendName(sb, this.name).append('\"');
}
if (this.filename != null) {
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
sb.append("; filename=\"")
.append(encodeQuotedPairs(this.filename))
.append('\"');
sb.append("; filename=\"");
appendName(sb, this.filename).append('\"');
}
else {
sb.append("; filename=\"")
.append(transliterateToAscii(encodeQuotedPairs(this.filename)))
.append(transliterateToAscii(appendName(new StringBuilder(), this.filename).toString()))
.append("\"; filename*=")
.append(encodeRfc5987Filename(this.filename, this.charset));
}
@@ -253,7 +253,7 @@ public final class ContentDisposition {
part.substring(eqIndex + 2, part.length() - 1) :
part.substring(eqIndex + 1));
if (attribute.equals("name") ) {
name = value;
name = (value.indexOf('\\') != -1 ? decodeQuotedPairs(value) : value);
}
else if (attribute.equals("filename*") ) {
int idx1 = value.indexOf('\'');
@@ -503,19 +503,20 @@ public final class ContentDisposition {
return sb.toString();
}
private static String encodeQuotedPairs(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filename.length() ; i++) {
char c = filename.charAt(i);
if (c == '"' || c == '\\') {
sb.append('\\');
private static StringBuilder appendName(StringBuilder buffer, String name) {
for (int i = 0; i < name.length() ; i++) {
char c = name.charAt(i);
// strip control characters
if (c <= 0x1F || c == 0x7F) {
continue;
}
sb.append(c);
// encode quoted pairs
if (c == '"' || c == '\\') {
buffer.append('\\');
}
buffer.append(c);
}
return sb.toString();
return buffer;
}
private static String decodeQuotedPairs(String filename) {
@@ -181,6 +181,15 @@ class ContentDispositionTests {
assertThat(cd.getFilename()).isEqualTo("foo\\bar \"baz\" qux \\\" quux.txt");
}
@Test
void parseBackslashInName() {
String s = "form-data; name=\"foo\\\"bar\"; filename=\"foo.txt\"";
ContentDisposition cd = ContentDisposition.parse(s);
assertThat(cd.getName()).isEqualTo("foo\"bar");
assertThat(cd.getFilename()).isEqualTo("foo.txt");
assertThat(cd.toString()).isEqualTo(s);
}
@Test
void parseBackslashInLastPosition() {
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"bar\\\"");