4545import java .net .http .HttpResponse .BodyHandler ;
4646import java .net .http .HttpResponse .ResponseInfo ;
4747import java .net .http .HttpResponse .BodySubscriber ;
48+ import java .util .Set ;
4849import java .util .regex .Matcher ;
4950import java .util .regex .Pattern ;
5051import jdk .internal .net .http .ResponseSubscribers .PathSubscriber ;
@@ -229,16 +230,120 @@ private FileDownloadBodyHandler(Path directory,
229230 static final String DISPOSITION_TYPE = "attachment;" ;
230231
231232 /** The "filename" parameter. */
232- static final Pattern FILENAME = Pattern .compile ("filename\\ s*=" , CASE_INSENSITIVE );
233+ static final Pattern FILENAME = Pattern .compile ("filename\\ s*=\\ s* " , CASE_INSENSITIVE );
233234
234235 static final List <String > PROHIBITED = List .of ("." , ".." , "" , "~" , "|" );
235236
237+ // Characters disallowed in token values
238+
239+ static final Set <Character > NOT_ALLOWED_IN_TOKEN = Set .of (
240+ '(' , ')' , '<' , '>' , '@' ,
241+ ',' , ';' , ':' , '\\' , '"' ,
242+ '/' , '[' , ']' , '?' , '=' ,
243+ '{' , '}' , ' ' , '\t' );
244+
245+ static boolean allowedInToken (char c ) {
246+ if (NOT_ALLOWED_IN_TOKEN .contains (c ))
247+ return false ;
248+ // exclude CTL chars <= 31, == 127, or anything >= 128
249+ return isTokenText (c );
250+ }
251+
236252 static final UncheckedIOException unchecked (ResponseInfo rinfo ,
237253 String msg ) {
238254 String s = String .format ("%s in response [%d, %s]" , msg , rinfo .statusCode (), rinfo .headers ());
239255 return new UncheckedIOException (new IOException (s ));
240256 }
241257
258+ static final UncheckedIOException unchecked (String msg ) {
259+ return new UncheckedIOException (new IOException (msg ));
260+ }
261+
262+ // Process a "filename=" parameter, which is either a "token"
263+ // or a "quoted string". If a token, it is terminated by a
264+ // semicolon or the end of the string.
265+ // If a quoted string (surrounded by "" chars then the closing "
266+ // terminates the name.
267+ // quoted strings may contain quoted-pairs (eg embedded " chars)
268+
269+ static String processFilename (String src ) throws UncheckedIOException {
270+ if ("" .equals (src ))
271+ return src ;
272+ if (src .charAt (0 ) == '\"' ) {
273+ return processQuotedString (src .substring (1 ));
274+ } else {
275+ return processToken (src );
276+ }
277+ }
278+
279+ static boolean isTokenText (char c ) throws UncheckedIOException {
280+ return c > 31 && c < 127 ;
281+ }
282+
283+ static boolean isQuotedStringText (char c ) throws UncheckedIOException {
284+ return c > 31 ;
285+ }
286+
287+ static String processQuotedString (String src ) throws UncheckedIOException {
288+ boolean inqpair = false ;
289+ int len = src .length ();
290+ StringBuilder sb = new StringBuilder ();
291+
292+ for (int i =0 ; i <len ; i ++) {
293+ char c = src .charAt (i );
294+ if (!isQuotedStringText (c )) {
295+ throw unchecked ("Illegal character" );
296+ }
297+ if (c == '\"' ) {
298+ if (!inqpair ) {
299+ return sb .toString ();
300+ } else {
301+ sb .append (c );
302+ }
303+ } else if (c == '\\' ) {
304+ if (!inqpair ) {
305+ inqpair = true ;
306+ continue ;
307+ } else {
308+ // the quoted char is '\'
309+ sb .append (c );
310+ }
311+ } else {
312+ sb .append (c );
313+ }
314+ if (inqpair ) {
315+ inqpair = false ;
316+ }
317+ }
318+ // not terminated by "
319+ throw unchecked ("Invalid quoted string" );
320+ }
321+
322+ static String processToken (String src ) throws UncheckedIOException {
323+ int end = 0 ;
324+ int len = src .length ();
325+ boolean whitespace = false ;
326+
327+ for (int i =0 ; i <len ; i ++) {
328+ char c = src .charAt (i );
329+ if (c == ';' ) {
330+ break ;
331+ }
332+ if (c == ' ' || c == '\t' ) {
333+ // WS only until ; or end of string
334+ whitespace = true ;
335+ continue ;
336+ }
337+ end ++;
338+ if (whitespace || !allowedInToken (c )) {
339+ String msg = whitespace ? "whitespace must be followed by a semicolon"
340+ : c + " is not allowed in a token" ;
341+ throw unchecked (msg );
342+ }
343+ }
344+ return src .substring (0 , end );
345+ }
346+
242347 @ Override
243348 public BodySubscriber <Path > apply (ResponseInfo responseInfo ) {
244349 String dispoHeader = responseInfo .headers ().firstValue ("Content-Disposition" )
@@ -256,13 +361,7 @@ public BodySubscriber<Path> apply(ResponseInfo responseInfo) {
256361 }
257362 int n = matcher .end ();
258363
259- int semi = dispoHeader .substring (n ).indexOf (";" );
260- String filenameParam ;
261- if (semi < 0 ) {
262- filenameParam = dispoHeader .substring (n );
263- } else {
264- filenameParam = dispoHeader .substring (n , n + semi );
265- }
364+ String filenameParam = processFilename (dispoHeader .substring (n ));
266365
267366 // strip all but the last path segment
268367 int x = filenameParam .lastIndexOf ("/" );
@@ -276,19 +375,6 @@ public BodySubscriber<Path> apply(ResponseInfo responseInfo) {
276375
277376 filenameParam = filenameParam .trim ();
278377
279- if (filenameParam .startsWith ("\" " )) { // quoted-string
280- if (!filenameParam .endsWith ("\" " ) || filenameParam .length () == 1 ) {
281- throw unchecked (responseInfo ,
282- "Badly quoted Content-Disposition filename parameter" );
283- }
284- filenameParam = filenameParam .substring (1 , filenameParam .length () -1 );
285- } else { // token,
286- if (filenameParam .contains (" " )) { // space disallowed
287- throw unchecked (responseInfo ,
288- "unquoted space in Content-Disposition filename parameter" );
289- }
290- }
291-
292378 if (PROHIBITED .contains (filenameParam )) {
293379 throw unchecked (responseInfo ,
294380 "Prohibited Content-Disposition filename parameter:"
0 commit comments