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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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('\"'); sb.append(encodeQuotedPairs(this.filename)).append('\"');
} }
else { else {
sb.append("; filename=\"");
sb.append(encodeQuotedPrintableFilename(this.filename, this.charset)).append('\"');
sb.append("; filename*="); sb.append("; filename*=");
sb.append(encodeFilename(this.filename, this.charset)); sb.append(encodeRfc5987Filename(this.filename, this.charset));
} }
} }
if (this.size != null) { if (this.size != null) {
@ -364,11 +366,11 @@ public final class ContentDisposition {
charset = Charset.forName(value.substring(0, idx1).trim()); charset = Charset.forName(value.substring(0, idx1).trim());
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset must be UTF-8 or ISO-8859-1"); "Charset must be UTF-8 or ISO-8859-1");
filename = decodeFilename(value.substring(idx2 + 1), charset); filename = decodeRfc5987Filename(value.substring(idx2 + 1), charset);
} }
else { else {
// US ASCII // US ASCII
filename = decodeFilename(value, StandardCharsets.US_ASCII); filename = decodeRfc5987Filename(value, StandardCharsets.US_ASCII);
} }
} }
else if (attribute.equals("filename") && (filename == null)) { else if (attribute.equals("filename") && (filename == null)) {
@ -491,7 +493,7 @@ public final class ContentDisposition {
* @return the encoded header field param * @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a> * @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(filename, "'filename' must not be null");
Assert.notNull(charset, "'charset' 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. * Decode the given header field param as described in RFC 2047.
* @param filename the filename * @param filename the filename
* @param charset the charset for 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> * @see <a href="https://tools.ietf.org/html/rfc2047">RFC 2047</a>
*/ */
private static String decodeQuotedPrintableFilename(String filename, Charset charset) { private static String decodeQuotedPrintableFilename(String filename, Charset charset) {
@ -564,6 +566,42 @@ public final class ContentDisposition {
return StreamUtils.copyToString(baos, charset); 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) { private static String encodeQuotedPairs(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename; return filename;
@ -603,15 +641,14 @@ public final class ContentDisposition {
* @return the encoded header field param * @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a> * @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(input, "'input' must not be null");
Assert.notNull(charset, "'charset' 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(!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"); 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); byte[] source = input.getBytes(charset);
int len = source.length; StringBuilder sb = new StringBuilder(source.length << 1);
StringBuilder sb = new StringBuilder(len << 1);
sb.append(charset.name()); sb.append(charset.name());
sb.append("''"); sb.append("''");
for (byte b : source) { for (byte b : source) {
@ -620,8 +657,8 @@ public final class ContentDisposition {
} }
else { else {
sb.append('%'); sb.append('%');
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); char hex1 = hexDigit(b >> 4);
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); char hex2 = hexDigit(b);
sb.append(hex1); sb.append(hex1);
sb.append(hex2); sb.append(hex2);
} }
@ -629,6 +666,10 @@ public final class ContentDisposition {
return sb.toString(); return sb.toString();
} }
private static char hexDigit(int b) {
return Character.toUpperCase(Character.forDigit(b & 0xF, 16));
}
/** /**
* A mutable builder for {@code ContentDisposition}. * 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -249,7 +249,9 @@ class ContentDispositionTests {
.name("name") .name("name")
.filename("中文.txt", StandardCharsets.UTF_8) .filename("中文.txt", StandardCharsets.UTF_8)
.build().toString()) .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 @Test