Include quoted printable filename in ContentDisposition::toString

This commit ensures the ContentDisposition class prints the filename in
both in the regular filename parameter and the extended filename*
parameter (RFC 5987).
Quoted printable (RFC 2047) is used to encode any non-ASCII characters
in the regular filename parameter.

Closes gh-29861
This commit is contained in:
Arjen Poutsma 2023-02-01 13:38:19 +01:00
parent ebca9f726f
commit 58fbf60d2d
2 changed files with 56 additions and 13 deletions

View File

@ -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 <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
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 <a href="https://tools.ietf.org/html/rfc2047">RFC 2047</a>
*/
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 <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 (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 <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
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}.

View File

@ -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