diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 843286b1a8..c725123e29 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -262,8 +262,10 @@ public final class ContentDisposition { sb.append(encodeQuotedPairs(this.filename)).append('\"'); } else { + sb.append("; filename=\""); + sb.append(encodeQuotedPrintableFilename(this.filename, this.charset)).append('\"'); sb.append("; filename*="); - sb.append(encodeFilename(this.filename, this.charset)); + sb.append(encodeRfc5987Filename(this.filename, this.charset)); } } if (this.size != null) { @@ -364,11 +366,11 @@ public final class ContentDisposition { charset = Charset.forName(value.substring(0, idx1).trim()); Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Charset must be UTF-8 or ISO-8859-1"); - filename = decodeFilename(value.substring(idx2 + 1), charset); + filename = decodeRfc5987Filename(value.substring(idx2 + 1), charset); } else { // US ASCII - filename = decodeFilename(value, StandardCharsets.US_ASCII); + filename = decodeRfc5987Filename(value, StandardCharsets.US_ASCII); } } else if (attribute.equals("filename") && (filename == null)) { @@ -491,7 +493,7 @@ public final class ContentDisposition { * @return the encoded header field param * @see RFC 5987 */ - private static String decodeFilename(String filename, Charset charset) { + private static String decodeRfc5987Filename(String filename, Charset charset) { Assert.notNull(filename, "'filename' must not be null"); Assert.notNull(charset, "'charset' must not be null"); @@ -531,7 +533,7 @@ public final class ContentDisposition { * Decode the given header field param as described in RFC 2047. * @param filename the filename * @param charset the charset for the filename - * @return the encoded header field param + * @return the decoded header field param * @see RFC 2047 */ private static String decodeQuotedPrintableFilename(String filename, Charset charset) { @@ -564,6 +566,42 @@ public final class ContentDisposition { return StreamUtils.copyToString(baos, charset); } + /** + * Encode the given header field param as described in RFC 2047. + * @param filename the filename + * @param charset the charset for the filename + * @return the encoded header field param + * @see RFC 2047 + */ + private static String encodeQuotedPrintableFilename(String filename, Charset charset) { + Assert.notNull(filename, "'filename' must not be null"); + Assert.notNull(charset, "'charset' must not be null"); + + byte[] source = filename.getBytes(charset); + StringBuilder sb = new StringBuilder(source.length << 1); + sb.append("=?"); + sb.append(charset.name()); + sb.append("?Q?"); + for (byte b : source) { + if (isPrintable(b)) { + sb.append((char) b); + } + else { + sb.append('='); + char ch1 = hexDigit(b >> 4); + char ch2 = hexDigit(b); + sb.append(ch1); + sb.append(ch2); + } + } + sb.append("?="); + return sb.toString(); + } + + private static boolean isPrintable(byte c) { + return (c >= '!' && c <= '<') || (c >= '>' && c <= '~'); + } + private static String encodeQuotedPairs(String filename) { if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { return filename; @@ -603,15 +641,14 @@ public final class ContentDisposition { * @return the encoded header field param * @see RFC 5987 */ - private static String encodeFilename(String input, Charset charset) { + private static String encodeRfc5987Filename(String input, Charset charset) { Assert.notNull(input, "'input' must not be null"); Assert.notNull(charset, "'charset' must not be null"); 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 are supported"); byte[] source = input.getBytes(charset); - int len = source.length; - StringBuilder sb = new StringBuilder(len << 1); + StringBuilder sb = new StringBuilder(source.length << 1); sb.append(charset.name()); sb.append("''"); for (byte b : source) { @@ -620,8 +657,8 @@ public final class ContentDisposition { } else { sb.append('%'); - char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); - char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); + char hex1 = hexDigit(b >> 4); + char hex2 = hexDigit(b); sb.append(hex1); sb.append(hex2); } @@ -629,6 +666,10 @@ public final class ContentDisposition { return sb.toString(); } + private static char hexDigit(int b) { + return Character.toUpperCase(Character.forDigit(b & 0xF, 16)); + } + /** * A mutable builder for {@code ContentDisposition}. diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index c5434444d6..ca2a7cb0aa 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -249,7 +249,9 @@ class ContentDispositionTests { .name("name") .filename("中文.txt", StandardCharsets.UTF_8) .build().toString()) - .isEqualTo("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"); + .isEqualTo("form-data; name=\"name\"; " + + "filename=\"=?UTF-8?Q?=E4=B8=AD=E6=96=87.txt?=\"; " + + "filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"); } @Test