parent
44da775134
commit
41f40c6c22
|
@ -458,7 +458,11 @@ public final class ContentDisposition {
|
||||||
Builder name(String name);
|
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);
|
Builder filename(String filename);
|
||||||
|
|
||||||
|
@ -539,10 +543,24 @@ public final class ContentDisposition {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Builder filename(String filename) {
|
public Builder filename(String filename) {
|
||||||
this.filename = filename;
|
Assert.hasText(filename, "No filename");
|
||||||
|
this.filename = escapeQuotationMarks(filename);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String escapeQuotationMarks(String filename) {
|
||||||
|
if (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 == '\\');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Builder filename(String filename, Charset charset) {
|
public Builder filename(String filename, Charset charset) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
|
|
@ -19,11 +19,13 @@ package org.springframework.http;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
import static org.springframework.http.ContentDisposition.builder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link ContentDisposition}
|
* Unit tests for {@link ContentDisposition}
|
||||||
|
@ -38,7 +40,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void parse() {
|
public void parse() {
|
||||||
assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123"))
|
assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123"))
|
||||||
.isEqualTo(ContentDisposition.builder("form-data")
|
.isEqualTo(builder("form-data")
|
||||||
.name("foo")
|
.name("foo")
|
||||||
.filename("foo.txt")
|
.filename("foo.txt")
|
||||||
.size(123L)
|
.size(123L)
|
||||||
|
@ -48,7 +50,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void parseFilenameUnquoted() {
|
public void parseFilenameUnquoted() {
|
||||||
assertThat(parse("form-data; filename=unquoted"))
|
assertThat(parse("form-data; filename=unquoted"))
|
||||||
.isEqualTo(ContentDisposition.builder("form-data")
|
.isEqualTo(builder("form-data")
|
||||||
.filename("unquoted")
|
.filename("unquoted")
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
@ -56,7 +58,7 @@ public class ContentDispositionTests {
|
||||||
@Test // SPR-16091
|
@Test // SPR-16091
|
||||||
public void parseFilenameWithSemicolon() {
|
public void parseFilenameWithSemicolon() {
|
||||||
assertThat(parse("attachment; filename=\"filename with ; semicolon.txt\""))
|
assertThat(parse("attachment; filename=\"filename with ; semicolon.txt\""))
|
||||||
.isEqualTo(ContentDisposition.builder("attachment")
|
.isEqualTo(builder("attachment")
|
||||||
.filename("filename with ; semicolon.txt")
|
.filename("filename with ; semicolon.txt")
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
@ -64,7 +66,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void parseEncodedFilename() {
|
public void parseEncodedFilename() {
|
||||||
assertThat(parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"))
|
assertThat(parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"))
|
||||||
.isEqualTo(ContentDisposition.builder("form-data")
|
.isEqualTo(builder("form-data")
|
||||||
.name("name")
|
.name("name")
|
||||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||||
.build());
|
.build());
|
||||||
|
@ -73,7 +75,7 @@ public class ContentDispositionTests {
|
||||||
@Test // gh-24112
|
@Test // gh-24112
|
||||||
public void parseEncodedFilenameWithPaddedCharset() {
|
public void parseEncodedFilenameWithPaddedCharset() {
|
||||||
assertThat(parse("attachment; filename*= UTF-8''some-file.zip"))
|
assertThat(parse("attachment; filename*= UTF-8''some-file.zip"))
|
||||||
.isEqualTo(ContentDisposition.builder("attachment")
|
.isEqualTo(builder("attachment")
|
||||||
.filename("some-file.zip", StandardCharsets.UTF_8)
|
.filename("some-file.zip", StandardCharsets.UTF_8)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
@ -81,7 +83,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void parseEncodedFilenameWithoutCharset() {
|
public void parseEncodedFilenameWithoutCharset() {
|
||||||
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
||||||
.isEqualTo(ContentDisposition.builder("form-data")
|
.isEqualTo(builder("form-data")
|
||||||
.name("name")
|
.name("name")
|
||||||
.filename("test.txt")
|
.filename("test.txt")
|
||||||
.build());
|
.build());
|
||||||
|
@ -104,18 +106,30 @@ public class ContentDispositionTests {
|
||||||
|
|
||||||
@Test // gh-23077
|
@Test // gh-23077
|
||||||
public void parseWithEscapedQuote() {
|
public void parseWithEscapedQuote() {
|
||||||
assertThat(parse("form-data; name=\"file\"; filename=\"\\\"The Twilight Zone\\\".txt\"; size=123"))
|
|
||||||
.isEqualTo(ContentDisposition.builder("form-data")
|
BiConsumer<String, String> tester = (description, filename) -> {
|
||||||
.name("file")
|
assertThat(parse("form-data; name=\"file\"; filename=\"" + filename + "\"; size=123"))
|
||||||
.filename("\\\"The Twilight Zone\\\".txt")
|
.as(description)
|
||||||
.size(123L)
|
.isEqualTo(builder("form-data").name("file").filename(filename).size(123L).build());
|
||||||
.build());
|
};
|
||||||
|
|
||||||
|
tester.accept("Escaped quotes should be ignored",
|
||||||
|
"\\\"The Twilight Zone\\\".txt");
|
||||||
|
|
||||||
|
tester.accept("Escaped quotes preceded by escaped backslashes should be ignored",
|
||||||
|
"\\\\\\\"The Twilight Zone\\\\\\\".txt");
|
||||||
|
|
||||||
|
tester.accept("Escaped backslashes should not suppress quote",
|
||||||
|
"The Twilight Zone \\\\");
|
||||||
|
|
||||||
|
tester.accept("Escaped backslashes should not suppress quote",
|
||||||
|
"The Twilight Zone \\\\\\\\");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseWithExtraSemicolons() {
|
public void parseWithExtraSemicolons() {
|
||||||
assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123"))
|
assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123"))
|
||||||
.isEqualTo(ContentDisposition.builder("form-data")
|
.isEqualTo(builder("form-data")
|
||||||
.name("foo")
|
.name("foo")
|
||||||
.filename("foo.txt")
|
.filename("foo.txt")
|
||||||
.size(123L)
|
.size(123L)
|
||||||
|
@ -133,7 +147,7 @@ public class ContentDispositionTests {
|
||||||
"creation-date=\"" + creationTime.format(formatter) + "\"; " +
|
"creation-date=\"" + creationTime.format(formatter) + "\"; " +
|
||||||
"modification-date=\"" + modificationTime.format(formatter) + "\"; " +
|
"modification-date=\"" + modificationTime.format(formatter) + "\"; " +
|
||||||
"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
|
"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
|
||||||
ContentDisposition.builder("attachment")
|
builder("attachment")
|
||||||
.creationDate(creationTime)
|
.creationDate(creationTime)
|
||||||
.modificationDate(modificationTime)
|
.modificationDate(modificationTime)
|
||||||
.readDate(readTime)
|
.readDate(readTime)
|
||||||
|
@ -149,7 +163,7 @@ public class ContentDispositionTests {
|
||||||
"creation-date=\"-1\"; " +
|
"creation-date=\"-1\"; " +
|
||||||
"modification-date=\"-1\"; " +
|
"modification-date=\"-1\"; " +
|
||||||
"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
|
"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
|
||||||
ContentDisposition.builder("attachment")
|
builder("attachment")
|
||||||
.readDate(readTime)
|
.readDate(readTime)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
@ -177,7 +191,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void format() {
|
public void format() {
|
||||||
assertThat(
|
assertThat(
|
||||||
ContentDisposition.builder("form-data")
|
builder("form-data")
|
||||||
.name("foo")
|
.name("foo")
|
||||||
.filename("foo.txt")
|
.filename("foo.txt")
|
||||||
.size(123L)
|
.size(123L)
|
||||||
|
@ -188,7 +202,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void formatWithEncodedFilename() {
|
public void formatWithEncodedFilename() {
|
||||||
assertThat(
|
assertThat(
|
||||||
ContentDisposition.builder("form-data")
|
builder("form-data")
|
||||||
.name("name")
|
.name("name")
|
||||||
.filename("中文.txt", StandardCharsets.UTF_8)
|
.filename("中文.txt", StandardCharsets.UTF_8)
|
||||||
.build().toString())
|
.build().toString())
|
||||||
|
@ -198,7 +212,7 @@ public class ContentDispositionTests {
|
||||||
@Test
|
@Test
|
||||||
public void formatWithEncodedFilenameUsingUsAscii() {
|
public void formatWithEncodedFilenameUsingUsAscii() {
|
||||||
assertThat(
|
assertThat(
|
||||||
ContentDisposition.builder("form-data")
|
builder("form-data")
|
||||||
.name("name")
|
.name("name")
|
||||||
.filename("test.txt", StandardCharsets.US_ASCII)
|
.filename("test.txt", StandardCharsets.US_ASCII)
|
||||||
.build()
|
.build()
|
||||||
|
@ -206,10 +220,37 @@ public class ContentDispositionTests {
|
||||||
.isEqualTo("form-data; name=\"name\"; filename=\"test.txt\"");
|
.isEqualTo("form-data; name=\"name\"; filename=\"test.txt\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test // gh-24220
|
||||||
|
public void formatWithFilenameWithQuotes() {
|
||||||
|
|
||||||
|
BiConsumer<String, String> tester = (input, output) -> {
|
||||||
|
assertThat(builder("form-data").filename(input).build().toString())
|
||||||
|
.isEqualTo("form-data; filename=\"" + output + "\"");
|
||||||
|
};
|
||||||
|
|
||||||
|
String filename = "\"foo.txt";
|
||||||
|
tester.accept(filename, "\\" + filename);
|
||||||
|
|
||||||
|
filename = "\\\"foo.txt";
|
||||||
|
tester.accept(filename, filename);
|
||||||
|
|
||||||
|
filename = "\\\\\"foo.txt";
|
||||||
|
tester.accept(filename, "\\" + filename);
|
||||||
|
|
||||||
|
filename = "\\\\\\\"foo.txt";
|
||||||
|
tester.accept(filename, filename);
|
||||||
|
|
||||||
|
filename = "\\\\\\\\\"foo.txt";
|
||||||
|
tester.accept(filename, "\\" + filename);
|
||||||
|
|
||||||
|
tester.accept("\"\"foo.txt", "\\\"\\\"foo.txt");
|
||||||
|
tester.accept("\"\"\"foo.txt", "\\\"\\\"\\\"foo.txt");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void formatWithEncodedFilenameUsingInvalidCharset() {
|
public void formatWithEncodedFilenameUsingInvalidCharset() {
|
||||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||||
ContentDisposition.builder("form-data")
|
builder("form-data")
|
||||||
.name("name")
|
.name("name")
|
||||||
.filename("test.txt", StandardCharsets.UTF_16)
|
.filename("test.txt", StandardCharsets.UTF_16)
|
||||||
.build()
|
.build()
|
||||||
|
|
Loading…
Reference in New Issue