Skip to content

Commit

Permalink
Escape quotes in filename
Browse files Browse the repository at this point in the history
Also sync up to master and 5.1.x on refactorings in ContentDisposition
and ContentDispositionTests.

Closes spring-projectsgh-24230
  • Loading branch information
rstoyanchev committed Jan 7, 2020
1 parent 8356762 commit 0583b33
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 133 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
Expand Down Expand Up @@ -28,8 +28,9 @@
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import static java.nio.charset.StandardCharsets.*;
import static java.time.format.DateTimeFormatter.*;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;

/**
* Represent the Content-Disposition type and parameters as defined in RFC 2183.
Expand All @@ -39,7 +40,11 @@
* @since 5.0
* @see <a href="https://tools.ietf.org/html/rfc2183">RFC 2183</a>
*/
public class ContentDisposition {
public final class ContentDisposition {

private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
"Invalid header field parameter format (as defined in RFC 5987)";


@Nullable
private final String type;
Expand Down Expand Up @@ -200,11 +205,11 @@ public String toString() {
if (this.filename != null) {
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
sb.append("; filename=\"");
sb.append(this.filename).append('\"');
sb.append(escapeQuotationsInFilename(this.filename)).append('\"');
}
else {
sb.append("; filename*=");
sb.append(encodeHeaderFieldParam(this.filename, this.charset));
sb.append(encodeFilename(this.filename, this.charset));
}
}
if (this.size != null) {
Expand Down Expand Up @@ -270,15 +275,23 @@ public static ContentDisposition parse(String contentDisposition) {
String attribute = part.substring(0, eqIndex);
String value = (part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ?
part.substring(eqIndex + 2, part.length() - 1) :
part.substring(eqIndex + 1, part.length()));
part.substring(eqIndex + 1));
if (attribute.equals("name") ) {
name = value;
}
else if (attribute.equals("filename*") ) {
filename = decodeHeaderFieldParam(value);
charset = Charset.forName(value.substring(0, value.indexOf('\'')));
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
int idx1 = value.indexOf('\'');
int idx2 = value.indexOf('\'', idx1 + 1);
if (idx1 != -1 && idx2 != -1) {
charset = Charset.forName(value.substring(0, idx1).trim());
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
filename = decodeFilename(value.substring(idx2 + 1), charset);
}
else {
// US ASCII
filename = decodeFilename(value, StandardCharsets.US_ASCII);
}
}
else if (attribute.equals("filename") && (filename == null)) {
filename = value;
Expand Down Expand Up @@ -330,16 +343,18 @@ private static List<String> tokenize(String headerValue) {
do {
int nextIndex = index + 1;
boolean quoted = false;
boolean escaped = false;
while (nextIndex < headerValue.length()) {
char ch = headerValue.charAt(nextIndex);
if (ch == ';') {
if (!quoted) {
break;
}
}
else if (ch == '"') {
else if (!escaped && ch == '"') {
quoted = !quoted;
}
escaped = (!escaped && ch == '\\');
nextIndex++;
}
String part = headerValue.substring(index + 1, nextIndex).trim();
Expand All @@ -356,22 +371,15 @@ else if (ch == '"') {
/**
* Decode the given header field param as describe in RFC 5987.
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
* @param input the header field param
* @param filename the header field param
* @param charset the charset to use
* @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
private static String decodeHeaderFieldParam(String input) {
Assert.notNull(input, "Input String should not be null");
int firstQuoteIndex = input.indexOf('\'');
int secondQuoteIndex = input.indexOf('\'', firstQuoteIndex + 1);
// US_ASCII
if (firstQuoteIndex == -1 || secondQuoteIndex == -1) {
return input;
}
Charset charset = Charset.forName(input.substring(0, firstQuoteIndex));
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset);
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 bos = new ByteArrayOutputStream();
int index = 0;
while (index < value.length) {
Expand All @@ -380,13 +388,18 @@ private static String decodeHeaderFieldParam(String input) {
bos.write((char) b);
index++;
}
else if (b == '%') {
char[] array = { (char)value[index + 1], (char)value[index + 2]};
bos.write(Integer.parseInt(String.valueOf(array), 16));
else if (b == '%' && index < value.length - 2) {
char[] array = new char[]{(char) value[index + 1], (char) value[index + 2]};
try {
bos.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 (as defined in RFC 5987)");
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT);
}
}
return new String(bos.toByteArray(), charset);
Expand All @@ -398,6 +411,23 @@ private static boolean isRFC5987AttrChar(byte 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 (char c : filename.toCharArray()) {
sb.append((c == '"' && !escaped) ? "\\\"" : 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
Expand All @@ -406,14 +436,11 @@ private static boolean isRFC5987AttrChar(byte c) {
* @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
private static String encodeHeaderFieldParam(String input, Charset charset) {
Assert.notNull(input, "Input String should not be null");
Assert.notNull(charset, "Charset should not be null");
if (StandardCharsets.US_ASCII.equals(charset)) {
return input;
}
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
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);
Expand Down Expand Up @@ -446,7 +473,11 @@ public interface Builder {
Builder name(String name);

/**
* Set the value of the {@literal filename} parameter.
* 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);

Expand Down Expand Up @@ -527,12 +558,14 @@ public Builder name(String name) {

@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;
Expand Down

0 comments on commit 0583b33

Please sign in to comment.