Skip to content

Commit f2faf84

Browse files
committed
Add RFC5987 support for HTTP header field params
This commit adds support for HTTP header field parameters encoding, as described in RFC5987. Note that the default implementation still relies on US-ASCII encoding, as the latest rfc7230 Section 3.2.4 says that: > Newly defined header fields SHOULD limit their field values to US-ASCII octets Issue: SPR-14547
1 parent 41f7680 commit f2faf84

File tree

4 files changed

+88
-3
lines changed

4 files changed

+88
-3
lines changed

spring-core/src/main/java/org/springframework/util/StringUtils.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.util;
1818

19+
import java.nio.charset.Charset;
20+
import java.nio.charset.StandardCharsets;
1921
import java.util.ArrayList;
2022
import java.util.Arrays;
2123
import java.util.Collection;
@@ -50,6 +52,7 @@
5052
* @author Rick Evans
5153
* @author Arjen Poutsma
5254
* @author Sam Brannen
55+
* @author Brian Clozel
5356
* @since 16 April 2001
5457
*/
5558
public abstract class StringUtils {
@@ -1193,4 +1196,44 @@ public static String arrayToCommaDelimitedString(Object[] arr) {
11931196
return arrayToDelimitedString(arr, ",");
11941197
}
11951198

1199+
/**
1200+
* Encode the given header field param as describe in the rfc5987.
1201+
* @param input the header field param
1202+
* @param charset the charset of the header field param string
1203+
* @return the encoded header field param
1204+
* @see <a href="https://tools.ietf.org/html/rfc5987">rfc5987</a>
1205+
* @since 5.0
1206+
*/
1207+
public static String encodeHttpHeaderFieldParam(String input, Charset charset) {
1208+
Assert.notNull(charset, "charset should not be null");
1209+
if(StandardCharsets.US_ASCII.equals(charset)) {
1210+
return input;
1211+
}
1212+
Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset),
1213+
"charset should be UTF-8 or ISO-8859-1");
1214+
final byte[] source = input.getBytes(charset);
1215+
final int len = source.length;
1216+
final StringBuilder sb = new StringBuilder(len << 1);
1217+
sb.append(charset.name());
1218+
sb.append("''");
1219+
for (byte b : source) {
1220+
if (isRFC5987AttrChar(b)) {
1221+
sb.append((char) b);
1222+
}
1223+
else {
1224+
sb.append('%');
1225+
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
1226+
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
1227+
sb.append(hex1);
1228+
sb.append(hex2);
1229+
}
1230+
}
1231+
return sb.toString();
1232+
}
1233+
1234+
private static boolean isRFC5987AttrChar(byte c) {
1235+
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
1236+
|| c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-'
1237+
|| c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
1238+
}
11961239
}

spring-core/src/test/java/org/springframework/util/StringUtilsTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.util;
1818

19+
import java.nio.charset.StandardCharsets;
1920
import java.util.Arrays;
2021
import java.util.Locale;
2122
import java.util.Properties;
@@ -700,4 +701,19 @@ public void testParseLocaleWithVariantContainingCountryCode() {
700701
assertEquals("Variant containing country code not extracted correctly", variant, locale.getVariant());
701702
}
702703

704+
// SPR-14547
705+
@Test
706+
public void encodeHttpHeaderFieldParam() {
707+
String result = StringUtils.encodeHttpHeaderFieldParam("test.txt", StandardCharsets.US_ASCII);
708+
assertEquals("test.txt", result);
709+
710+
result = StringUtils.encodeHttpHeaderFieldParam("中文.txt", StandardCharsets.UTF_8);
711+
assertEquals("UTF-8''%E4%B8%AD%E6%96%87.txt", result);
712+
}
713+
714+
@Test(expected = IllegalArgumentException.class)
715+
public void encodeHttpHeaderFieldParamInvalidCharset() {
716+
StringUtils.encodeHttpHeaderFieldParam("test", StandardCharsets.UTF_16);
717+
}
718+
703719
}

spring-web/src/main/java/org/springframework/http/HttpHeaders.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.Serializable;
2020
import java.net.URI;
2121
import java.nio.charset.Charset;
22+
import java.nio.charset.StandardCharsets;
2223
import java.text.ParseException;
2324
import java.text.SimpleDateFormat;
2425
import java.util.ArrayList;
@@ -672,12 +673,32 @@ public List<String> getConnection() {
672673
* @param filename the filename (may be {@code null})
673674
*/
674675
public void setContentDispositionFormData(String name, String filename) {
676+
setContentDispositionFormData(name, filename, null);
677+
}
678+
679+
/**
680+
* Set the (new) value of the {@code Content-Disposition} header
681+
* for {@code form-data}, optionally encoding the filename using the rfc5987.
682+
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
683+
* @param name the control name
684+
* @param filename the filename (may be {@code null})
685+
* @param charset the charset used for the filename (may be {@code null})
686+
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.4">rfc7230 Section 3.2.4</a>
687+
* @since 5.0
688+
*/
689+
public void setContentDispositionFormData(String name, String filename, Charset charset) {
675690
Assert.notNull(name, "'name' must not be null");
676691
StringBuilder builder = new StringBuilder("form-data; name=\"");
677692
builder.append(name).append('\"');
678693
if (filename != null) {
679-
builder.append("; filename=\"");
680-
builder.append(filename).append('\"');
694+
if(charset == null || StandardCharsets.US_ASCII.equals(charset)) {
695+
builder.append("; filename=\"");
696+
builder.append(filename).append('\"');
697+
}
698+
else {
699+
builder.append("; filename*=");
700+
builder.append(StringUtils.encodeHttpHeaderFieldParam(filename, charset));
701+
}
681702
}
682703
set(CONTENT_DISPOSITION, builder.toString());
683704
}

spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ public void contentDisposition() {
321321
headers.setContentDispositionFormData("name", "filename");
322322
assertEquals("Invalid Content-Disposition header", "form-data; name=\"name\"; filename=\"filename\"",
323323
headers.getFirst("Content-Disposition"));
324+
325+
headers.setContentDispositionFormData("name", "中文.txt", StandardCharsets.UTF_8);
326+
assertEquals("Invalid Content-Disposition header",
327+
"form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt",
328+
headers.getFirst("Content-Disposition"));
324329
}
325330

326331
@Test // SPR-11917

0 commit comments

Comments
 (0)