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 843286b1a8..c725123e29 100644
--- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java
+++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -262,8 +262,10 @@ public final class ContentDisposition {
sb.append(encodeQuotedPairs(this.filename)).append('\"');
}
else {
+ sb.append("; filename=\"");
+ sb.append(encodeQuotedPrintableFilename(this.filename, this.charset)).append('\"');
sb.append("; filename*=");
- sb.append(encodeFilename(this.filename, this.charset));
+ sb.append(encodeRfc5987Filename(this.filename, this.charset));
}
}
if (this.size != null) {
@@ -364,11 +366,11 @@ public final class ContentDisposition {
charset = Charset.forName(value.substring(0, idx1).trim());
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset must be UTF-8 or ISO-8859-1");
- filename = decodeFilename(value.substring(idx2 + 1), charset);
+ filename = decodeRfc5987Filename(value.substring(idx2 + 1), charset);
}
else {
// US ASCII
- filename = decodeFilename(value, StandardCharsets.US_ASCII);
+ filename = decodeRfc5987Filename(value, StandardCharsets.US_ASCII);
}
}
else if (attribute.equals("filename") && (filename == null)) {
@@ -491,7 +493,7 @@ public final class ContentDisposition {
* @return the encoded header field param
* @see RFC 5987
*/
- private static String decodeFilename(String filename, Charset charset) {
+ private static String decodeRfc5987Filename(String filename, Charset charset) {
Assert.notNull(filename, "'filename' must not be null");
Assert.notNull(charset, "'charset' must not be null");
@@ -531,7 +533,7 @@ public final class ContentDisposition {
* Decode 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
+ * @return the decoded header field param
* @see RFC 2047
*/
private static String decodeQuotedPrintableFilename(String filename, Charset charset) {
@@ -564,6 +566,42 @@ public final class ContentDisposition {
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 RFC 2047
+ */
+ 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 (isPrintable(b)) {
+ sb.append((char) b);
+ }
+ else {
+ sb.append('=');
+ char ch1 = hexDigit(b >> 4);
+ char ch2 = hexDigit(b);
+ sb.append(ch1);
+ sb.append(ch2);
+ }
+ }
+ sb.append("?=");
+ return sb.toString();
+ }
+
+ private static boolean isPrintable(byte c) {
+ return (c >= '!' && c <= '<') || (c >= '>' && c <= '~');
+ }
+
private static String encodeQuotedPairs(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename;
@@ -603,15 +641,14 @@ public final class ContentDisposition {
* @return the encoded header field param
* @see RFC 5987
*/
- private static String encodeFilename(String input, Charset charset) {
+ private static String encodeRfc5987Filename(String input, Charset charset) {
Assert.notNull(input, "'input' must not be null");
Assert.notNull(charset, "'charset' must not be null");
Assert.isTrue(!StandardCharsets.US_ASCII.equals(charset), "ASCII does not require encoding");
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Only UTF-8 and ISO-8859-1 are supported");
byte[] source = input.getBytes(charset);
- int len = source.length;
- StringBuilder sb = new StringBuilder(len << 1);
+ StringBuilder sb = new StringBuilder(source.length << 1);
sb.append(charset.name());
sb.append("''");
for (byte b : source) {
@@ -620,8 +657,8 @@ public final class ContentDisposition {
}
else {
sb.append('%');
- char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
- char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
+ char hex1 = hexDigit(b >> 4);
+ char hex2 = hexDigit(b);
sb.append(hex1);
sb.append(hex2);
}
@@ -629,6 +666,10 @@ public final class ContentDisposition {
return sb.toString();
}
+ private static char hexDigit(int b) {
+ return Character.toUpperCase(Character.forDigit(b & 0xF, 16));
+ }
+
/**
* A mutable builder for {@code ContentDisposition}.
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 c5434444d6..ca2a7cb0aa 100644
--- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java
+++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -249,7 +249,9 @@ class ContentDispositionTests {
.name("name")
.filename("中文.txt", StandardCharsets.UTF_8)
.build().toString())
- .isEqualTo("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt");
+ .isEqualTo("form-data; name=\"name\"; " +
+ "filename=\"=?UTF-8?Q?=E4=B8=AD=E6=96=87.txt?=\"; " +
+ "filename*=UTF-8''%E4%B8%AD%E6%96%87.txt");
}
@Test