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 8516e898e95..aa7c03580df 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -599,7 +599,7 @@ public final class ContentDisposition { } private static boolean isPrintable(byte c) { - return (c >= '!' && c <= '<') || (c >= '>' && c <= '~'); + return (c >= '!' && c <= '<') || (c >= '@' && c <= '~') || c == '>'; } 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 ac4a8315430..e46a361a262 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -325,4 +325,20 @@ class ContentDispositionTests { 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[] parts = cd.toString().split("; "); + + String quotedPrintableFilename = parts[0] + "; " + parts[1]; + assertThat(ContentDisposition.parse(quotedPrintableFilename).getFilename()) + .isEqualTo(filename); + + String rfc5987Filename = parts[0] + "; " + parts[2]; + assertThat(ContentDisposition.parse(rfc5987Filename).getFilename()) + .isEqualTo(filename); + } }