1
1
/*
2
- * Copyright 2002-2018 the original author or authors.
2
+ * Copyright 2002-2020 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
28
28
import org .springframework .util .Assert ;
29
29
import org .springframework .util .ObjectUtils ;
30
30
31
- import static java .nio .charset .StandardCharsets .*;
32
- import static java .time .format .DateTimeFormatter .*;
31
+ import static java .nio .charset .StandardCharsets .ISO_8859_1 ;
32
+ import static java .nio .charset .StandardCharsets .UTF_8 ;
33
+ import static java .time .format .DateTimeFormatter .RFC_1123_DATE_TIME ;
33
34
34
35
/**
35
36
* Represent the Content-Disposition type and parameters as defined in RFC 2183.
39
40
* @since 5.0
40
41
* @see <a href="https://tools.ietf.org/html/rfc2183">RFC 2183</a>
41
42
*/
42
- public class ContentDisposition {
43
+ public final class ContentDisposition {
44
+
45
+ private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
46
+ "Invalid header field parameter format (as defined in RFC 5987)" ;
47
+
43
48
44
49
@ Nullable
45
50
private final String type ;
@@ -200,11 +205,11 @@ public String toString() {
200
205
if (this .filename != null ) {
201
206
if (this .charset == null || StandardCharsets .US_ASCII .equals (this .charset )) {
202
207
sb .append ("; filename=\" " );
203
- sb .append (this .filename ).append ('\"' );
208
+ sb .append (escapeQuotationsInFilename ( this .filename ) ).append ('\"' );
204
209
}
205
210
else {
206
211
sb .append ("; filename*=" );
207
- sb .append (encodeHeaderFieldParam (this .filename , this .charset ));
212
+ sb .append (encodeFilename (this .filename , this .charset ));
208
213
}
209
214
}
210
215
if (this .size != null ) {
@@ -270,15 +275,23 @@ public static ContentDisposition parse(String contentDisposition) {
270
275
String attribute = part .substring (0 , eqIndex );
271
276
String value = (part .startsWith ("\" " , eqIndex + 1 ) && part .endsWith ("\" " ) ?
272
277
part .substring (eqIndex + 2 , part .length () - 1 ) :
273
- part .substring (eqIndex + 1 , part . length () ));
278
+ part .substring (eqIndex + 1 ));
274
279
if (attribute .equals ("name" ) ) {
275
280
name = value ;
276
281
}
277
282
else if (attribute .equals ("filename*" ) ) {
278
- filename = decodeHeaderFieldParam (value );
279
- charset = Charset .forName (value .substring (0 , value .indexOf ('\'' )));
280
- Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
281
- "Charset should be UTF-8 or ISO-8859-1" );
283
+ int idx1 = value .indexOf ('\'' );
284
+ int idx2 = value .indexOf ('\'' , idx1 + 1 );
285
+ if (idx1 != -1 && idx2 != -1 ) {
286
+ charset = Charset .forName (value .substring (0 , idx1 ).trim ());
287
+ Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
288
+ "Charset should be UTF-8 or ISO-8859-1" );
289
+ filename = decodeFilename (value .substring (idx2 + 1 ), charset );
290
+ }
291
+ else {
292
+ // US ASCII
293
+ filename = decodeFilename (value , StandardCharsets .US_ASCII );
294
+ }
282
295
}
283
296
else if (attribute .equals ("filename" ) && (filename == null )) {
284
297
filename = value ;
@@ -330,16 +343,18 @@ private static List<String> tokenize(String headerValue) {
330
343
do {
331
344
int nextIndex = index + 1 ;
332
345
boolean quoted = false ;
346
+ boolean escaped = false ;
333
347
while (nextIndex < headerValue .length ()) {
334
348
char ch = headerValue .charAt (nextIndex );
335
349
if (ch == ';' ) {
336
350
if (!quoted ) {
337
351
break ;
338
352
}
339
353
}
340
- else if (ch == '"' ) {
354
+ else if (! escaped && ch == '"' ) {
341
355
quoted = !quoted ;
342
356
}
357
+ escaped = (!escaped && ch == '\\' );
343
358
nextIndex ++;
344
359
}
345
360
String part = headerValue .substring (index + 1 , nextIndex ).trim ();
@@ -356,22 +371,15 @@ else if (ch == '"') {
356
371
/**
357
372
* Decode the given header field param as describe in RFC 5987.
358
373
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
359
- * @param input the header field param
374
+ * @param filename the header field param
375
+ * @param charset the charset to use
360
376
* @return the encoded header field param
361
377
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
362
378
*/
363
- private static String decodeHeaderFieldParam (String input ) {
364
- Assert .notNull (input , "Input String should not be null" );
365
- int firstQuoteIndex = input .indexOf ('\'' );
366
- int secondQuoteIndex = input .indexOf ('\'' , firstQuoteIndex + 1 );
367
- // US_ASCII
368
- if (firstQuoteIndex == -1 || secondQuoteIndex == -1 ) {
369
- return input ;
370
- }
371
- Charset charset = Charset .forName (input .substring (0 , firstQuoteIndex ));
372
- Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
373
- "Charset should be UTF-8 or ISO-8859-1" );
374
- byte [] value = input .substring (secondQuoteIndex + 1 , input .length ()).getBytes (charset );
379
+ private static String decodeFilename (String filename , Charset charset ) {
380
+ Assert .notNull (filename , "'input' String` should not be null" );
381
+ Assert .notNull (charset , "'charset' should not be null" );
382
+ byte [] value = filename .getBytes (charset );
375
383
ByteArrayOutputStream bos = new ByteArrayOutputStream ();
376
384
int index = 0 ;
377
385
while (index < value .length ) {
@@ -380,13 +388,18 @@ private static String decodeHeaderFieldParam(String input) {
380
388
bos .write ((char ) b );
381
389
index ++;
382
390
}
383
- else if (b == '%' ) {
384
- char [] array = { (char )value [index + 1 ], (char )value [index + 2 ]};
385
- bos .write (Integer .parseInt (String .valueOf (array ), 16 ));
391
+ else if (b == '%' && index < value .length - 2 ) {
392
+ char [] array = new char []{(char ) value [index + 1 ], (char ) value [index + 2 ]};
393
+ try {
394
+ bos .write (Integer .parseInt (String .valueOf (array ), 16 ));
395
+ }
396
+ catch (NumberFormatException ex ) {
397
+ throw new IllegalArgumentException (INVALID_HEADER_FIELD_PARAMETER_FORMAT , ex );
398
+ }
386
399
index +=3 ;
387
400
}
388
401
else {
389
- throw new IllegalArgumentException ("Invalid header field parameter format (as defined in RFC 5987)" );
402
+ throw new IllegalArgumentException (INVALID_HEADER_FIELD_PARAMETER_FORMAT );
390
403
}
391
404
}
392
405
return new String (bos .toByteArray (), charset );
@@ -398,6 +411,23 @@ private static boolean isRFC5987AttrChar(byte c) {
398
411
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~' ;
399
412
}
400
413
414
+ private static String escapeQuotationsInFilename (String filename ) {
415
+ if (filename .indexOf ('"' ) == -1 && filename .indexOf ('\\' ) == -1 ) {
416
+ return filename ;
417
+ }
418
+ boolean escaped = false ;
419
+ StringBuilder sb = new StringBuilder ();
420
+ for (char c : filename .toCharArray ()) {
421
+ sb .append ((c == '"' && !escaped ) ? "\\ \" " : c );
422
+ escaped = (!escaped && c == '\\' );
423
+ }
424
+ // Remove backslash at the end..
425
+ if (escaped ) {
426
+ sb .deleteCharAt (sb .length () - 1 );
427
+ }
428
+ return sb .toString ();
429
+ }
430
+
401
431
/**
402
432
* Encode the given header field param as describe in RFC 5987.
403
433
* @param input the header field param
@@ -406,14 +436,11 @@ private static boolean isRFC5987AttrChar(byte c) {
406
436
* @return the encoded header field param
407
437
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
408
438
*/
409
- private static String encodeHeaderFieldParam (String input , Charset charset ) {
410
- Assert .notNull (input , "Input String should not be null" );
411
- Assert .notNull (charset , "Charset should not be null" );
412
- if (StandardCharsets .US_ASCII .equals (charset )) {
413
- return input ;
414
- }
415
- Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
416
- "Charset should be UTF-8 or ISO-8859-1" );
439
+ private static String encodeFilename (String input , Charset charset ) {
440
+ Assert .notNull (input , "`input` is required" );
441
+ Assert .notNull (charset , "`charset` is required" );
442
+ Assert .isTrue (!StandardCharsets .US_ASCII .equals (charset ), "ASCII does not require encoding" );
443
+ Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ), "Only UTF-8 and ISO-8859-1 supported." );
417
444
byte [] source = input .getBytes (charset );
418
445
int len = source .length ;
419
446
StringBuilder sb = new StringBuilder (len << 1 );
@@ -446,7 +473,11 @@ public interface Builder {
446
473
Builder name (String name );
447
474
448
475
/**
449
- * Set the value of the {@literal filename} parameter.
476
+ * Set the value of the {@literal filename} parameter. The given
477
+ * filename will be formatted as quoted-string, as defined in RFC 2616,
478
+ * section 2.2, and any quote characters within the filename value will
479
+ * be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes
480
+ * {@code "foo\\\"bar.txt"}.
450
481
*/
451
482
Builder filename (String filename );
452
483
@@ -527,12 +558,14 @@ public Builder name(String name) {
527
558
528
559
@ Override
529
560
public Builder filename (String filename ) {
561
+ Assert .hasText (filename , "No filename" );
530
562
this .filename = filename ;
531
563
return this ;
532
564
}
533
565
534
566
@ Override
535
567
public Builder filename (String filename , Charset charset ) {
568
+ Assert .hasText (filename , "No filename" );
536
569
this .filename = filename ;
537
570
this .charset = charset ;
538
571
return this ;
0 commit comments