diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java index 2c63917607..79c5fda97a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java @@ -23,6 +23,7 @@ import io.netty.util.internal.StringUtil; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Map; import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER; @@ -77,6 +78,18 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders { super(nameHashingStrategy, valueConverter, nameValidator); } + @Override + public List getAll(CharSequence name) { + List values = super.getAll(name); + if (values.isEmpty()) { + return values; + } + if (values.size() != 1) { + throw new IllegalStateException("CombinedHttpHeaders should only have one value"); + } + return StringUtil.unescapeCsvFields(values.get(0)); + } + @Override public CombinedHttpHeadersImpl add(Headers headers) { // Override the fast-copy mechanism used by DefaultHeaders @@ -158,6 +171,12 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders { return this; } + @Override + public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) { + super.set(name, commaSeparate(objectEscaper(), value)); + return this; + } + @Override public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) { super.set(name, commaSeparate(objectEscaper(), values)); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java index 59ae6b544d..6ec9bfaf36 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.http; import io.netty.handler.codec.http.HttpHeadersTestUtils.HeaderValue; import org.junit.Test; +import java.util.Arrays; import java.util.Collections; import static io.netty.util.AsciiString.contentEquals; @@ -101,7 +102,7 @@ public class CombinedHttpHeadersTest { final CombinedHttpHeaders headers = newCombinedHttpHeaders(); headers.add(HEADER_NAME, HeaderValue.SIX_QUOTED.subset(4)); assertTrue(contentEquals(HeaderValue.SIX_QUOTED.subsetAsCsvString(4), headers.get(HEADER_NAME))); - assertTrue(contentEquals(HeaderValue.SIX_QUOTED.subsetAsCsvString(4), headers.getAll(HEADER_NAME).get(0))); + assertEquals(HeaderValue.SIX_QUOTED.subset(4), headers.getAll(HEADER_NAME)); } @Test @@ -109,7 +110,7 @@ public class CombinedHttpHeadersTest { final CombinedHttpHeaders headers = newCombinedHttpHeaders(); headers.add(HEADER_NAME, HeaderValue.EIGHT.subset(6)); assertTrue(contentEquals(HeaderValue.EIGHT.subsetAsCsvString(6), headers.get(HEADER_NAME))); - assertTrue(contentEquals(HeaderValue.EIGHT.subsetAsCsvString(6), headers.getAll(HEADER_NAME).get(0))); + assertEquals(HeaderValue.EIGHT.subset(6), headers.getAll(HEADER_NAME)); } @Test (expected = NullPointerException.class) @@ -168,7 +169,7 @@ public class CombinedHttpHeadersTest { public void addIterableCsvEmtpy() { final CombinedHttpHeaders headers = newCombinedHttpHeaders(); headers.add(HEADER_NAME, Collections.emptyList()); - assertTrue(contentEquals("", headers.getAll(HEADER_NAME).get(0))); + assertEquals(Arrays.asList(""), headers.getAll(HEADER_NAME)); } @Test @@ -234,7 +235,7 @@ public class CombinedHttpHeadersTest { private static void assertCsvValues(final CombinedHttpHeaders headers, final HeaderValue headerValue) { assertTrue(contentEquals(headerValue.asCsv(), headers.get(HEADER_NAME))); - assertTrue(contentEquals(headerValue.asCsv(), headers.getAll(HEADER_NAME).get(0))); + assertEquals(headerValue.asList(), headers.getAll(HEADER_NAME)); } private static void assertCsvValue(final CombinedHttpHeaders headers, final HeaderValue headerValue) { @@ -253,4 +254,21 @@ public class CombinedHttpHeadersTest { headers.add(HEADER_NAME, v.toString()); } } + + @Test + public void testGetAll() { + final CombinedHttpHeaders headers = newCombinedHttpHeaders(); + headers.set(HEADER_NAME, Arrays.asList("a", "b", "c")); + assertEquals(Arrays.asList("a", "b", "c"), headers.getAll(HEADER_NAME)); + headers.set(HEADER_NAME, Arrays.asList("a,", "b,", "c,")); + assertEquals(Arrays.asList("a,", "b,", "c,"), headers.getAll(HEADER_NAME)); + headers.set(HEADER_NAME, Arrays.asList("a\"", "b\"", "c\"")); + assertEquals(Arrays.asList("a\"", "b\"", "c\""), headers.getAll(HEADER_NAME)); + headers.set(HEADER_NAME, Arrays.asList("\"a\"", "\"b\"", "\"c\"")); + assertEquals(Arrays.asList("a", "b", "c"), headers.getAll(HEADER_NAME)); + headers.set(HEADER_NAME, "a,b,c"); + assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME)); + headers.set(HEADER_NAME, "\"a,b,c\""); + assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME)); + } } diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java index e60c734bb0..10461e348d 100644 --- a/common/src/main/java/io/netty/util/internal/StringUtil.java +++ b/common/src/main/java/io/netty/util/internal/StringUtil.java @@ -16,6 +16,7 @@ package io.netty.util.internal; import java.io.IOException; +import java.util.ArrayList; import java.util.Formatter; import java.util.List; @@ -425,6 +426,76 @@ public final class StringUtil { } /** + * Unescapes the specified escaped CSV fields according to + * RFC-4180. + * + * @param value A string with multiple CSV escaped fields which will be unescaped according to + * RFC-4180 + * @return {@link List} the list of unescaped fields + */ + public static List unescapeCsvFields(CharSequence value) { + List unescaped = new ArrayList(2); + StringBuilder current = InternalThreadLocalMap.get().stringBuilder(); + boolean quoted = false; + int last = value.length() - 1; + for (int i = 0; i <= last; i++) { + char c = value.charAt(i); + if (quoted) { + switch (c) { + case DOUBLE_QUOTE: + if (i == last) { + // Add the last field and return + unescaped.add(current.toString()); + return unescaped; + } + char next = value.charAt(++i); + if (next == DOUBLE_QUOTE) { + // 2 double-quotes should be unescaped to one + current.append(DOUBLE_QUOTE); + break; + } + if (next == COMMA) { + // This is the end of a field. Let's start to parse the next field. + quoted = false; + unescaped.add(current.toString()); + current.setLength(0); + break; + } + // double-quote followed by other character is invalid + throw newInvalidEscapedCsvFieldException(value, i - 1); + default: + current.append(c); + } + } else { + switch (c) { + case COMMA: + // Start to parse the next field + unescaped.add(current.toString()); + current.setLength(0); + break; + case DOUBLE_QUOTE: + if (current.length() == 0) { + quoted = true; + break; + } + // double-quote appears without being enclosed with double-quotes + case LINE_FEED: + case CARRIAGE_RETURN: + // special characters appears without being enclosed with double-quotes + throw newInvalidEscapedCsvFieldException(value, i); + default: + current.append(c); + } + } + } + if (quoted) { + throw newInvalidEscapedCsvFieldException(value, last); + } + unescaped.add(current.toString()); + return unescaped; + } + + /**s * Validate if {@code value} is a valid csv field without double-quotes. * * @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes. diff --git a/common/src/test/java/io/netty/util/internal/StringUtilTest.java b/common/src/test/java/io/netty/util/internal/StringUtilTest.java index 3084094f00..1ee27e59ff 100644 --- a/common/src/test/java/io/netty/util/internal/StringUtilTest.java +++ b/common/src/test/java/io/netty/util/internal/StringUtilTest.java @@ -15,6 +15,7 @@ */ package io.netty.util.internal; +import java.util.Arrays; import org.junit.Test; import static io.netty.util.internal.StringUtil.*; @@ -376,6 +377,47 @@ public class StringUtilTest { assertEquals(value, unescapeCsv(StringUtil.escapeCsv(value))); } + @Test + public void testUnescapeCsvFields() { + assertEquals(Arrays.asList(""), unescapeCsvFields("")); + assertEquals(Arrays.asList("", ""), unescapeCsvFields(",")); + assertEquals(Arrays.asList("a", ""), unescapeCsvFields("a,")); + assertEquals(Arrays.asList("", "a"), unescapeCsvFields(",a")); + assertEquals(Arrays.asList("\""), unescapeCsvFields("\"\"\"\"")); + assertEquals(Arrays.asList("\"", "\""), unescapeCsvFields("\"\"\"\",\"\"\"\"")); + assertEquals(Arrays.asList("netty"), unescapeCsvFields("netty")); + assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("hello,netty")); + assertEquals(Arrays.asList("hello,netty"), unescapeCsvFields("\"hello,netty\"")); + assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("\"hello\",\"netty\"")); + assertEquals(Arrays.asList("a\"b", "c\"d"), unescapeCsvFields("\"a\"\"b\",\"c\"\"d\"")); + assertEquals(Arrays.asList("a\rb", "c\nd"), unescapeCsvFields("\"a\rb\",\"c\nd\"")); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvFieldsWithCRWithoutQuote() { + unescapeCsvFields("a,\r"); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvFieldsWithLFWithoutQuote() { + unescapeCsvFields("a,\r"); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvFieldsWithQuote() { + unescapeCsvFields("a,\""); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvFieldsWithQuote2() { + unescapeCsvFields("\",a"); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvFieldsWithQuote3() { + unescapeCsvFields("a\"b,a"); + } + @Test public void testSimpleClassName() throws Exception { testSimpleClassName(String.class);