From f4b05dc2e730ca667daf8861c6eb2d9a6b83d534 Mon Sep 17 00:00:00 2001 From: Dimitrios Liapis Date: Wed, 7 Nov 2018 00:03:20 +0100 Subject: [PATCH] MediaType parsing supports comma inside quotes Issue: SPR-17459 --- .../springframework/util/MimeTypeUtils.java | 23 ++++++-- .../springframework/util/MimeTypeTests.java | 51 ++++++++++++++++++ .../org/springframework/http/MediaType.java | 23 ++++++-- .../springframework/http/MediaTypeTests.java | 53 ++++++++++++++++++- 4 files changed, 139 insertions(+), 11 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index d6581241a3..987d1e8ca6 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -37,6 +37,7 @@ import org.springframework.util.MimeType.SpecificityComparator; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Dimitrios Liapis * @since 4.0 */ public abstract class MimeTypeUtils { @@ -257,12 +258,24 @@ public abstract class MimeTypeUtils { if (!StringUtils.hasLength(mimeTypes)) { return Collections.emptyList(); } - String[] tokens = StringUtils.tokenizeToStringArray(mimeTypes, ","); - List result = new ArrayList<>(tokens.length); - for (String token : tokens) { - result.add(parseMimeType(token)); + boolean isQuoted = false; + int nextBeginIndex = 0; + List tokens = new ArrayList<>(); + for(int i = 0; i < mimeTypes.length() - 1; i++) { + //tokenizing on commas that are not within double quotes + if(mimeTypes.charAt(i) == ',' && !isQuoted) { + tokens.add(parseMimeType(mimeTypes.substring(nextBeginIndex,i))); + nextBeginIndex = i + 1; + //ignoring escaped double quote within double quotes + } else if(isQuoted && mimeTypes.charAt(i) == '"' && mimeTypes.charAt(i-1) == '\\') { + continue; + } else if(mimeTypes.charAt(i) == '"') { + isQuoted = !isQuoted; + } } - return result; + //either the last part of the tokenization or the original string + tokens.add(parseMimeType(mimeTypes.substring(nextBeginIndex))); + return tokens; } /** diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index 904f230820..bf84523a82 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -36,6 +36,7 @@ import static org.junit.Assert.*; * @author Arjen Poutsma * @author Juergen Hoeller * @author Sam Brannen + * @author Dimitrios Liapis */ public class MimeTypeTests { @@ -276,6 +277,56 @@ public class MimeTypeTests { assertEquals("Invalid amount of mime types", 0, mimeTypes.size()); } + // SPR-17459 + @Test + public void parseMimeTypesWithOddNumberOfDoubleQuotedCommas() { + String s = "foo/bar;param=\",\""; + List mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertEquals("Invalid amount of mime types", 1, mimeTypes.size()); + assertEquals("Comma should be part of the mime type", s, mimeTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMimeTypesWithEvenNumberOfDoubleQuotedCommas() { + String s = "foo/bar;param=\"s,a,\""; + List mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertEquals("Invalid amount of mime types", 1, mimeTypes.size()); + assertEquals("Comma should be part of the mime type", s, mimeTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMimeTypesWithAndWithoutDoubleQuotedCommas() { + String s = "foo/bar;param=\"s,\", text/x-c"; + List mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertEquals("Invalid amount of mime types", 2, mimeTypes.size()); + assertEquals("Comma should be part of the mime type", "foo/bar;param=\"s,\"", mimeTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMimeTypesIgnoreEscapedDoubleQuoteWithinDoubleQuotes() { + String s = "foo/bar;param=\"a\\\"b,c\""; + List mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertEquals("Invalid amount of mime types", 1, mimeTypes.size()); + assertEquals("Escaped quote within quotes should be ignored when considering comma tokenization", s, mimeTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMimeTypesIgnoreEscapedBackslash() { + String s = "foo/bar;param=\"\\\\\""; + List mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertEquals("Invalid amount of mime types", 1, mimeTypes.size()); + assertEquals("Escaped backslash should be ignored when considering comma tokenization", s, mimeTypes.get(0).toString()); + + s = "foo/bar;param=\"\\,\\\""; + mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertEquals("Invalid amount of mime types", 1, mimeTypes.size()); + assertEquals("Escaped backslash should be ignored when considering comma tokenization", s, mimeTypes.get(0).toString()); + } + @Test public void compareTo() { MimeType audioBasic = new MimeType("audio", "basic"); diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index d7962f3929..047b1e5db2 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -44,6 +44,7 @@ import org.springframework.util.StringUtils; * @author Rossen Stoyanchev * @author Sebastien Deleuze * @author Kazuki Shimizu + * @author Dimitrios Liapis * @since 3.0 * @see * HTTP 1.1: Semantics and Content, section 3.1.1.1 @@ -552,12 +553,24 @@ public class MediaType extends MimeType implements Serializable { if (!StringUtils.hasLength(mediaTypes)) { return Collections.emptyList(); } - String[] tokens = StringUtils.tokenizeToStringArray(mediaTypes, ","); - List result = new ArrayList<>(tokens.length); - for (String token : tokens) { - result.add(parseMediaType(token)); + boolean isQuoted = false; + int nextBeginIndex = 0; + List tokens = new ArrayList<>(); + for(int i = 0; i < mediaTypes.length() - 1; i++) { + //tokenizing on commas that are not within double quotes + if(mediaTypes.charAt(i) == ',' && !isQuoted) { + tokens.add(parseMediaType(mediaTypes.substring(nextBeginIndex, i))); + nextBeginIndex = i + 1; + //ignoring escaped double quote within double quotes + } else if(isQuoted && mediaTypes.charAt(i) == '"' && mediaTypes.charAt(i-1) == '\\') { + continue; + } else if(mediaTypes.charAt(i) == '"') { + isQuoted = !isQuoted; + } } - return result; + //either the last part of the tokenization or the original string + tokens.add(parseMediaType(mediaTypes.substring(nextBeginIndex))); + return tokens; } /** diff --git a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java index fcee3943c0..e537c1b497 100644 --- a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -32,6 +32,7 @@ import static org.junit.Assert.*; /** * @author Arjen Poutsma * @author Juergen Hoeller + * @author Dimitrios Liapis */ public class MediaTypeTests { @@ -143,6 +144,56 @@ public class MediaTypeTests { assertEquals("Invalid amount of media types", 0, mediaTypes.size()); } + // SPR-17459 + @Test + public void parseMediaTypesWithOddNumberOfDoubleQuotedCommas() { + String s = "foo/bar;param=\",\""; + List mediaTypes = MediaType.parseMediaTypes(s); + assertEquals("Invalid amount of media types", 1, mediaTypes.size()); + assertEquals("Comma should be part of the media type", s, mediaTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMediaTypesWithEvenNumberOfDoubleQuotedCommas() { + String s = "foo/bar;param=\"s,a,\""; + List mediaTypes = MediaType.parseMediaTypes(s); + assertEquals("Invalid amount of media types", 1, mediaTypes.size()); + assertEquals("Comma should be part of the media type", s, mediaTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMediaTypesWithAndWithoutDoubleQuotedCommas() { + String s = "foo/bar;param=\"s,\", text/x-c"; + List mediaTypes = MediaType.parseMediaTypes(s); + assertEquals("Invalid amount of media types", 2, mediaTypes.size()); + assertEquals("Comma should be part of the media type", "foo/bar;param=\"s,\"", mediaTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMediaTypesIgnoreEscapedDoubleQuoteWithinDoubleQuotes() { + String s = "foo/bar;param=\"a\\\"b,c\""; + List mediaTypes = MediaType.parseMediaTypes(s); + assertEquals("Invalid amount of media types", 1, mediaTypes.size()); + assertEquals("Escaped quote within quotes should be ignored when considering comma tokenization", s, mediaTypes.get(0).toString()); + } + + // SPR-17459 + @Test + public void parseMediaTypesIgnoreEscapedBackslash() { + String s = "foo/bar;param=\"\\\\\""; + List mediaTypes = MediaType.parseMediaTypes(s); + assertEquals("Invalid amount of media types", 1, mediaTypes.size()); + assertEquals("Escaped quote within quotes should be ignored when considering comma tokenization", s, mediaTypes.get(0).toString()); + + s = "foo/bar;param=\"\\,\\\""; + mediaTypes = MediaType.parseMediaTypes(s); + assertEquals("Invalid amount of media types", 1, mediaTypes.size()); + assertEquals("Escaped quote within quotes should be ignored when considering comma tokenization", s, mediaTypes.get(0).toString()); + } + @Test public void compareTo() { MediaType audioBasic = new MediaType("audio", "basic");