Обработка имен файлов * параметров с пробелами через RFC 5987 приводит к '+' в именах файлов
У меня есть какой-то устаревший код, с которым я имею дело (поэтому я не могу просто использовать URL с закодированным компонентом имени файла), который позволяет пользователю загружать файл с нашего веб-сайта. Поскольку наши имена файлов часто бывают на разных языках, все они хранятся как UTF-8. Я написал некоторый код для обработки преобразования RFC5987 в соответствующий параметр имени файла *. Это прекрасно работает, пока у меня нет имени файла с не-ascii символами и пробелами. В соответствии с RFC символ пробела не является частью attr_char, поэтому он кодируется как%20. У меня есть новые версии Chrome, а также Firefox, и все они конвертируются в% 20 в + при загрузке. Я попытался не кодировать пространство и положить закодированное имя файла в кавычки и получить тот же результат. Я прослушал ответ от сервера, чтобы убедиться, что контейнер сервлета не перебирает мои заголовки, и они выглядят правильно для меня. В RFC даже есть примеры, содержащие%20. Я что-то упустил или у всех этих браузеров есть ошибка, связанная с этим?
Спасибо заранее. Код, который я использую для кодирования имени файла, приведен ниже.
Питер
public static boolean bcsrch(final char[] chars, final char c) {
final int len = chars.length;
int base = 0;
int last = len - 1; /* Last element in table */
int p;
while (last >= base) {
p = base + ((last - base) >> 1);
if (c == chars[p])
return true; /* Key found */
else if (c < chars[p])
last = p - 1;
else
base = p + 1;
}
return false; /* Key not found */
}
public static String rfc5987_encode(final String s) {
final int len = s.length();
final StringBuilder sb = new StringBuilder(len << 1);
final char[] digits = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
final char[] attr_char = {'!','#','$','&','\'','+','-','.','0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','^','_','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','|', '~'};
for (int i = 0; i < len; ++i) {
final char c = s.charAt(i);
if (bcsrch(attr_char, c))
sb.append(c);
else {
final char[] encoded = {'%', 0, 0};
encoded[1] = digits[0x0f & (c >>> 4)];
encoded[2] = digits[c & 0x0f];
sb.append(encoded);
}
}
return sb.toString();
}
Обновить
Вот снимок экрана диалога загрузки, который я получаю для файла с китайскими иероглифами с пробелами, как упомянуто в моем комментарии.
3 ответа
Поэтому, как указал Джулиан в комментариях, я сделал ошибку новичка Java и забыл выполнить преобразование своего символа в байт (таким образом, я закодировал кодовую точку символа вместо байтового представления символа), следовательно, кодировка была совершенно неправильной. Это четко указано как требование в RFC 5987. Я буду публиковать исправленный код для выполнения преобразования. Как только кодировка верна, параметр filename* правильно распознается браузером, а имя файла, использованное для загрузки, является правильным.
Ниже приведен исправленный код перехода, который работает с байтами UTF-8 строки. Имя файла, которое доставляло мне неприятности, теперь правильно закодировано, выглядит так:
Content-Disposition: вложение; Имя файла *=UTF-8''Museum%20%E5%8D%9A%E7%89%A9%E9%A6%86.jpg
public static String rfc5987_encode(final String s) throws UnsupportedEncodingException {
final byte[] s_bytes = s.getBytes("UTF-8");
final int len = s_bytes.length;
final StringBuilder sb = new StringBuilder(len << 1);
final char[] digits = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
final byte[] attr_char = {'!','#','$','&','+','-','.','0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','^','_','`', 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','|', '~'};
for (int i = 0; i < len; ++i) {
final byte b = s_bytes[i];
if (Arrays.binarySearch(attr_char, b) >= 0)
sb.append((char) b);
else {
sb.append('%');
sb.append(digits[0x0f & (b >>> 4)]);
sb.append(digits[b & 0x0f]);
}
}
return sb.toString();
}
обновление 2022 г.
Этот ответ дополняет ответ 10-летней давности , предоставляя информацию о библиотеке apache, в которой есть методы для кодирования и декодирования строк в соответствии с RFC 5987.
Кодер и декодер RFC5987 доступны в классе org.apache.cxf.attachment.Rfc5987Util.
Мне удалось импортировать банку в мой проект maven, добавив зависимость:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-core</artifactId>
<version>3.5.2</version>
</dependency>
(проверьте последнюю версию на https://jar-download.com/artifacts/org.apache.cxf/cxf-core)
Пример теста
@Test
public void verifyRfc5987EncodingandDecoding() throws UnsupportedEncodingException {
final String s = "!\"$£%^&*()_-+={[}]:@~;'#,./<>?\\|✓éèæðŃœ";
assertThat(Rfc5987Util.decode(
Rfc5987Util.encode(s, "UTF-8"),
"UTF-8"),
equalTo(s));
}
В дополнение к ответу @matt-wallis: если вы уже используетеorg.springframework:spring-web
в вашем проекте вы можете использоватьContentDisposition
-строитель:
String contentDispositionHeaderValue = ContentDisposition.attachment()
.filename(someFilename, StandardCharsets.UTF_8)
.build()
.toString();
response.addHeader("Content-Disposition", contentDispositionHeaderValue);