Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. * @param filename the filename * @param charset the charset for the filename * @return the encoded header field param * @see RFC 5987 */ private static String decodeRfc5987Filename(String filename, Charset charset) { Assert.notNull(filename, "'filename' must not be null"); Assert.notNull(charset, "'charset' must not be null"); byte[] value = filename.getBytes(charset); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int index = 0; while (index < value.length) { byte b = value[index]; if (isRFC5987AttrChar(b)) { baos.write((char) b); index++; } else if (b == '%' && index < value.length - 2) { char[] array = new char[]{(char) value[index + 1], (char) value[index + 2]}; try { baos.write(Integer.parseInt(String.valueOf(array), 16)); } catch (NumberFormatException ex) { throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT, ex); } index+=3; } else { throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT); } } return StreamUtils.copyToString(baos, charset); } private static boolean isRFC5987AttrChar(byte c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; } /** * Decode the given header field param as described in RFC 2047. * @param filename the filename * @param charset the charset for the filename * @return the decoded header field param * @see RFC 2047 */ private static String decodeQuotedPrintableFilename(String filename, Charset charset) { Assert.notNull(filename, "'filename' must not be null"); Assert.notNull(charset, "'charset' must not be null"); byte[] value = filename.getBytes(US_ASCII); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int index = 0; while (index < value.length) { byte b = value[index]; if (b == '_') { // RFC 2047, section 4.2, rule (2) baos.write(' '); index++; } else if (b == '=' && index < value.length - 2) { int i1 = Character.digit((char) value[index + 1], 16); int i2 = Character.digit((char) value[index + 2], 16); if (i1 == -1 || i2 == -1) { throw new IllegalArgumentException("Not a valid hex sequence: " + filename.substring(index)); } baos.write((i1 << 4) | i2); index += 3; } else { baos.write(b); index++; } } 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 (b == 32) { // RFC 2047, section 4.2, rule (2) sb.append('_'); } else 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) { int b = c; if (b < 0) { b = 256 + b; } return PRINTABLE.get(b); } 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('\\'); } sb.append(c); } return sb.toString(); } private static String decodeQuotedPairs(String filename) { StringBuilder sb = new StringBuilder(); int length = filename.length(); for (int i = 0; i < length; i++) { char c = filename.charAt(i); if (filename.charAt(i) == '\\' && i + 1 < length) { i++; char next = filename.charAt(i); if (next != '"' && next != '\\') { sb.append(c); } sb.append(next); } else { sb.append(c); } } return sb.toString(); } /** * Encode the given header field param as describe in RFC 5987. * @param input the header field param * @param charset the charset of the header field param string, * only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported * @return the encoded header field param * @see RFC 5987 */ 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); StringBuilder sb = new StringBuilder(source.length << 1); sb.append(charset.name()); sb.append("''"); for (byte b : source) { if (isRFC5987AttrChar(b)) { sb.append((char) b); } else { sb.append('%'); char hex1 = hexDigit(b >> 4); char hex2 = hexDigit(b); sb.append(hex1); sb.append(hex2); } } return sb.toString(); } private static char hexDigit(int b) { return Character.toUpperCase(Character.forDigit(b & 0xF, 16)); } /** * A mutable builder for {@code ContentDisposition}. */ public interface Builder { /** * Set the value of the {@literal name} parameter. */ Builder name(@Nullable String name); /** * Set the value of the {@literal filename} parameter. The given * filename will be formatted as quoted-string, as defined in RFC 2616, * section 2.2, and any quote characters within the filename value will * be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes * {@code "foo\\\"bar.txt"}. */ Builder filename(@Nullable String filename); /** * Set the value of the {@code filename} that will be encoded as * defined in RFC 5987. Only the US-ASCII, UTF-8, and ISO-8859-1 * charsets are supported. *
Note: Do not use this for a * {@code "multipart/form-data"} request since * RFC 7578, Section 4.2 * and also RFC 5987 mention it does not apply to multipart requests. */ Builder filename(@Nullable String filename, @Nullable Charset charset); /** * Set the value of the {@literal size} parameter. * @deprecated since 5.2.3 as per * RFC 6266, Appendix B, * to be removed in a future release. */ @Deprecated Builder size(@Nullable Long size); /** * Set the value of the {@literal creation-date} parameter. * @deprecated since 5.2.3 as per * RFC 6266, Appendix B, * to be removed in a future release. */ @Deprecated Builder creationDate(@Nullable ZonedDateTime creationDate); /** * Set the value of the {@literal modification-date} parameter. * @deprecated since 5.2.3 as per * RFC 6266, Appendix B, * to be removed in a future release. */ @Deprecated Builder modificationDate(@Nullable ZonedDateTime modificationDate); /** * Set the value of the {@literal read-date} parameter. * @deprecated since 5.2.3 as per * RFC 6266, Appendix B, * to be removed in a future release. */ @Deprecated Builder readDate(@Nullable ZonedDateTime readDate); /** * Build the content disposition. */ ContentDisposition build(); } private static class BuilderImpl implements Builder { private final String type; @Nullable private String name; @Nullable private String filename; @Nullable private Charset charset; @Nullable private Long size; @Nullable private ZonedDateTime creationDate; @Nullable private ZonedDateTime modificationDate; @Nullable private ZonedDateTime readDate; public BuilderImpl(String type) { Assert.hasText(type, "'type' must not be not empty"); this.type = type; } @Override public Builder name(String name) { this.name = name; return this; } @Override public Builder filename(String filename) { this.filename = filename; return this; } @Override public Builder filename(String filename, Charset charset) { this.filename = filename; this.charset = charset; return this; } @Override @SuppressWarnings("deprecation") public Builder size(Long size) { this.size = size; return this; } @Override @SuppressWarnings("deprecation") public Builder creationDate(ZonedDateTime creationDate) { this.creationDate = creationDate; return this; } @Override @SuppressWarnings("deprecation") public Builder modificationDate(ZonedDateTime modificationDate) { this.modificationDate = modificationDate; return this; } @Override @SuppressWarnings("deprecation") public Builder readDate(ZonedDateTime readDate) { this.readDate = readDate; return this; } @Override public ContentDisposition build() { return new ContentDisposition(this.type, this.name, this.filename, this.charset, this.size, this.creationDate, this.modificationDate, this.readDate); } } }