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 decodeFilename(String filename, Charset charset) { Assert.notNull(filename, "'input' String` should not be null"); Assert.notNull(charset, "'charset' should 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 == '~'; } private static String escapeQuotationsInFilename(String filename) { if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { return filename; } boolean escaped = false; StringBuilder sb = new StringBuilder(); for (int i = 0; i < filename.length() ; i++) { char c = filename.charAt(i); if (!escaped && c == '"') { sb.append("\\\""); } else { sb.append(c); } escaped = (!escaped && c == '\\'); } // Remove backslash at the end.. if (escaped) { sb.deleteCharAt(sb.length() - 1); } 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 encodeFilename(String input, Charset charset) { Assert.notNull(input, "`input` is required"); Assert.notNull(charset, "`charset` is required"); 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 supported."); byte[] source = input.getBytes(charset); int len = source.length; StringBuilder sb = new StringBuilder(len << 1); sb.append(charset.name()); sb.append("''"); for (byte b : source) { if (isRFC5987AttrChar(b)) { sb.append((char) b); } else { sb.append('%'); char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); sb.append(hex1); sb.append(hex2); } } return sb.toString(); } /** * A mutable builder for {@code ContentDisposition}. */ public interface Builder { /** * Set the value of the {@literal name} parameter. */ Builder name(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(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(String filename, 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(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(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(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(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) { Assert.hasText(filename, "No filename"); this.filename = filename; return this; } @Override public Builder filename(String filename, Charset charset) { Assert.hasText(filename, "No filename"); 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); } } }