From f43dc7d5513bb6f29a4bcb12ffeea739b64b47c3 Mon Sep 17 00:00:00 2001 From: Xiaoyan Lin Date: Sat, 6 Feb 2016 14:21:07 -0800 Subject: [PATCH] Add unescapeCsv to StringUtil Motivation: See #3435 Modifications: Add unescapeCsv to StringUtil Result: StringUtil has the counter part of escapeCsv: unescapeCsv --- .../io/netty/util/internal/StringUtil.java | 61 +++++++++++++++++++ .../netty/util/internal/StringUtilTest.java | 55 +++++++++++++++++ 2 files changed, 116 insertions(+) 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 473ba597d1..e60c734bb0 100644 --- a/common/src/main/java/io/netty/util/internal/StringUtil.java +++ b/common/src/main/java/io/netty/util/internal/StringUtil.java @@ -387,6 +387,67 @@ public final class StringUtil { escaped.append(DOUBLE_QUOTE) : value; } + /** + * Unescapes the specified escaped CSV field, if necessary according to + * RFC-4180. + * + * @param value The escaped CSV field which will be unescaped according to + * RFC-4180 + * @return {@link CharSequence} the unescaped value if necessary, or the value unchanged + */ + public static CharSequence unescapeCsv(CharSequence value) { + int length = checkNotNull(value, "value").length(); + if (length == 0) { + return value; + } + int last = length - 1; + boolean quoted = isDoubleQuote(value.charAt(0)) && isDoubleQuote(value.charAt(last)) && length != 1; + if (!quoted) { + validateCsvFormat(value); + return value; + } + StringBuilder unescaped = InternalThreadLocalMap.get().stringBuilder(); + for (int i = 1; i < last; i++) { + char current = value.charAt(i); + if (current == DOUBLE_QUOTE) { + if (isDoubleQuote(value.charAt(i + 1)) && (i + 1) != last) { + // Followed by a double-quote but not the last character + // Just skip the next double-quote + i++; + } else { + // Not followed by a double-quote or the following double-quote is the last character + throw newInvalidEscapedCsvFieldException(value, i); + } + } + unescaped.append(current); + } + return unescaped.toString(); + } + + /** + * Validate if {@code value} is a valid csv field without double-quotes. + * + * @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes. + */ + private static void validateCsvFormat(CharSequence value) { + int length = value.length(); + for (int i = 0; i < length; i++) { + switch (value.charAt(i)) { + case DOUBLE_QUOTE: + case LINE_FEED: + case CARRIAGE_RETURN: + case COMMA: + // If value contains any special character, it should be enclosed with double-quotes + throw newInvalidEscapedCsvFieldException(value, i); + default: + } + } + } + + private static IllegalArgumentException newInvalidEscapedCsvFieldException(CharSequence value, int index) { + return new IllegalArgumentException("invalid escaped CSV field: " + value + " index: " + index); + } + /** * Get the length of a string, {@code null} input is considered {@code 0} length. */ 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 66229dacb1..3084094f00 100644 --- a/common/src/test/java/io/netty/util/internal/StringUtilTest.java +++ b/common/src/test/java/io/netty/util/internal/StringUtilTest.java @@ -321,6 +321,61 @@ public class StringUtilTest { } } + @Test + public void testUnescapeCsv() { + assertEquals("", unescapeCsv("")); + assertEquals("\"", unescapeCsv("\"\"\"\"")); + assertEquals("\"\"", unescapeCsv("\"\"\"\"\"\"")); + assertEquals("\"\"\"", unescapeCsv("\"\"\"\"\"\"\"\"")); + assertEquals("\"netty\"", unescapeCsv("\"\"\"netty\"\"\"")); + assertEquals("netty", unescapeCsv("netty")); + assertEquals("netty", unescapeCsv("\"netty\"")); + assertEquals("\r", unescapeCsv("\"\r\"")); + assertEquals("\n", unescapeCsv("\"\n\"")); + assertEquals("hello,netty", unescapeCsv("\"hello,netty\"")); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvWithSingleQuote() { + unescapeCsv("\""); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvWithOddQuote() { + unescapeCsv("\"\"\""); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvWithCRAndWithoutQuote() { + unescapeCsv("\r"); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvWithLFAndWithoutQuote() { + unescapeCsv("\n"); + } + + @Test(expected = IllegalArgumentException.class) + public void unescapeCsvWithCommaAndWithoutQuote() { + unescapeCsv(","); + } + + @Test + public void escapeCsvAndUnEscapeCsv() { + assertEscapeCsvAndUnEscapeCsv(""); + assertEscapeCsvAndUnEscapeCsv("netty"); + assertEscapeCsvAndUnEscapeCsv("hello,netty"); + assertEscapeCsvAndUnEscapeCsv("hello,\"netty\""); + assertEscapeCsvAndUnEscapeCsv("\""); + assertEscapeCsvAndUnEscapeCsv(","); + assertEscapeCsvAndUnEscapeCsv("\r"); + assertEscapeCsvAndUnEscapeCsv("\n"); + } + + private void assertEscapeCsvAndUnEscapeCsv(String value) { + assertEquals(value, unescapeCsv(StringUtil.escapeCsv(value))); + } + @Test public void testSimpleClassName() throws Exception { testSimpleClassName(String.class);