1
1
/*
2
- * Copyright 2002-2019 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.
42
42
*/
43
43
public final class ContentDisposition {
44
44
45
+ private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
46
+ "Invalid header field parameter format (as defined in RFC 5987)" ;
47
+
48
+
45
49
@ Nullable
46
50
private final String type ;
47
51
@@ -201,11 +205,11 @@ public String toString() {
201
205
if (this .filename != null ) {
202
206
if (this .charset == null || StandardCharsets .US_ASCII .equals (this .charset )) {
203
207
sb .append ("; filename=\" " );
204
- sb .append (this .filename ).append ('\"' );
208
+ sb .append (escapeQuotationsInFilename ( this .filename ) ).append ('\"' );
205
209
}
206
210
else {
207
211
sb .append ("; filename*=" );
208
- sb .append (encodeHeaderFieldParam (this .filename , this .charset ));
212
+ sb .append (encodeFilename (this .filename , this .charset ));
209
213
}
210
214
}
211
215
if (this .size != null ) {
@@ -271,15 +275,23 @@ public static ContentDisposition parse(String contentDisposition) {
271
275
String attribute = part .substring (0 , eqIndex );
272
276
String value = (part .startsWith ("\" " , eqIndex + 1 ) && part .endsWith ("\" " ) ?
273
277
part .substring (eqIndex + 2 , part .length () - 1 ) :
274
- part .substring (eqIndex + 1 , part . length () ));
278
+ part .substring (eqIndex + 1 ));
275
279
if (attribute .equals ("name" ) ) {
276
280
name = value ;
277
281
}
278
282
else if (attribute .equals ("filename*" ) ) {
279
- filename = decodeHeaderFieldParam (value );
280
- charset = Charset .forName (value .substring (0 , value .indexOf ('\'' )).trim ());
281
- Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
282
- "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
+ }
283
295
}
284
296
else if (attribute .equals ("filename" ) && (filename == null )) {
285
297
filename = value ;
@@ -359,22 +371,15 @@ else if (!escaped && ch == '"') {
359
371
/**
360
372
* Decode the given header field param as describe in RFC 5987.
361
373
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
362
- * @param input the header field param
374
+ * @param filename the header field param
375
+ * @param charset the charset to use
363
376
* @return the encoded header field param
364
377
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
365
378
*/
366
- private static String decodeHeaderFieldParam (String input ) {
367
- Assert .notNull (input , "Input String should not be null" );
368
- int firstQuoteIndex = input .indexOf ('\'' );
369
- int secondQuoteIndex = input .indexOf ('\'' , firstQuoteIndex + 1 );
370
- // US_ASCII
371
- if (firstQuoteIndex == -1 || secondQuoteIndex == -1 ) {
372
- return input ;
373
- }
374
- Charset charset = Charset .forName (input .substring (0 , firstQuoteIndex ).trim ());
375
- Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
376
- "Charset should be UTF-8 or ISO-8859-1" );
377
- 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 );
378
383
ByteArrayOutputStream bos = new ByteArrayOutputStream ();
379
384
int index = 0 ;
380
385
while (index < value .length ) {
@@ -383,13 +388,18 @@ private static String decodeHeaderFieldParam(String input) {
383
388
bos .write ((char ) b );
384
389
index ++;
385
390
}
386
- else if (b == '%' ) {
387
- char [] array = { (char )value [index + 1 ], (char )value [index + 2 ]};
388
- 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
+ }
389
399
index +=3 ;
390
400
}
391
401
else {
392
- throw new IllegalArgumentException ("Invalid header field parameter format (as defined in RFC 5987)" );
402
+ throw new IllegalArgumentException (INVALID_HEADER_FIELD_PARAMETER_FORMAT );
393
403
}
394
404
}
395
405
return new String (bos .toByteArray (), charset );
@@ -401,6 +411,23 @@ private static boolean isRFC5987AttrChar(byte c) {
401
411
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~' ;
402
412
}
403
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
+
404
431
/**
405
432
* Encode the given header field param as describe in RFC 5987.
406
433
* @param input the header field param
@@ -409,14 +436,11 @@ private static boolean isRFC5987AttrChar(byte c) {
409
436
* @return the encoded header field param
410
437
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
411
438
*/
412
- private static String encodeHeaderFieldParam (String input , Charset charset ) {
413
- Assert .notNull (input , "Input String should not be null" );
414
- Assert .notNull (charset , "Charset should not be null" );
415
- if (StandardCharsets .US_ASCII .equals (charset )) {
416
- return input ;
417
- }
418
- Assert .isTrue (UTF_8 .equals (charset ) || ISO_8859_1 .equals (charset ),
419
- "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." );
420
444
byte [] source = input .getBytes (charset );
421
445
int len = source .length ;
422
446
StringBuilder sb = new StringBuilder (len << 1 );
@@ -449,7 +473,11 @@ public interface Builder {
449
473
Builder name (String name );
450
474
451
475
/**
452
- * 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"}.
453
481
*/
454
482
Builder filename (String filename );
455
483
@@ -530,12 +558,14 @@ public Builder name(String name) {
530
558
531
559
@ Override
532
560
public Builder filename (String filename ) {
561
+ Assert .hasText (filename , "No filename" );
533
562
this .filename = filename ;
534
563
return this ;
535
564
}
536
565
537
566
@ Override
538
567
public Builder filename (String filename , Charset charset ) {
568
+ Assert .hasText (filename , "No filename" );
539
569
this .filename = filename ;
540
570
this .charset = charset ;
541
571
return this ;
0 commit comments